Merge "JAudioTrack: Add getRoutedDeviceId() and other methods"
diff --git a/camera/aidl/android/hardware/camera2/ICameraDeviceCallbacks.aidl b/camera/aidl/android/hardware/camera2/ICameraDeviceCallbacks.aidl
index 28252c0..4db7f85 100644
--- a/camera/aidl/android/hardware/camera2/ICameraDeviceCallbacks.aidl
+++ b/camera/aidl/android/hardware/camera2/ICameraDeviceCallbacks.aidl
@@ -30,6 +30,7 @@
const int ERROR_CAMERA_REQUEST = 3;
const int ERROR_CAMERA_RESULT = 4;
const int ERROR_CAMERA_BUFFER = 5;
+ const int ERROR_CAMERA_DISABLED = 6;
oneway void onDeviceError(int errorCode, in CaptureResultExtras resultExtras);
oneway void onDeviceIdle();
diff --git a/camera/ndk/include/camera/NdkCameraMetadataTags.h b/camera/ndk/include/camera/NdkCameraMetadataTags.h
index 2c144b7..6e861a6 100644
--- a/camera/ndk/include/camera/NdkCameraMetadataTags.h
+++ b/camera/ndk/include/camera/NdkCameraMetadataTags.h
@@ -1282,7 +1282,7 @@
* <p>State | Transition Cause | New State | Notes
* :------------:|:----------------:|:---------:|:-----------------------:
* INACTIVE | | INACTIVE | Camera device auto exposure algorithm is disabled</p>
- * <p>When ACAMERA_CONTROL_AE_MODE is AE_MODE_ON_*:</p>
+ * <p>When ACAMERA_CONTROL_AE_MODE is AE_MODE_ON*:</p>
* <p>State | Transition Cause | New State | Notes
* :-------------:|:--------------------------------------------:|:--------------:|:-----------------:
* INACTIVE | Camera device initiates AE scan | SEARCHING | Values changing
@@ -1303,10 +1303,13 @@
* LOCKED | aeLock is ON and aePrecaptureTrigger is CANCEL| LOCKED | Precapture trigger is ignored when AE is already locked
* Any state (excluding LOCKED) | ACAMERA_CONTROL_AE_PRECAPTURE_TRIGGER is START | PRECAPTURE | Start AE precapture metering sequence
* Any state (excluding LOCKED) | ACAMERA_CONTROL_AE_PRECAPTURE_TRIGGER is CANCEL| INACTIVE | Currently active precapture metering sequence is canceled</p>
+ * <p>If the camera device supports AE external flash mode (ON_EXTERNAL_FLASH is included in
+ * ACAMERA_CONTROL_AE_AVAILABLE_MODES), aeState must be FLASH_REQUIRED after the camera device
+ * finishes AE scan and it's too dark without flash.</p>
* <p>For the above table, the camera device may skip reporting any state changes that happen
* without application intervention (i.e. mode switch, trigger, locking). Any state that
* can be skipped in that manner is called a transient state.</p>
- * <p>For example, for above AE modes (AE_MODE_ON_*), in addition to the state transitions
+ * <p>For example, for above AE modes (AE_MODE_ON*), in addition to the state transitions
* listed in above table, it is also legal for the camera device to skip one or more
* transient states between two results. See below table for examples:</p>
* <p>State | Transition Cause | New State | Notes
@@ -1319,6 +1322,7 @@
* CONVERGED | Camera device finished AE scan | FLASH_REQUIRED | Converged but too dark w/o flash after a new scan, transient states are skipped by camera device.
* FLASH_REQUIRED | Camera device finished AE scan | CONVERGED | Converged after a new scan, transient states are skipped by camera device.</p>
*
+ * @see ACAMERA_CONTROL_AE_AVAILABLE_MODES
* @see ACAMERA_CONTROL_AE_LOCK
* @see ACAMERA_CONTROL_AE_MODE
* @see ACAMERA_CONTROL_AE_PRECAPTURE_TRIGGER
@@ -5374,6 +5378,19 @@
*/
ACAMERA_CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE = 4,
+ /**
+ * <p>An external flash has been turned on.</p>
+ * <p>It informs the camera device that an external flash has been turned on, and that
+ * metering (and continuous focus if active) should be quickly recaculated to account
+ * for the external flash. Otherwise, this mode acts like ON.</p>
+ * <p>When the external flash is turned off, AE mode should be changed to one of the
+ * other available AE modes.</p>
+ * <p>If the camera device supports AE external flash mode, aeState must be
+ * FLASH_REQUIRED after the camera device finishes AE scan and it's too dark without
+ * flash.</p>
+ */
+ ACAMERA_CONTROL_AE_MODE_ON_EXTERNAL_FLASH = 5,
+
} acamera_metadata_enum_android_control_ae_mode_t;
// ACAMERA_CONTROL_AE_PRECAPTURE_TRIGGER
diff --git a/drm/libmediadrm/DrmHal.cpp b/drm/libmediadrm/DrmHal.cpp
index bc37557..31344c3 100644
--- a/drm/libmediadrm/DrmHal.cpp
+++ b/drm/libmediadrm/DrmHal.cpp
@@ -838,6 +838,11 @@
return toStatusT(status);
}
+status_t DrmHal::getMetrics(MediaAnalyticsItem* metrics) {
+ // TODO: Replace this with real metrics.
+ metrics->setCString("/drm/mediadrm/dummymetric", "dummy");
+ return OK;
+}
status_t DrmHal::setCipherAlgorithm(Vector<uint8_t> const &sessionId,
String8 const &algorithm) {
@@ -1030,6 +1035,7 @@
}
}
+
void DrmHal::reportMetrics() const
{
Vector<uint8_t> metrics;
diff --git a/drm/libmediadrm/IDrm.cpp b/drm/libmediadrm/IDrm.cpp
index 8ff6e6a..d157be7 100644
--- a/drm/libmediadrm/IDrm.cpp
+++ b/drm/libmediadrm/IDrm.cpp
@@ -46,6 +46,7 @@
GET_PROPERTY_BYTE_ARRAY,
SET_PROPERTY_STRING,
SET_PROPERTY_BYTE_ARRAY,
+ GET_METRICS,
SET_CIPHER_ALGORITHM,
SET_MAC_ALGORITHM,
ENCRYPT,
@@ -393,6 +394,18 @@
return reply.readInt32();
}
+ virtual status_t getMetrics(MediaAnalyticsItem *item) {
+ Parcel data, reply;
+ data.writeInterfaceToken(IDrm::getInterfaceDescriptor());
+
+ status_t status = remote()->transact(GET_METRICS, data, &reply);
+ if (status != OK) {
+ return status;
+ }
+
+ item->readFromParcel(reply);
+ return reply.readInt32();
+ }
virtual status_t setCipherAlgorithm(Vector<uint8_t> const &sessionId,
String8 const &algorithm) {
@@ -829,6 +842,17 @@
return OK;
}
+ case GET_METRICS:
+ {
+ CHECK_INTERFACE(IDrm, data, reply);
+
+ MediaAnalyticsItem item;
+ status_t result = getMetrics(&item);
+ item.writeToParcel(reply);
+ reply->writeInt32(result);
+ return OK;
+ }
+
case SET_CIPHER_ALGORITHM:
{
CHECK_INTERFACE(IDrm, data, reply);
diff --git a/include/media/MmapStreamInterface.h b/include/media/MmapStreamInterface.h
index d689e25..0196a0c 100644
--- a/include/media/MmapStreamInterface.h
+++ b/include/media/MmapStreamInterface.h
@@ -52,6 +52,9 @@
* \param[in,out] deviceId audio device the stream should preferably be routed to/from
* Requested as input,
* Actual as output
+ * \param[in,out] sessionId audio sessionId for the stream
+ * Requested as input, may be AUDIO_SESSION_ALLOCATE
+ * Actual as output
* \param[in] callback the MmapStreamCallback interface used by AudioFlinger to notify
* condition changes affecting the stream operation
* \param[out] interface the MmapStreamInterface interface controlling the created stream
@@ -66,6 +69,7 @@
audio_config_base_t *config,
const AudioClient& client,
audio_port_handle_t *deviceId,
+ audio_session_t *sessionId,
const sp<MmapStreamCallback>& callback,
sp<MmapStreamInterface>& interface,
audio_port_handle_t *handle);
diff --git a/media/libaaudio/include/aaudio/AAudio.h b/media/libaaudio/include/aaudio/AAudio.h
index 00c43dc..5da8114 100644
--- a/media/libaaudio/include/aaudio/AAudio.h
+++ b/media/libaaudio/include/aaudio/AAudio.h
@@ -123,17 +123,20 @@
/**
* No particular performance needs. Default.
*/
- AAUDIO_PERFORMANCE_MODE_NONE = 10,
+ AAUDIO_PERFORMANCE_MODE_NONE = 10,
/**
* Extending battery life is most important.
+ *
+ * This mode is not supported in input streams.
+ * Mode NONE will be used if this is requested.
*/
- AAUDIO_PERFORMANCE_MODE_POWER_SAVING,
+ AAUDIO_PERFORMANCE_MODE_POWER_SAVING,
/**
* Reducing latency is most important.
*/
- AAUDIO_PERFORMANCE_MODE_LOW_LATENCY
+ AAUDIO_PERFORMANCE_MODE_LOW_LATENCY
};
typedef int32_t aaudio_performance_mode_t;
@@ -280,6 +283,25 @@
};
typedef int32_t aaudio_input_preset_t;
+enum {
+ /**
+ * Do not allocate a session ID.
+ * Effects cannot be used with this stream.
+ * Default.
+ */
+ AAUDIO_SESSION_ID_NONE = -1,
+
+ /**
+ * Allocate a session ID that can be used to attach and control
+ * effects using the Java AudioEffects API.
+ * Note that the use of this flag may result in higher latency.
+ *
+ * Note that this matches the value of AudioManager.AUDIO_SESSION_ID_GENERATE.
+ */
+ AAUDIO_SESSION_ID_ALLOCATE = 0,
+};
+typedef int32_t aaudio_session_id_t;
+
typedef struct AAudioStreamStruct AAudioStream;
typedef struct AAudioStreamBuilderStruct AAudioStreamBuilder;
@@ -496,6 +518,32 @@
AAUDIO_API void AAudioStreamBuilder_setInputPreset(AAudioStreamBuilder* builder,
aaudio_input_preset_t inputPreset);
+/** Set the requested session ID.
+ *
+ * The session ID can be used to associate a stream with effects processors.
+ * The effects are controlled using the Android AudioEffect Java API.
+ *
+ * The default, if you do not call this function, is AAUDIO_SESSION_ID_NONE.
+ *
+ * If set to AAUDIO_SESSION_ID_ALLOCATE then a session ID will be allocated
+ * when the stream is opened.
+ *
+ * The allocated session ID can be obtained by calling AAudioStream_getSessionId()
+ * and then used with this function when opening another stream.
+ * This allows effects to be shared between streams.
+ *
+ * Session IDs from AAudio can be used the Android Java APIs and vice versa.
+ * So a session ID from an AAudio stream can be passed to Java
+ * and effects applied using the Java AudioEffect API.
+ *
+ * Allocated session IDs will always be positive and nonzero.
+ *
+ * @param builder reference provided by AAudio_createStreamBuilder()
+ * @param sessionId an allocated sessionID or AAUDIO_SESSION_ID_ALLOCATE
+ */
+AAUDIO_API void AAudioStreamBuilder_setSessionId(AAudioStreamBuilder* builder,
+ aaudio_session_id_t sessionId);
+
/**
* Return one of these values from the data callback function.
*/
@@ -526,7 +574,13 @@
* For an input stream, this function should read and process numFrames of data
* from the audioData buffer.
*
- * Note that this callback function should be considered a "real-time" function.
+ * The audio data is passed through the buffer. So do NOT call AAudioStream_read() or
+ * AAudioStream_write() on the stream that is making the callback.
+ *
+ * Note that numFrames can vary unless AAudioStreamBuilder_setFramesPerDataCallback()
+ * is called.
+ *
+ * Also note that this callback function should be considered a "real-time" function.
* It must not do anything that could cause an unbounded delay because that can cause the
* audio to glitch or pop.
*
@@ -537,6 +591,7 @@
* <li>any network operations such as streaming</li>
* <li>use any mutexes or other synchronization primitives</li>
* <li>sleep</li>
+ * <li>stop or close the stream</li>
* </ul>
*
* If you need to move data, eg. MIDI commands, in or out of the callback function then
@@ -545,7 +600,7 @@
* @param stream reference provided by AAudioStreamBuilder_openStream()
* @param userData the same address that was passed to AAudioStreamBuilder_setCallback()
* @param audioData a pointer to the audio data
- * @param numFrames the number of frames to be processed
+ * @param numFrames the number of frames to be processed, which can vary
* @return AAUDIO_CALLBACK_RESULT_*
*/
typedef aaudio_data_callback_result_t (*AAudioStream_dataCallback)(
@@ -620,18 +675,19 @@
aaudio_result_t error);
/**
- * Request that AAudio call this functions if any error occurs on a callback thread.
+ * Request that AAudio call this function if any error occurs or the stream is disconnected.
*
* It will be called, for example, if a headset or a USB device is unplugged causing the stream's
- * device to be unavailable.
- * In response, this function could signal or launch another thread to reopen a
- * stream on another device. Do not reopen the stream in this callback.
- *
- * This will not be called because of actions by the application, such as stopping
- * or closing a stream.
- *
+ * device to be unavailable or "disconnected".
* Another possible cause of error would be a timeout or an unanticipated internal error.
*
+ * In response, this function should signal or create another thread to stop
+ * and close this stream. The other thread could then reopen a stream on another device.
+ * Do not stop or close the stream, or reopen the new stream, directly from this callback.
+ *
+ * This callback will not be called because of actions by the application, such as stopping
+ * or closing a stream.
+ *
* Note that the AAudio callbacks will never be called simultaneously from multiple threads.
*
* @param builder reference provided by AAudio_createStreamBuilder()
@@ -743,11 +799,13 @@
* This will update the current client state.
*
* <pre><code>
- * aaudio_stream_state_t currentState;
- * aaudio_result_t result = AAudioStream_getState(stream, ¤tState);
- * while (result == AAUDIO_OK && currentState != AAUDIO_STREAM_STATE_PAUSING) {
+ * aaudio_result_t result = AAUDIO_OK;
+ * aaudio_stream_state_t currentState = AAudioStream_getState(stream);
+ * aaudio_stream_state_t inputState = currentState;
+ * while (result == AAUDIO_OK && currentState != AAUDIO_STREAM_STATE_PAUSED) {
* result = AAudioStream_waitForStateChange(
- * stream, currentState, ¤tState, MY_TIMEOUT_NANOS);
+ * stream, inputState, ¤tState, MY_TIMEOUT_NANOS);
+ * inputState = currentState;
* }
* </code></pre>
*
@@ -872,10 +930,10 @@
* This call can be used if the application needs to know the value of numFrames before
* the stream is started. This is not normally necessary.
*
- * If a specific size was requested by calling AAudioStreamBuilder_setCallbackSizeInFrames()
+ * If a specific size was requested by calling AAudioStreamBuilder_setFramesPerDataCallback()
* then this will be the same size.
*
- * If AAudioStreamBuilder_setCallbackSizeInFrames() was not called then this will
+ * If AAudioStreamBuilder_setFramesPerDataCallback() was not called then this will
* return the size chosen by AAudio, or AAUDIO_UNSPECIFIED.
*
* AAUDIO_UNSPECIFIED indicates that the callback buffer size for this stream
@@ -983,6 +1041,28 @@
AAUDIO_API int64_t AAudioStream_getFramesRead(AAudioStream* stream);
/**
+ * Passes back the session ID associated with this stream.
+ *
+ * The session ID can be used to associate a stream with effects processors.
+ * The effects are controlled using the Android AudioEffect Java API.
+ *
+ * If AAudioStreamBuilder_setSessionId() was called with AAUDIO_SESSION_ID_ALLOCATE
+ * then a new session ID should be allocated once when the stream is opened.
+ *
+ * If AAudioStreamBuilder_setSessionId() was called with a previously allocated
+ * session ID then that value should be returned.
+ *
+ * If AAudioStreamBuilder_setSessionId() was not called then this function should
+ * return AAUDIO_SESSION_ID_NONE.
+ *
+ * The sessionID for a stream should not change once the stream has been opened.
+ *
+ * @param stream reference provided by AAudioStreamBuilder_openStream()
+ * @return session ID or AAUDIO_SESSION_ID_NONE
+ */
+AAUDIO_API aaudio_session_id_t AAudioStream_getSessionId(AAudioStream* stream);
+
+/**
* Passes back the time at which a particular frame was presented.
* This can be used to synchronize audio with video or MIDI.
* It can also be used to align a recorded stream with a playback stream.
diff --git a/media/libaaudio/libaaudio.map.txt b/media/libaaudio/libaaudio.map.txt
index 98fbb6f..cbf5921 100644
--- a/media/libaaudio/libaaudio.map.txt
+++ b/media/libaaudio/libaaudio.map.txt
@@ -20,6 +20,7 @@
AAudioStreamBuilder_setUsage; # introduced=28
AAudioStreamBuilder_setContentType; # introduced=28
AAudioStreamBuilder_setInputPreset; # introduced=28
+ AAudioStreamBuilder_setSessionId; # introduced=28
AAudioStreamBuilder_openStream;
AAudioStreamBuilder_delete;
AAudioStream_close;
@@ -50,6 +51,7 @@
AAudioStream_getInputPreset; # introduced=28
AAudioStream_getFramesWritten;
AAudioStream_getFramesRead;
+ AAudioStream_getSessionId; # introduced=28
AAudioStream_getTimestamp;
AAudioStream_isMMapUsed;
local:
diff --git a/media/libaaudio/src/binding/AAudioStreamConfiguration.cpp b/media/libaaudio/src/binding/AAudioStreamConfiguration.cpp
index 97672a0..959db61 100644
--- a/media/libaaudio/src/binding/AAudioStreamConfiguration.cpp
+++ b/media/libaaudio/src/binding/AAudioStreamConfiguration.cpp
@@ -56,6 +56,8 @@
if (status != NO_ERROR) goto error;
status = parcel->writeInt32((int32_t) getInputPreset());
if (status != NO_ERROR) goto error;
+ status = parcel->writeInt32(getSessionId());
+ if (status != NO_ERROR) goto error;
return NO_ERROR;
error:
ALOGE("AAudioStreamConfiguration.writeToParcel(): write failed = %d", status);
@@ -94,6 +96,9 @@
status = parcel->readInt32(&value);
if (status != NO_ERROR) goto error;
setInputPreset((aaudio_input_preset_t) value);
+ status = parcel->readInt32(&value);
+ if (status != NO_ERROR) goto error;
+ setSessionId(value);
return NO_ERROR;
error:
ALOGE("AAudioStreamConfiguration.readFromParcel(): read failed = %d", status);
diff --git a/media/libaaudio/src/client/AudioStreamInternal.cpp b/media/libaaudio/src/client/AudioStreamInternal.cpp
index 6d5a64f..4980e97 100644
--- a/media/libaaudio/src/client/AudioStreamInternal.cpp
+++ b/media/libaaudio/src/client/AudioStreamInternal.cpp
@@ -112,6 +112,10 @@
request.getConfiguration().setDirection(getDirection());
request.getConfiguration().setSharingMode(getSharingMode());
+ request.getConfiguration().setUsage(getUsage());
+ request.getConfiguration().setContentType(getContentType());
+ request.getConfiguration().setInputPreset(getInputPreset());
+
request.getConfiguration().setBufferCapacity(builder.getBufferCapacity());
mServiceStreamHandle = mServiceInterface.openStream(request, configurationOutput);
@@ -129,8 +133,13 @@
setSampleRate(configurationOutput.getSampleRate());
setSamplesPerFrame(configurationOutput.getSamplesPerFrame());
setDeviceId(configurationOutput.getDeviceId());
+ setSessionId(configurationOutput.getSessionId());
setSharingMode(configurationOutput.getSharingMode());
+ setUsage(configurationOutput.getUsage());
+ setContentType(configurationOutput.getContentType());
+ setInputPreset(configurationOutput.getInputPreset());
+
// Save device format so we can do format conversion and volume scaling together.
mDeviceFormat = configurationOutput.getFormat();
diff --git a/media/libaaudio/src/core/AAudioAudio.cpp b/media/libaaudio/src/core/AAudioAudio.cpp
index 9e5ca8e..df0db79 100644
--- a/media/libaaudio/src/core/AAudioAudio.cpp
+++ b/media/libaaudio/src/core/AAudioAudio.cpp
@@ -196,12 +196,19 @@
}
AAUDIO_API void AAudioStreamBuilder_setBufferCapacityInFrames(AAudioStreamBuilder* builder,
- int32_t frames)
+ int32_t frames)
{
AudioStreamBuilder *streamBuilder = convertAAudioBuilderToStreamBuilder(builder);
streamBuilder->setBufferCapacity(frames);
}
+AAUDIO_API void AAudioStreamBuilder_setSessionId(AAudioStreamBuilder* builder,
+ aaudio_session_id_t sessionId)
+{
+ AudioStreamBuilder *streamBuilder = convertAAudioBuilderToStreamBuilder(builder);
+ streamBuilder->setSessionId(sessionId);
+}
+
AAUDIO_API void AAudioStreamBuilder_setDataCallback(AAudioStreamBuilder* builder,
AAudioStream_dataCallback callback,
void *userData)
@@ -483,6 +490,12 @@
return audioStream->getInputPreset();
}
+AAUDIO_API int32_t AAudioStream_getSessionId(AAudioStream* stream)
+{
+ AudioStream *audioStream = convertAAudioStreamToAudioStream(stream);
+ return audioStream->getSessionId();
+}
+
AAUDIO_API int64_t AAudioStream_getFramesWritten(AAudioStream* stream)
{
AudioStream *audioStream = convertAAudioStreamToAudioStream(stream);
diff --git a/media/libaaudio/src/core/AAudioStreamParameters.cpp b/media/libaaudio/src/core/AAudioStreamParameters.cpp
index 23c4eb8..9645ea8 100644
--- a/media/libaaudio/src/core/AAudioStreamParameters.cpp
+++ b/media/libaaudio/src/core/AAudioStreamParameters.cpp
@@ -38,6 +38,7 @@
mSamplesPerFrame = other.mSamplesPerFrame;
mSampleRate = other.mSampleRate;
mDeviceId = other.mDeviceId;
+ mSessionId = other.mSessionId;
mSharingMode = other.mSharingMode;
mAudioFormat = other.mAudioFormat;
mDirection = other.mDirection;
@@ -59,6 +60,15 @@
return AAUDIO_ERROR_OUT_OF_RANGE;
}
+ // All Session ID values are legal.
+ switch (mSessionId) {
+ case AAUDIO_SESSION_ID_NONE:
+ case AAUDIO_SESSION_ID_ALLOCATE:
+ break;
+ default:
+ break;
+ }
+
switch (mSharingMode) {
case AAUDIO_SHARING_MODE_EXCLUSIVE:
case AAUDIO_SHARING_MODE_SHARED:
@@ -154,6 +164,7 @@
void AAudioStreamParameters::dump() const {
ALOGD("mDeviceId = %6d", mDeviceId);
+ ALOGD("mSessionId = %6d", mSessionId);
ALOGD("mSampleRate = %6d", mSampleRate);
ALOGD("mSamplesPerFrame = %6d", mSamplesPerFrame);
ALOGD("mSharingMode = %6d", (int)mSharingMode);
@@ -164,4 +175,3 @@
ALOGD("mContentType = %6d", mContentType);
ALOGD("mInputPreset = %6d", mInputPreset);
}
-
diff --git a/media/libaaudio/src/core/AAudioStreamParameters.h b/media/libaaudio/src/core/AAudioStreamParameters.h
index 0c173f5..ce5dacd 100644
--- a/media/libaaudio/src/core/AAudioStreamParameters.h
+++ b/media/libaaudio/src/core/AAudioStreamParameters.h
@@ -112,6 +112,14 @@
mInputPreset = inputPreset;
}
+ aaudio_session_id_t getSessionId() const {
+ return mSessionId;
+ }
+
+ void setSessionId(aaudio_session_id_t sessionId) {
+ mSessionId = sessionId;
+ }
+
int32_t calculateBytesPerFrame() const {
return getSamplesPerFrame() * AAudioConvert_formatToSizeInBytes(getFormat());
}
@@ -137,6 +145,7 @@
aaudio_content_type_t mContentType = AAUDIO_UNSPECIFIED;
aaudio_input_preset_t mInputPreset = AAUDIO_UNSPECIFIED;
int32_t mBufferCapacity = AAUDIO_UNSPECIFIED;
+ aaudio_session_id_t mSessionId = AAUDIO_SESSION_ID_NONE;
};
} /* namespace aaudio */
diff --git a/media/libaaudio/src/core/AudioStream.cpp b/media/libaaudio/src/core/AudioStream.cpp
index 4947e2b..82c0667 100644
--- a/media/libaaudio/src/core/AudioStream.cpp
+++ b/media/libaaudio/src/core/AudioStream.cpp
@@ -108,10 +108,14 @@
mSampleRate, mSamplesPerFrame, mFormat,
AudioStream_convertSharingModeToShortText(mSharingMode),
(getDirection() == AAUDIO_DIRECTION_OUTPUT) ? "OUTPUT" : "INPUT");
- ALOGI("open() device = %d, perfMode = %d, callback: %s with frames = %d",
- mDeviceId, mPerformanceMode,
+ ALOGI("open() device = %d, sessionId = %d, perfMode = %d, callback: %s with frames = %d",
+ mDeviceId,
+ mSessionId,
+ mPerformanceMode,
(isDataCallbackSet() ? "ON" : "OFF"),
mFramesPerDataCallback);
+ ALOGI("open() usage = %d, contentType = %d, inputPreset = %d",
+ mUsage, mContentType, mInputPreset);
return AAUDIO_OK;
}
diff --git a/media/libaaudio/src/core/AudioStream.h b/media/libaaudio/src/core/AudioStream.h
index 82e7189..42b585f 100644
--- a/media/libaaudio/src/core/AudioStream.h
+++ b/media/libaaudio/src/core/AudioStream.h
@@ -216,6 +216,10 @@
return mInputPreset;
}
+ int32_t getSessionId() const {
+ return mSessionId;
+ }
+
/**
* This is only valid after setSamplesPerFrame() and setFormat() have been called.
*/
@@ -420,6 +424,7 @@
/**
* This should not be called after the open() call.
+ * TODO for multiple setters: assert(mState == AAUDIO_STREAM_STATE_UNINITIALIZED)
*/
void setSampleRate(int32_t sampleRate) {
mSampleRate = sampleRate;
@@ -454,10 +459,15 @@
mDeviceId = deviceId;
}
+ void setSessionId(int32_t sessionId) {
+ mSessionId = sessionId;
+ }
+
std::atomic<bool> mCallbackEnabled{false};
float mDuckAndMuteVolume = 1.0f;
+
protected:
void setPeriodNanoseconds(int64_t periodNanoseconds) {
@@ -468,6 +478,27 @@
return mPeriodNanoseconds.load(std::memory_order_acquire);
}
+ /**
+ * This should not be called after the open() call.
+ */
+ void setUsage(aaudio_usage_t usage) {
+ mUsage = usage;
+ }
+
+ /**
+ * This should not be called after the open() call.
+ */
+ void setContentType(aaudio_content_type_t contentType) {
+ mContentType = contentType;
+ }
+
+ /**
+ * This should not be called after the open() call.
+ */
+ void setInputPreset(aaudio_input_preset_t inputPreset) {
+ mInputPreset = inputPreset;
+ }
+
private:
std::mutex mStreamLock;
@@ -483,10 +514,13 @@
aaudio_format_t mFormat = AAUDIO_FORMAT_UNSPECIFIED;
aaudio_stream_state_t mState = AAUDIO_STREAM_STATE_UNINITIALIZED;
aaudio_performance_mode_t mPerformanceMode = AAUDIO_PERFORMANCE_MODE_NONE;
+
aaudio_usage_t mUsage = AAUDIO_USAGE_MEDIA;
aaudio_content_type_t mContentType = AAUDIO_CONTENT_TYPE_MUSIC;
aaudio_input_preset_t mInputPreset = AAUDIO_INPUT_PRESET_GENERIC;
+ int32_t mSessionId = AAUDIO_UNSPECIFIED;
+
// callback ----------------------------------
AAudioStream_dataCallback mDataCallbackProc = nullptr; // external callback functions
diff --git a/media/libaaudio/src/core/AudioStreamBuilder.cpp b/media/libaaudio/src/core/AudioStreamBuilder.cpp
index f7cb8d6..293a6a8 100644
--- a/media/libaaudio/src/core/AudioStreamBuilder.cpp
+++ b/media/libaaudio/src/core/AudioStreamBuilder.cpp
@@ -141,6 +141,13 @@
// TODO Support other performance settings in MMAP mode.
// Disable MMAP if low latency not requested.
if (getPerformanceMode() != AAUDIO_PERFORMANCE_MODE_LOW_LATENCY) {
+ ALOGD("build() MMAP not available because AAUDIO_PERFORMANCE_MODE_LOW_LATENCY not used.");
+ allowMMap = false;
+ }
+
+ // SessionID and Effects are only supported in Legacy mode.
+ if (getSessionId() != AAUDIO_SESSION_ID_NONE) {
+ ALOGD("build() MMAP not available because sessionId used.");
allowMMap = false;
}
diff --git a/media/libaaudio/src/legacy/AudioStreamRecord.cpp b/media/libaaudio/src/legacy/AudioStreamRecord.cpp
index 5f4ab9b..61a0f8a 100644
--- a/media/libaaudio/src/legacy/AudioStreamRecord.cpp
+++ b/media/libaaudio/src/legacy/AudioStreamRecord.cpp
@@ -117,6 +117,9 @@
.tags = ""
};
+ aaudio_session_id_t requestedSessionId = builder.getSessionId();
+ audio_session_t sessionId = AAudioConvert_aaudioToAndroidSessionId(requestedSessionId);
+
mAudioRecord = new AudioRecord(
mOpPackageName // const String16& opPackageName TODO does not compile
);
@@ -130,7 +133,7 @@
callbackData,
notificationFrames,
false /*threadCanCallJava*/,
- AUDIO_SESSION_ALLOCATE,
+ sessionId,
streamTransferType,
flags,
AUDIO_UID_INVALID, // DEFAULT uid
@@ -189,6 +192,13 @@
setState(AAUDIO_STREAM_STATE_OPEN);
setDeviceId(mAudioRecord->getRoutedDeviceId());
+
+ aaudio_session_id_t actualSessionId =
+ (requestedSessionId == AAUDIO_SESSION_ID_NONE)
+ ? AAUDIO_SESSION_ID_NONE
+ : (aaudio_session_id_t) mAudioRecord->getSessionId();
+ setSessionId(actualSessionId);
+
mAudioRecord->addAudioDeviceCallback(mDeviceCallback);
return AAUDIO_OK;
diff --git a/media/libaaudio/src/legacy/AudioStreamTrack.cpp b/media/libaaudio/src/legacy/AudioStreamTrack.cpp
index 17a8d52..52c7822 100644
--- a/media/libaaudio/src/legacy/AudioStreamTrack.cpp
+++ b/media/libaaudio/src/legacy/AudioStreamTrack.cpp
@@ -134,6 +134,11 @@
.tags = ""
};
+ static_assert(AAUDIO_UNSPECIFIED == AUDIO_SESSION_ALLOCATE, "Session IDs should match");
+
+ aaudio_session_id_t requestedSessionId = builder.getSessionId();
+ audio_session_t sessionId = AAudioConvert_aaudioToAndroidSessionId(requestedSessionId);
+
mAudioTrack = new AudioTrack();
mAudioTrack->set(
AUDIO_STREAM_DEFAULT, // ignored because we pass attributes below
@@ -147,7 +152,7 @@
notificationFrames,
0, // DEFAULT sharedBuffer*/,
false, // DEFAULT threadCanCallJava
- AUDIO_SESSION_ALLOCATE,
+ sessionId,
streamTransferType,
NULL, // DEFAULT audio_offload_info_t
AUDIO_UID_INVALID, // DEFAULT uid
@@ -193,6 +198,13 @@
setState(AAUDIO_STREAM_STATE_OPEN);
setDeviceId(mAudioTrack->getRoutedDeviceId());
+
+ aaudio_session_id_t actualSessionId =
+ (requestedSessionId == AAUDIO_SESSION_ID_NONE)
+ ? AAUDIO_SESSION_ID_NONE
+ : (aaudio_session_id_t) mAudioTrack->getSessionId();
+ setSessionId(actualSessionId);
+
mAudioTrack->addAudioDeviceCallback(mDeviceCallback);
// Update performance mode based on the actual stream flags.
diff --git a/media/libaaudio/src/utility/AAudioUtilities.cpp b/media/libaaudio/src/utility/AAudioUtilities.cpp
index c6adf33..2bee6e3 100644
--- a/media/libaaudio/src/utility/AAudioUtilities.cpp
+++ b/media/libaaudio/src/utility/AAudioUtilities.cpp
@@ -250,6 +250,13 @@
return result;
}
+audio_session_t AAudioConvert_aaudioToAndroidSessionId(aaudio_session_id_t sessionId) {
+ // If not a valid sessionId then convert to a safe value of AUDIO_SESSION_ALLOCATE.
+ return (sessionId < AAUDIO_SESSION_ID_MIN)
+ ? AUDIO_SESSION_ALLOCATE
+ : (audio_session_t) sessionId;
+}
+
audio_format_t AAudioConvert_aaudioToAndroidDataFormat(aaudio_format_t aaudioFormat) {
audio_format_t androidFormat;
switch (aaudioFormat) {
diff --git a/media/libaaudio/src/utility/AAudioUtilities.h b/media/libaaudio/src/utility/AAudioUtilities.h
index f2347f5..0c59f6d 100644
--- a/media/libaaudio/src/utility/AAudioUtilities.h
+++ b/media/libaaudio/src/utility/AAudioUtilities.h
@@ -27,6 +27,9 @@
#include "aaudio/AAudio.h"
+
+constexpr aaudio_session_id_t AAUDIO_SESSION_ID_MIN = 1; // must be positive
+
/**
* Convert an AAudio result into the closest matching Android status.
*/
@@ -38,6 +41,13 @@
aaudio_result_t AAudioConvert_androidToAAudioResult(android::status_t status);
/**
+ * Convert an aaudio_session_id_t to a value that is safe to pass to AudioFlinger.
+ * @param sessionId
+ * @return safe value
+ */
+audio_session_t AAudioConvert_aaudioToAndroidSessionId(aaudio_session_id_t sessionId);
+
+/**
* Convert an array of floats to an array of int16_t.
*
* @param source
diff --git a/media/libaaudio/tests/Android.bp b/media/libaaudio/tests/Android.bp
index 33718fc..45d417f 100644
--- a/media/libaaudio/tests/Android.bp
+++ b/media/libaaudio/tests/Android.bp
@@ -113,6 +113,18 @@
}
cc_test {
+ name: "test_session_id",
+ defaults: ["libaaudio_tests_defaults"],
+ srcs: ["test_session_id.cpp"],
+ shared_libs: [
+ "libaaudio",
+ "libbinder",
+ "libcutils",
+ "libutils",
+ ],
+}
+
+cc_test {
name: "test_aaudio_monkey",
defaults: ["libaaudio_tests_defaults"],
srcs: ["test_aaudio_monkey.cpp"],
diff --git a/media/libaaudio/tests/test_session_id.cpp b/media/libaaudio/tests/test_session_id.cpp
new file mode 100644
index 0000000..d9072af
--- /dev/null
+++ b/media/libaaudio/tests/test_session_id.cpp
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+// Test AAudio SessionId, which is used to associate Effects with a stream
+
+#include <stdio.h>
+#include <unistd.h>
+
+#include <aaudio/AAudio.h>
+#include <gtest/gtest.h>
+
+constexpr int64_t kNanosPerSecond = 1000000000;
+constexpr int kNumFrames = 256;
+constexpr int kChannelCount = 2;
+
+// Test AAUDIO_SESSION_ID_NONE default
+static void checkSessionIdNone(aaudio_performance_mode_t perfMode) {
+
+ float *buffer = new float[kNumFrames * kChannelCount];
+
+ AAudioStreamBuilder *aaudioBuilder = nullptr;
+
+ AAudioStream *aaudioStream1 = nullptr;
+ int32_t sessionId1 = 0;
+
+ // Use an AAudioStreamBuilder to contain requested parameters.
+ ASSERT_EQ(AAUDIO_OK, AAudio_createStreamBuilder(&aaudioBuilder));
+
+ // Request stream properties.
+ AAudioStreamBuilder_setPerformanceMode(aaudioBuilder, perfMode);
+
+ // Create an AAudioStream using the Builder.
+ ASSERT_EQ(AAUDIO_OK, AAudioStreamBuilder_openStream(aaudioBuilder, &aaudioStream1));
+
+ // Since we did not request or specify a SessionID, we should get NONE
+ sessionId1 = AAudioStream_getSessionId(aaudioStream1);
+ ASSERT_EQ(AAUDIO_SESSION_ID_NONE, sessionId1);
+
+ ASSERT_EQ(AAUDIO_OK, AAudioStream_requestStart(aaudioStream1));
+
+ ASSERT_EQ(kNumFrames, AAudioStream_write(aaudioStream1, buffer, kNumFrames, kNanosPerSecond));
+
+ EXPECT_EQ(AAUDIO_OK, AAudioStream_requestStop(aaudioStream1));
+
+ EXPECT_EQ(AAUDIO_OK, AAudioStream_close(aaudioStream1));
+ delete[] buffer;
+ AAudioStreamBuilder_delete(aaudioBuilder);
+}
+
+TEST(test_session_id, aaudio_session_id_none_perfnone) {
+ checkSessionIdNone(AAUDIO_PERFORMANCE_MODE_NONE);
+}
+
+TEST(test_session_id, aaudio_session_id_none_lowlat) {
+ checkSessionIdNone(AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
+}
+
+// Test AAUDIO_SESSION_ID_ALLOCATE
+static void checkSessionIdAllocate(aaudio_performance_mode_t perfMode,
+ aaudio_direction_t direction) {
+
+ float *buffer = new float[kNumFrames * kChannelCount];
+
+ AAudioStreamBuilder *aaudioBuilder = nullptr;
+
+ AAudioStream *aaudioStream1 = nullptr;
+ int32_t sessionId1 = 0;
+ AAudioStream *aaudioStream2 = nullptr;
+ int32_t sessionId2 = 0;
+
+ // Use an AAudioStreamBuilder to contain requested parameters.
+ ASSERT_EQ(AAUDIO_OK, AAudio_createStreamBuilder(&aaudioBuilder));
+
+ // Request stream properties.
+ AAudioStreamBuilder_setPerformanceMode(aaudioBuilder, perfMode);
+ // This stream could be input or output.
+ AAudioStreamBuilder_setDirection(aaudioBuilder, direction);
+
+ // Ask AAudio to allocate a Session ID.
+ AAudioStreamBuilder_setSessionId(aaudioBuilder, AAUDIO_SESSION_ID_ALLOCATE);
+
+ // Create an AAudioStream using the Builder.
+ ASSERT_EQ(AAUDIO_OK, AAudioStreamBuilder_openStream(aaudioBuilder, &aaudioStream1));
+
+ // Get the allocated ID from the stream.
+ sessionId1 = AAudioStream_getSessionId(aaudioStream1);
+ ASSERT_LT(0, sessionId1); // Must be positive.
+
+ ASSERT_EQ(AAUDIO_OK, AAudioStream_requestStart(aaudioStream1));
+
+ if (direction == AAUDIO_DIRECTION_INPUT) {
+ ASSERT_EQ(kNumFrames, AAudioStream_read(aaudioStream1,
+ buffer, kNumFrames, kNanosPerSecond));
+ } else {
+ ASSERT_EQ(kNumFrames, AAudioStream_write(aaudioStream1,
+ buffer, kNumFrames, kNanosPerSecond));
+ }
+
+ EXPECT_EQ(AAUDIO_OK, AAudioStream_requestStop(aaudioStream1));
+
+ // Now open a second stream using the same session ID. ==================
+ AAudioStreamBuilder_setSessionId(aaudioBuilder, sessionId1);
+
+ // Reverse direction for second stream.
+ aaudio_direction_t otherDirection = (direction == AAUDIO_DIRECTION_OUTPUT)
+ ? AAUDIO_DIRECTION_INPUT
+ : AAUDIO_DIRECTION_OUTPUT;
+ AAudioStreamBuilder_setDirection(aaudioBuilder, otherDirection);
+
+ // Create an AAudioStream using the Builder.
+ ASSERT_EQ(AAUDIO_OK, AAudioStreamBuilder_openStream(aaudioBuilder, &aaudioStream2));
+
+ // Get the allocated ID from the stream.
+ // It should match the ID that we set it to in the builder.
+ sessionId2 = AAudioStream_getSessionId(aaudioStream2);
+ ASSERT_EQ(sessionId1, sessionId2);
+
+ ASSERT_EQ(AAUDIO_OK, AAudioStream_requestStart(aaudioStream2));
+
+ if (otherDirection == AAUDIO_DIRECTION_INPUT) {
+ ASSERT_EQ(kNumFrames, AAudioStream_read(aaudioStream2,
+ buffer, kNumFrames, kNanosPerSecond));
+ } else {
+ ASSERT_EQ(kNumFrames, AAudioStream_write(aaudioStream2,
+ buffer, kNumFrames, kNanosPerSecond));
+ }
+
+ EXPECT_EQ(AAUDIO_OK, AAudioStream_requestStop(aaudioStream2));
+
+ EXPECT_EQ(AAUDIO_OK, AAudioStream_close(aaudioStream2));
+
+
+ EXPECT_EQ(AAUDIO_OK, AAudioStream_close(aaudioStream1));
+ delete[] buffer;
+ AAudioStreamBuilder_delete(aaudioBuilder);
+}
+
+TEST(test_session_id, aaudio_session_id_alloc_perfnone_in) {
+ checkSessionIdAllocate(AAUDIO_PERFORMANCE_MODE_NONE, AAUDIO_DIRECTION_INPUT);
+}
+TEST(test_session_id, aaudio_session_id_alloc_perfnone_out) {
+ checkSessionIdAllocate(AAUDIO_PERFORMANCE_MODE_NONE, AAUDIO_DIRECTION_OUTPUT);
+}
+
+TEST(test_session_id, aaudio_session_id_alloc_lowlat_in) {
+ checkSessionIdAllocate(AAUDIO_PERFORMANCE_MODE_LOW_LATENCY, AAUDIO_DIRECTION_INPUT);
+}
+TEST(test_session_id, aaudio_session_id_alloc_lowlat_out) {
+ checkSessionIdAllocate(AAUDIO_PERFORMANCE_MODE_LOW_LATENCY, AAUDIO_DIRECTION_OUTPUT);
+}
diff --git a/media/libaudioclient/AudioSystem.cpp b/media/libaudioclient/AudioSystem.cpp
index 50fe385..16150f6 100644
--- a/media/libaudioclient/AudioSystem.cpp
+++ b/media/libaudioclient/AudioSystem.cpp
@@ -39,8 +39,7 @@
sp<AudioSystem::AudioFlingerClient> AudioSystem::gAudioFlingerClient;
audio_error_callback AudioSystem::gAudioErrorCallback = NULL;
dynamic_policy_callback AudioSystem::gDynPolicyCallback = NULL;
-record_config_callback AudioSystem::gRecordConfigCallback = NULL;
-
+record_config_callback AudioSystem::gRecordConfigCallback = NULL;
// establish binder interface to AudioFlinger service
const sp<IAudioFlinger> AudioSystem::get_audio_flinger()
@@ -917,11 +916,14 @@
}
status_t AudioSystem::startInput(audio_io_handle_t input,
- audio_session_t session)
+ audio_session_t session,
+ audio_devices_t device,
+ uid_t uid,
+ bool *silenced)
{
const sp<IAudioPolicyService>& aps = AudioSystem::get_audio_policy_service();
if (aps == 0) return PERMISSION_DENIED;
- return aps->startInput(input, session);
+ return aps->startInput(input, session, device, uid, silenced);
}
status_t AudioSystem::stopInput(audio_io_handle_t input,
diff --git a/media/libaudioclient/IAudioFlinger.cpp b/media/libaudioclient/IAudioFlinger.cpp
index 56ddd4f..6507a5c 100644
--- a/media/libaudioclient/IAudioFlinger.cpp
+++ b/media/libaudioclient/IAudioFlinger.cpp
@@ -48,6 +48,7 @@
SET_MODE,
SET_MIC_MUTE,
GET_MIC_MUTE,
+ SET_RECORD_SILENCED,
SET_PARAMETERS,
GET_PARAMETERS,
REGISTER_CLIENT,
@@ -306,6 +307,15 @@
return reply.readInt32();
}
+ virtual void setRecordSilenced(uid_t uid, bool silenced)
+ {
+ Parcel data, reply;
+ data.writeInterfaceToken(IAudioFlinger::getInterfaceDescriptor());
+ data.writeInt32(uid);
+ data.writeInt32(silenced ? 1 : 0);
+ remote()->transact(SET_RECORD_SILENCED, data, &reply);
+ }
+
virtual status_t setParameters(audio_io_handle_t ioHandle, const String8& keyValuePairs)
{
Parcel data, reply;
@@ -859,6 +869,7 @@
case RELEASE_AUDIO_PATCH:
case LIST_AUDIO_PATCHES:
case SET_AUDIO_PORT_CONFIG:
+ case SET_RECORD_SILENCED:
ALOGW("%s: transaction %d received from PID %d",
__func__, code, IPCThreadState::self()->getCallingPid());
return INVALID_OPERATION;
@@ -1024,6 +1035,15 @@
reply->writeInt32( getMicMute() );
return NO_ERROR;
} break;
+ case SET_RECORD_SILENCED: {
+ CHECK_INTERFACE(IAudioFlinger, data, reply);
+ uid_t uid = data.readInt32();
+ audio_source_t source;
+ data.read(&source, sizeof(audio_source_t));
+ bool silenced = data.readInt32() == 1;
+ setRecordSilenced(uid, silenced);
+ return NO_ERROR;
+ } break;
case SET_PARAMETERS: {
CHECK_INTERFACE(IAudioFlinger, data, reply);
audio_io_handle_t ioHandle = (audio_io_handle_t) data.readInt32();
diff --git a/media/libaudioclient/IAudioPolicyService.cpp b/media/libaudioclient/IAudioPolicyService.cpp
index 53bc1b7..c0e53b3 100644
--- a/media/libaudioclient/IAudioPolicyService.cpp
+++ b/media/libaudioclient/IAudioPolicyService.cpp
@@ -330,14 +330,22 @@
}
virtual status_t startInput(audio_io_handle_t input,
- audio_session_t session)
+ audio_session_t session,
+ audio_devices_t device,
+ uid_t uid,
+ bool *silenced)
{
Parcel data, reply;
data.writeInterfaceToken(IAudioPolicyService::getInterfaceDescriptor());
data.writeInt32(input);
data.writeInt32(session);
+ data.writeInt32(device);
+ data.writeInt32(uid);
+ data.writeInt32(*silenced ? 1 : 0);
remote()->transact(START_INPUT, data, &reply);
- return static_cast <status_t> (reply.readInt32());
+ status_t status = static_cast <status_t> (reply.readInt32());
+ *silenced = reply.readInt32() == 1;
+ return status;
}
virtual status_t stopInput(audio_io_handle_t input,
@@ -1045,7 +1053,12 @@
CHECK_INTERFACE(IAudioPolicyService, data, reply);
audio_io_handle_t input = static_cast <audio_io_handle_t>(data.readInt32());
audio_session_t session = static_cast <audio_session_t>(data.readInt32());
- reply->writeInt32(static_cast <uint32_t>(startInput(input, session)));
+ audio_devices_t device = static_cast <audio_devices_t>(data.readInt32());
+ uid_t uid = static_cast <uid_t>(data.readInt32());
+ bool silenced = data.readInt32() == 1;
+ status_t status = startInput(input, session, device, uid, &silenced);
+ reply->writeInt32(static_cast <uint32_t>(status));
+ reply->writeInt32(silenced ? 1 : 0);
return NO_ERROR;
} break;
diff --git a/media/libaudioclient/include/media/AudioSystem.h b/media/libaudioclient/include/media/AudioSystem.h
index 24a6e22..d6cc37a 100644
--- a/media/libaudioclient/include/media/AudioSystem.h
+++ b/media/libaudioclient/include/media/AudioSystem.h
@@ -244,7 +244,10 @@
audio_port_handle_t *portId);
static status_t startInput(audio_io_handle_t input,
- audio_session_t session);
+ audio_session_t session,
+ audio_devices_t device,
+ uid_t uid,
+ bool *silenced);
static status_t stopInput(audio_io_handle_t input,
audio_session_t session);
static void releaseInput(audio_io_handle_t input,
diff --git a/media/libaudioclient/include/media/IAudioFlinger.h b/media/libaudioclient/include/media/IAudioFlinger.h
index 57d9778..472a3da 100644
--- a/media/libaudioclient/include/media/IAudioFlinger.h
+++ b/media/libaudioclient/include/media/IAudioFlinger.h
@@ -368,6 +368,7 @@
// mic mute/state
virtual status_t setMicMute(bool state) = 0;
virtual bool getMicMute() const = 0;
+ virtual void setRecordSilenced(uid_t uid, bool silenced) = 0;
virtual status_t setParameters(audio_io_handle_t ioHandle,
const String8& keyValuePairs) = 0;
diff --git a/media/libaudioclient/include/media/IAudioPolicyService.h b/media/libaudioclient/include/media/IAudioPolicyService.h
index 5558b77..7e9413d 100644
--- a/media/libaudioclient/include/media/IAudioPolicyService.h
+++ b/media/libaudioclient/include/media/IAudioPolicyService.h
@@ -84,7 +84,10 @@
audio_port_handle_t *selectedDeviceId,
audio_port_handle_t *portId) = 0;
virtual status_t startInput(audio_io_handle_t input,
- audio_session_t session) = 0;
+ audio_session_t session,
+ audio_devices_t device,
+ uid_t uid,
+ bool *silenced) = 0;
virtual status_t stopInput(audio_io_handle_t input,
audio_session_t session) = 0;
virtual void releaseInput(audio_io_handle_t input,
diff --git a/media/libaudioprocessing/AudioMixer.cpp b/media/libaudioprocessing/AudioMixer.cpp
index f8e05e7..5fafb8a 100644
--- a/media/libaudioprocessing/AudioMixer.cpp
+++ b/media/libaudioprocessing/AudioMixer.cpp
@@ -1469,13 +1469,14 @@
int32_t *out = t1.mainBuffer;
size_t numFrames = 0;
do {
+ const size_t frameCount = min((size_t)BLOCKSIZE, state->frameCount - numFrames);
memset(outTemp, 0, sizeof(outTemp));
e2 = e1;
while (e2) {
const int i = 31 - __builtin_clz(e2);
e2 &= ~(1<<i);
track_t& t = state->tracks[i];
- size_t outFrames = BLOCKSIZE;
+ size_t outFrames = frameCount;
int32_t *aux = NULL;
if (CC_UNLIKELY(t.needs & NEEDS_AUX)) {
aux = t.auxBuffer + numFrames;
@@ -1490,7 +1491,7 @@
}
size_t inFrames = (t.frameCount > outFrames)?outFrames:t.frameCount;
if (inFrames > 0) {
- t.hook(&t, outTemp + (BLOCKSIZE - outFrames) * t.mMixerChannelCount,
+ t.hook(&t, outTemp + (frameCount - outFrames) * t.mMixerChannelCount,
inFrames, state->resampleTemp, aux);
t.frameCount -= inFrames;
outFrames -= inFrames;
@@ -1501,7 +1502,7 @@
if (t.frameCount == 0 && outFrames) {
t.bufferProvider->releaseBuffer(&t.buffer);
t.buffer.frameCount = (state->frameCount - numFrames) -
- (BLOCKSIZE - outFrames);
+ (frameCount - outFrames);
t.bufferProvider->getNextBuffer(&t.buffer);
t.in = t.buffer.raw;
if (t.in == NULL) {
@@ -1515,12 +1516,12 @@
}
convertMixerFormat(out, t1.mMixerFormat, outTemp, t1.mMixerInFormat,
- BLOCKSIZE * t1.mMixerChannelCount);
+ frameCount * t1.mMixerChannelCount);
// TODO: fix ugly casting due to choice of out pointer type
out = reinterpret_cast<int32_t*>((uint8_t*)out
- + BLOCKSIZE * t1.mMixerChannelCount
+ + frameCount * t1.mMixerChannelCount
* audio_bytes_per_sample(t1.mMixerFormat));
- numFrames += BLOCKSIZE;
+ numFrames += frameCount;
} while (numFrames < state->frameCount);
}
diff --git a/media/libmedia/Android.bp b/media/libmedia/Android.bp
index e7dc0fe..fd7400a 100644
--- a/media/libmedia/Android.bp
+++ b/media/libmedia/Android.bp
@@ -235,6 +235,8 @@
"-Wall",
],
+ version_script: "exports.lds",
+
sanitize: {
misc_undefined: [
"unsigned-integer-overflow",
diff --git a/media/libmedia/exports.lds b/media/libmedia/exports.lds
new file mode 100644
index 0000000..b09fbce
--- /dev/null
+++ b/media/libmedia/exports.lds
@@ -0,0 +1,7 @@
+{
+ global:
+ *;
+ local:
+ _ZN7android13MidiIoWrapper*;
+ _ZTVN7android13MidiIoWrapperE;
+};
diff --git a/media/libmedia/include/media/DrmHal.h b/media/libmedia/include/media/DrmHal.h
index 5d25e4d..55fbce9 100644
--- a/media/libmedia/include/media/DrmHal.h
+++ b/media/libmedia/include/media/DrmHal.h
@@ -24,6 +24,7 @@
#include <media/IDrm.h>
#include <media/IDrmClient.h>
+#include <media/MediaAnalyticsItem.h>
#include <utils/threads.h>
using ::android::hardware::drm::V1_0::EventType;
@@ -104,6 +105,7 @@
virtual status_t setPropertyString(String8 const &name, String8 const &value ) const;
virtual status_t setPropertyByteArray(String8 const &name,
Vector<uint8_t> const &value ) const;
+ virtual status_t getMetrics(MediaAnalyticsItem *item);
virtual status_t setCipherAlgorithm(Vector<uint8_t> const &sessionId,
String8 const &algorithm);
diff --git a/media/libmedia/include/media/IDrm.h b/media/libmedia/include/media/IDrm.h
index a57e372..ce0360b 100644
--- a/media/libmedia/include/media/IDrm.h
+++ b/media/libmedia/include/media/IDrm.h
@@ -18,6 +18,7 @@
#include <media/stagefright/foundation/ABase.h>
#include <media/drm/DrmAPI.h>
#include <media/IDrmClient.h>
+#include <media/MediaAnalyticsItem.h>
#ifndef ANDROID_IDRM_H_
@@ -86,6 +87,8 @@
virtual status_t setPropertyByteArray(String8 const &name,
Vector<uint8_t> const &value) const = 0;
+ virtual status_t getMetrics(MediaAnalyticsItem *item) = 0;
+
virtual status_t setCipherAlgorithm(Vector<uint8_t> const &sessionId,
String8 const &algorithm) = 0;
diff --git a/media/libstagefright/ACodec.cpp b/media/libstagefright/ACodec.cpp
index d9fdfe3..14ea2a8 100644
--- a/media/libstagefright/ACodec.cpp
+++ b/media/libstagefright/ACodec.cpp
@@ -551,6 +551,8 @@
mDescribeHDRStaticInfoIndex((OMX_INDEXTYPE)0),
mStateGeneration(0),
mVendorExtensionsStatus(kExtensionsUnchecked) {
+ memset(&mLastHDRStaticInfo, 0, sizeof(mLastHDRStaticInfo));
+
mUninitializedState = new UninitializedState(this);
mLoadedState = new LoadedState(this);
mLoadedToIdleState = new LoadedToIdleState(this);
@@ -6103,6 +6105,14 @@
mCodec->mLastNativeWindowDataSpace = dataSpace;
ALOGW_IF(err != NO_ERROR, "failed to set dataspace: %d", err);
}
+ if (buffer->format()->contains("hdr-static-info")) {
+ HDRStaticInfo info;
+ if (ColorUtils::getHDRStaticInfoFromFormat(buffer->format(), &info)
+ && memcmp(&mCodec->mLastHDRStaticInfo, &info, sizeof(info))) {
+ setNativeWindowHdrMetadata(mCodec->mNativeWindow.get(), &info);
+ mCodec->mLastHDRStaticInfo = info;
+ }
+ }
// save buffers sent to the surface so we can get render time when they return
int64_t mediaTimeUs = -1;
diff --git a/media/libstagefright/Android.bp b/media/libstagefright/Android.bp
index e16db00..c3d9c24 100644
--- a/media/libstagefright/Android.bp
+++ b/media/libstagefright/Android.bp
@@ -152,6 +152,8 @@
"-Wall",
],
+ version_script: "exports.lds",
+
product_variables: {
debuggable: {
// enable experiments only in userdebug and eng builds
diff --git a/media/libstagefright/MediaCodec.cpp b/media/libstagefright/MediaCodec.cpp
index 7cfa4ce..77d9ce4 100644
--- a/media/libstagefright/MediaCodec.cpp
+++ b/media/libstagefright/MediaCodec.cpp
@@ -1832,6 +1832,12 @@
mSurface.get(), (android_dataspace)dataSpace);
ALOGW_IF(err != 0, "failed to set dataspace on surface (%d)", err);
}
+ if (mOutputFormat->contains("hdr-static-info")) {
+ HDRStaticInfo info;
+ if (ColorUtils::getHDRStaticInfoFromFormat(mOutputFormat, &info)) {
+ setNativeWindowHdrMetadata(mSurface.get(), &info);
+ }
+ }
if (mime.startsWithIgnoreCase("video/")) {
mSoftRenderer = new SoftwareRenderer(mSurface, mRotationDegrees);
diff --git a/media/libstagefright/MediaExtractorFactory.cpp b/media/libstagefright/MediaExtractorFactory.cpp
index a777663..bbf87f0 100644
--- a/media/libstagefright/MediaExtractorFactory.cpp
+++ b/media/libstagefright/MediaExtractorFactory.cpp
@@ -146,9 +146,14 @@
MediaExtractor::ExtractorDef def;
void *libHandle;
String8 libPath;
+ String8 uuidString;
ExtractorPlugin(MediaExtractor::ExtractorDef definition, void *handle, String8 &path)
- : def(definition), libHandle(handle), libPath(path) { }
+ : def(definition), libHandle(handle), libPath(path) {
+ for (size_t i = 0; i < sizeof MediaExtractor::ExtractorDef::extractor_uuid; i++) {
+ uuidString.appendFormat("%02x", def.extractor_uuid.b[i]);
+ }
+ }
~ExtractorPlugin() {
if (libHandle != nullptr) {
ALOGV("closing handle for %s %d", libPath.c_str(), def.extractor_version);
@@ -307,4 +312,20 @@
gPluginsRegistered = true;
}
+status_t MediaExtractorFactory::dump(int fd, const Vector<String16>&) {
+ Mutex::Autolock autoLock(gPluginMutex);
+ String8 out;
+ out.append("Available extractors:\n");
+ for (auto it = gPlugins->begin(); it != gPlugins->end(); ++it) {
+ out.appendFormat(" %25s: uuid(%s), version(%u), path(%s)\n",
+ (*it)->def.extractor_name,
+ (*it)->uuidString.c_str(),
+ (*it)->def.extractor_version,
+ (*it)->libPath.c_str());
+ }
+ write(fd, out.string(), out.size());
+ return OK;
+}
+
+
} // namespace android
diff --git a/media/libstagefright/SurfaceUtils.cpp b/media/libstagefright/SurfaceUtils.cpp
index b7c1598..9e11a94 100644
--- a/media/libstagefright/SurfaceUtils.cpp
+++ b/media/libstagefright/SurfaceUtils.cpp
@@ -18,8 +18,8 @@
#define LOG_TAG "SurfaceUtils"
#include <utils/Log.h>
+#include <media/hardware/VideoAPI.h>
#include <media/stagefright/SurfaceUtils.h>
-
#include <gui/Surface.h>
namespace android {
@@ -128,6 +128,40 @@
return NO_ERROR;
}
+void setNativeWindowHdrMetadata(ANativeWindow *nativeWindow, HDRStaticInfo *info) {
+ struct android_smpte2086_metadata smpte2086_meta = {
+ .displayPrimaryRed = {
+ info->sType1.mR.x * 0.00002f,
+ info->sType1.mR.y * 0.00002f
+ },
+ .displayPrimaryGreen = {
+ info->sType1.mG.x * 0.00002f,
+ info->sType1.mG.y * 0.00002f
+ },
+ .displayPrimaryBlue = {
+ info->sType1.mB.x * 0.00002f,
+ info->sType1.mB.y * 0.00002f
+ },
+ .whitePoint = {
+ info->sType1.mW.x * 0.00002f,
+ info->sType1.mW.y * 0.00002f
+ },
+ .maxLuminance = (float) info->sType1.mMaxDisplayLuminance,
+ .minLuminance = info->sType1.mMinDisplayLuminance * 0.0001f
+ };
+
+ int err = native_window_set_buffers_smpte2086_metadata(nativeWindow, &smpte2086_meta);
+ ALOGW_IF(err != 0, "failed to set smpte2086 metadata on surface (%d)", err);
+
+ struct android_cta861_3_metadata cta861_meta = {
+ .maxContentLightLevel = (float) info->sType1.mMaxContentLightLevel,
+ .maxFrameAverageLightLevel = (float) info->sType1.mMaxFrameAverageLightLevel
+ };
+
+ err = native_window_set_buffers_cta861_3_metadata(nativeWindow, &cta861_meta);
+ ALOGW_IF(err != 0, "failed to set cta861_3 metadata on surface (%d)", err);
+}
+
status_t pushBlankBuffersToNativeWindow(ANativeWindow *nativeWindow /* nonnull */) {
status_t err = NO_ERROR;
ANativeWindowBuffer* anb = NULL;
diff --git a/media/libstagefright/codec2/SimpleC2Interface.cpp b/media/libstagefright/codec2/SimpleC2Interface.cpp
index f9cab26..f082243 100644
--- a/media/libstagefright/codec2/SimpleC2Interface.cpp
+++ b/media/libstagefright/codec2/SimpleC2Interface.cpp
@@ -23,7 +23,7 @@
namespace android {
c2_status_t SimpleC2Interface::query_vb(
- const std::vector<C2Param* const> &stackParams,
+ const std::vector<C2Param*> &stackParams,
const std::vector<C2Param::Index> &heapParamIndices,
c2_blocking_t mayBlock,
std::vector<std::unique_ptr<C2Param>>* const heapParams) const {
diff --git a/media/libstagefright/codec2/include/C2Buffer.h b/media/libstagefright/codec2/include/C2Buffer.h
index df9362c..cd90978 100644
--- a/media/libstagefright/codec2/include/C2Buffer.h
+++ b/media/libstagefright/codec2/include/C2Buffer.h
@@ -284,6 +284,7 @@
/// @{
public:
inline uint32_t offset() const { return mOffset; }
+ inline uint32_t endOffset() const { return mOffset + mSize; }
inline uint32_t size() const { return mSize; }
protected:
@@ -311,6 +312,32 @@
/// @}
};
+class C2_HIDE _C2LinearCapacityBase : public _C2LinearCapacityAspect {
+public:
+ inline explicit _C2LinearCapacityBase(size_t capacity)
+ : _C2LinearCapacityAspect(c2_min(capacity, std::numeric_limits<uint32_t>::max())) {}
+};
+
+/**
+ * Utility class for safe range calculations.
+ */
+class C2LinearRange : public _C2LinearRangeAspect {
+public:
+ inline C2LinearRange(const _C2LinearCapacityBase &parent, size_t offset, size_t size)
+ : _C2LinearRangeAspect(&parent, offset, size) {}
+};
+
+/**
+ * Utility class for simple capacity and range construction.
+ */
+class C2LinearCapacity : public _C2LinearCapacityBase {
+public:
+ using _C2LinearCapacityBase::_C2LinearCapacityBase;
+ inline C2LinearRange range(size_t offset, size_t size) {
+ return C2LinearRange(*this, offset, size);
+ }
+};
+
/**
* Aspect for objects that have an editable linear range.
*
diff --git a/media/libstagefright/codec2/include/C2Component.h b/media/libstagefright/codec2/include/C2Component.h
index 38d545e..a2168a0 100644
--- a/media/libstagefright/codec2/include/C2Component.h
+++ b/media/libstagefright/codec2/include/C2Component.h
@@ -150,7 +150,7 @@
* (this error code is only allowed for interfaces connected to components)
*/
virtual c2_status_t query_vb(
- const std::vector<C2Param* const> &stackParams,
+ const std::vector<C2Param*> &stackParams,
const std::vector<C2Param::Index> &heapParamIndices,
c2_blocking_t mayBlock,
std::vector<std::unique_ptr<C2Param>>* const heapParams) const = 0;
@@ -211,7 +211,7 @@
* (this error code is only allowed for interfaces connected to components)
*/
virtual c2_status_t config_vb(
- const std::vector<C2Param* const> ¶ms,
+ const std::vector<C2Param*> ¶ms,
c2_blocking_t mayBlock,
std::vector<std::unique_ptr<C2SettingResult>>* const failures) = 0;
@@ -850,7 +850,7 @@
* (unexpected)
*/
virtual c2_status_t query_sm(
- const std::vector<C2Param* const> &stackParams,
+ const std::vector<C2Param*> &stackParams,
const std::vector<C2Param::Index> &heapParamIndices,
std::vector<std::unique_ptr<C2Param>>* const heapParams) const = 0;
@@ -889,7 +889,7 @@
* (unexpected)
*/
virtual c2_status_t config_sm(
- const std::vector<C2Param* const> ¶ms,
+ const std::vector<C2Param*> ¶ms,
std::vector<std::unique_ptr<C2SettingResult>>* const failures) = 0;
// REFLECTION MECHANISM (USED FOR EXTENSION)
diff --git a/media/libstagefright/codec2/include/C2Param.h b/media/libstagefright/codec2/include/C2Param.h
index 2a8c1b2..9785c87 100644
--- a/media/libstagefright/codec2/include/C2Param.h
+++ b/media/libstagefright/codec2/include/C2Param.h
@@ -397,8 +397,8 @@
/// safe(r) type cast from pointer and size
inline static C2Param* From(void *addr, size_t len) {
- // _mSize must fit into size
- if (len < sizeof(_mSize) + offsetof(C2Param, _mSize)) {
+ // _mSize must fit into size, but really C2Param must also to be a valid param
+ if (len < sizeof(C2Param)) {
return nullptr;
}
// _mSize must match length
@@ -446,7 +446,7 @@
// if other is the same kind of (valid) param as this, copy it into this and return true.
// otherwise, do not copy anything, and return false.
inline bool updateFrom(const C2Param &other) {
- if (other._mSize == _mSize && other._mIndex == _mIndex && _mSize > 0) {
+ if (other._mSize <= _mSize && other._mIndex == _mIndex && _mSize > 0) {
memcpy(this, &other, _mSize);
return true;
}
@@ -620,6 +620,8 @@
#endif
private:
+ friend struct _C2ParamInspector;
+
uint32_t _mOffset; // offset of field
uint32_t _mSize; // size of field
};
@@ -720,6 +722,8 @@
DEFINE_OTHER_COMPARISON_OPERATORS(C2ParamField)
private:
+ friend struct _C2ParamInspector;
+
C2Param::Index _mIndex; ///< parameter index
_C2FieldId _mFieldId; ///< field identifier
};
diff --git a/media/libstagefright/codec2/include/SimpleC2Interface.h b/media/libstagefright/codec2/include/SimpleC2Interface.h
index 3796b0b..b934f12 100644
--- a/media/libstagefright/codec2/include/SimpleC2Interface.h
+++ b/media/libstagefright/codec2/include/SimpleC2Interface.h
@@ -59,12 +59,12 @@
inline C2String getName() const override { return mName; }
inline c2_node_id_t getId() const override { return mId; }
c2_status_t query_vb(
- const std::vector<C2Param* const> &stackParams,
+ const std::vector<C2Param*> &stackParams,
const std::vector<C2Param::Index> &heapParamIndices,
c2_blocking_t mayBlock,
std::vector<std::unique_ptr<C2Param>>* const heapParams) const override;
inline c2_status_t config_vb(
- const std::vector<C2Param* const> &,
+ const std::vector<C2Param*> &,
c2_blocking_t,
std::vector<std::unique_ptr<C2SettingResult>>* const) override {
return C2_OMITTED;
diff --git a/media/libstagefright/codec2/tests/C2ComponentInterface_test.cpp b/media/libstagefright/codec2/tests/C2ComponentInterface_test.cpp
index 339f927..f50af81 100644
--- a/media/libstagefright/codec2/tests/C2ComponentInterface_test.cpp
+++ b/media/libstagefright/codec2/tests/C2ComponentInterface_test.cpp
@@ -137,7 +137,7 @@
template <typename T> void queryUnsupportedParam();
// Execute an interface's config_vb(). |T| is a single parameter type, not std::vector.
- // config() creates std::vector<C2Param *const> {p} and passes it to config_vb().
+ // config() creates std::vector<C2Param *> {p} and passes it to config_vb().
template <typename T>
c2_status_t
config(T *const p,
@@ -195,7 +195,7 @@
} while (false)
template <typename T> c2_status_t C2CompIntfTest::queryOnStack(T *const p) {
- std::vector<C2Param *const> stackParams{p};
+ std::vector<C2Param*> stackParams{p};
return mIntf->query_vb(stackParams, {}, C2_DONT_BLOCK, nullptr);
}
@@ -260,7 +260,7 @@
template <typename T>
c2_status_t C2CompIntfTest::config(
T *const p, std::vector<std::unique_ptr<C2SettingResult>> *const failures) {
- std::vector<C2Param *const> params{p};
+ std::vector<C2Param*> params{p};
return mIntf->config_vb(params, C2_DONT_BLOCK, failures);
}
@@ -276,7 +276,7 @@
void C2CompIntfTest::configReadOnlyParam(const T &newParam) {
std::unique_ptr<T> p = makeParamFrom(newParam);
- std::vector<C2Param *const> params{p.get()};
+ std::vector<C2Param*> params{p.get()};
std::vector<std::unique_ptr<C2SettingResult>> failures;
// config_vb should be failed because a parameter is read-only.
@@ -289,7 +289,7 @@
void C2CompIntfTest::configWritableParamValidValue(const T &newParam, c2_status_t *configResult) {
std::unique_ptr<T> p = makeParamFrom(newParam);
- std::vector<C2Param *const> params{p.get()};
+ std::vector<C2Param*> params{p.get()};
std::vector<std::unique_ptr<C2SettingResult>> failures;
// In most cases, config_vb return C2_OK and the parameter's value should be changed
// to |newParam|, which is confirmed in a caller of configWritableParamValueValue().
@@ -312,7 +312,7 @@
void C2CompIntfTest::configWritableParamInvalidValue(const T &newParam) {
std::unique_ptr<T> p = makeParamFrom(newParam);
- std::vector<C2Param *const> params{p.get()};
+ std::vector<C2Param*> params{p.get()};
std::vector<std::unique_ptr<C2SettingResult>> failures;
// Although a parameter is writable, config_vb should be failed,
// because a new value is invalid.
diff --git a/media/libstagefright/codec2/tests/C2Param_test.cpp b/media/libstagefright/codec2/tests/C2Param_test.cpp
index 8ebc584..d186292 100644
--- a/media/libstagefright/codec2/tests/C2Param_test.cpp
+++ b/media/libstagefright/codec2/tests/C2Param_test.cpp
@@ -2450,7 +2450,7 @@
}
virtual c2_status_t config_vb(
- const std::vector<C2Param* const> ¶ms,
+ const std::vector<C2Param*> ¶ms,
c2_blocking_t mayBlock,
std::vector<std::unique_ptr<C2SettingResult>>* const failures) override {
(void)params;
@@ -2465,7 +2465,7 @@
}
virtual c2_status_t query_vb(
- const std::vector<C2Param* const> &stackParams,
+ const std::vector<C2Param*> &stackParams,
const std::vector<C2Param::Index> &heapParamIndices,
c2_blocking_t mayBlock,
std::vector<std::unique_ptr<C2Param>>* const heapParams) const override {
diff --git a/media/libstagefright/codec2/vndk/C2ParamInternal.h b/media/libstagefright/codec2/vndk/C2ParamInternal.h
new file mode 100644
index 0000000..0f3812a
--- /dev/null
+++ b/media/libstagefright/codec2/vndk/C2ParamInternal.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+#ifndef ANDROID_STAGEFRIGHT_C2PARAM_INTERNAL_H_
+#define ANDROID_STAGEFRIGHT_C2PARAM_INTERNAL_H_
+
+#include <C2Param.h>
+
+namespace android {
+
+struct C2_HIDE _C2ParamInspector {
+ inline static uint32_t getIndex(const C2ParamField &pf) {
+ return pf._mIndex;
+ }
+
+ inline static uint32_t getOffset(const C2ParamField &pf) {
+ return pf._mFieldId._mOffset;
+ }
+
+ inline static uint32_t getSize(const C2ParamField &pf) {
+ return pf._mFieldId._mSize;
+ }
+
+ inline static
+ C2ParamField CreateParamField(C2Param::Index index, uint32_t offset, uint32_t size) {
+ return C2ParamField(index, offset, size);
+ }
+};
+
+}
+
+#endif // ANDROID_STAGEFRIGHT_C2PARAM_INTERNAL_H_
+
diff --git a/media/libstagefright/codec2/vndk/C2Store.cpp b/media/libstagefright/codec2/vndk/C2Store.cpp
index eb72d17..daa9d3f 100644
--- a/media/libstagefright/codec2/vndk/C2Store.cpp
+++ b/media/libstagefright/codec2/vndk/C2Store.cpp
@@ -167,7 +167,7 @@
virtual c2_status_t querySupportedParams_nb(
std::vector<std::shared_ptr<C2ParamDescriptor>> *const params) const override;
virtual c2_status_t query_sm(
- const std::vector<C2Param *const> &stackParams,
+ const std::vector<C2Param*> &stackParams,
const std::vector<C2Param::Index> &heapParamIndices,
std::vector<std::unique_ptr<C2Param>> *const heapParams) const override;
virtual c2_status_t createInterface(
@@ -177,7 +177,7 @@
virtual c2_status_t copyBuffer(
std::shared_ptr<C2GraphicBuffer> src, std::shared_ptr<C2GraphicBuffer> dst) override;
virtual c2_status_t config_sm(
- const std::vector<C2Param *const> ¶ms,
+ const std::vector<C2Param*> ¶ms,
std::vector<std::unique_ptr<C2SettingResult>> *const failures) override;
C2PlatformComponentStore();
@@ -415,7 +415,7 @@
}
c2_status_t C2PlatformComponentStore::query_sm(
- const std::vector<C2Param *const> &stackParams,
+ const std::vector<C2Param*> &stackParams,
const std::vector<C2Param::Index> &heapParamIndices,
std::vector<std::unique_ptr<C2Param>> *const heapParams) const {
// there are no supported configs
@@ -424,7 +424,7 @@
}
c2_status_t C2PlatformComponentStore::config_sm(
- const std::vector<C2Param *const> ¶ms,
+ const std::vector<C2Param*> ¶ms,
std::vector<std::unique_ptr<C2SettingResult>> *const failures) {
// there are no supported configs
(void)failures;
diff --git a/media/libstagefright/codec2/vndk/include/util/C2ParamUtils.h b/media/libstagefright/codec2/vndk/include/util/C2ParamUtils.h
index 81c5495..3168248 100644
--- a/media/libstagefright/codec2/vndk/include/util/C2ParamUtils.h
+++ b/media/libstagefright/codec2/vndk/include/util/C2ParamUtils.h
@@ -59,6 +59,7 @@
/// \endcond
+#undef DEFINE_C2_ENUM_VALUE_AUTO_HELPER
#define DEFINE_C2_ENUM_VALUE_AUTO_HELPER(name, type, prefix, ...) \
template<> C2FieldDescriptor::named_values_type C2FieldDescriptor::namedValuesFor(const name &r __unused) { \
return C2ParamUtils::sanitizeEnumValues( \
@@ -67,6 +68,7 @@
prefix); \
}
+#undef DEFINE_C2_ENUM_VALUE_CUSTOM_HELPER
#define DEFINE_C2_ENUM_VALUE_CUSTOM_HELPER(name, type, names, ...) \
template<> C2FieldDescriptor::named_values_type C2FieldDescriptor::namedValuesFor(const name &r __unused) { \
return C2ParamUtils::customEnumValues( \
@@ -260,6 +262,21 @@
}
return namedValues;
}
+
+ /// safe(r) parsing from parameter blob
+ static
+ C2Param *ParseFirst(const uint8_t *blob, size_t size) {
+ // _mSize must fit into size, but really C2Param must also to be a valid param
+ if (size < sizeof(C2Param)) {
+ return nullptr;
+ }
+ // _mSize must match length
+ C2Param *param = (C2Param*)blob;
+ if (param->size() > size) {
+ return nullptr;
+ }
+ return param;
+ }
};
/* ---------------------------- UTILITIES FOR PARAMETER REFLECTION ---------------------------- */
diff --git a/media/libstagefright/codecs/aacdec/Android.bp b/media/libstagefright/codecs/aacdec/Android.bp
index abf3b1c..f1ff11b 100644
--- a/media/libstagefright/codecs/aacdec/Android.bp
+++ b/media/libstagefright/codecs/aacdec/Android.bp
@@ -58,6 +58,8 @@
cflags: ["-Werror"],
+ version_script: "exports.lds",
+
sanitize: {
misc_undefined: [
"signed-integer-overflow",
diff --git a/media/libstagefright/codecs/aacdec/exports.lds b/media/libstagefright/codecs/aacdec/exports.lds
new file mode 100644
index 0000000..e24f3fa
--- /dev/null
+++ b/media/libstagefright/codecs/aacdec/exports.lds
@@ -0,0 +1,5 @@
+{
+ global:
+ _Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPEPvPP17OMX_COMPONENTTYPE;
+ local: *;
+};
diff --git a/media/libstagefright/codecs/aacenc/Android.bp b/media/libstagefright/codecs/aacenc/Android.bp
index d734b9c..9342351 100644
--- a/media/libstagefright/codecs/aacenc/Android.bp
+++ b/media/libstagefright/codecs/aacenc/Android.bp
@@ -14,6 +14,8 @@
cflags: ["-Werror"],
+ version_script: "exports.lds",
+
sanitize: {
misc_undefined: [
"signed-integer-overflow",
diff --git a/media/libstagefright/codecs/aacenc/exports.lds b/media/libstagefright/codecs/aacenc/exports.lds
new file mode 100644
index 0000000..e24f3fa
--- /dev/null
+++ b/media/libstagefright/codecs/aacenc/exports.lds
@@ -0,0 +1,5 @@
+{
+ global:
+ _Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPEPvPP17OMX_COMPONENTTYPE;
+ local: *;
+};
diff --git a/media/libstagefright/codecs/amrnb/dec/Android.bp b/media/libstagefright/codecs/amrnb/dec/Android.bp
index b493e21..e4a2607 100644
--- a/media/libstagefright/codecs/amrnb/dec/Android.bp
+++ b/media/libstagefright/codecs/amrnb/dec/Android.bp
@@ -50,6 +50,8 @@
"-Werror",
],
+ version_script: "exports.lds",
+
//sanitize: {
// misc_undefined: [
// "signed-integer-overflow",
@@ -85,6 +87,8 @@
"-Werror",
],
+ version_script: "exports.lds",
+
//sanitize: {
// misc_undefined: [
// "signed-integer-overflow",
diff --git a/media/libstagefright/codecs/amrnb/dec/exports.lds b/media/libstagefright/codecs/amrnb/dec/exports.lds
new file mode 100644
index 0000000..e24f3fa
--- /dev/null
+++ b/media/libstagefright/codecs/amrnb/dec/exports.lds
@@ -0,0 +1,5 @@
+{
+ global:
+ _Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPEPvPP17OMX_COMPONENTTYPE;
+ local: *;
+};
diff --git a/media/libstagefright/codecs/amrnb/enc/Android.bp b/media/libstagefright/codecs/amrnb/enc/Android.bp
index 1e8fd31..f844459 100644
--- a/media/libstagefright/codecs/amrnb/enc/Android.bp
+++ b/media/libstagefright/codecs/amrnb/enc/Android.bp
@@ -70,6 +70,8 @@
"-Werror",
],
+ version_script: "exports.lds",
+
//addressing b/25409744
//sanitize: {
// misc_undefined: [
diff --git a/media/libstagefright/codecs/amrnb/enc/exports.lds b/media/libstagefright/codecs/amrnb/enc/exports.lds
new file mode 100644
index 0000000..e24f3fa
--- /dev/null
+++ b/media/libstagefright/codecs/amrnb/enc/exports.lds
@@ -0,0 +1,5 @@
+{
+ global:
+ _Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPEPvPP17OMX_COMPONENTTYPE;
+ local: *;
+};
diff --git a/media/libstagefright/codecs/amrwbenc/Android.bp b/media/libstagefright/codecs/amrwbenc/Android.bp
index b6f637f..ebe08c6 100644
--- a/media/libstagefright/codecs/amrwbenc/Android.bp
+++ b/media/libstagefright/codecs/amrwbenc/Android.bp
@@ -159,6 +159,8 @@
cflags: ["-Werror"],
+ version_script: "exports.lds",
+
sanitize: {
misc_undefined: [
"signed-integer-overflow",
diff --git a/media/libstagefright/codecs/amrwbenc/exports.lds b/media/libstagefright/codecs/amrwbenc/exports.lds
new file mode 100644
index 0000000..e24f3fa
--- /dev/null
+++ b/media/libstagefright/codecs/amrwbenc/exports.lds
@@ -0,0 +1,5 @@
+{
+ global:
+ _Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPEPvPP17OMX_COMPONENTTYPE;
+ local: *;
+};
diff --git a/media/libstagefright/codecs/avcdec/Android.bp b/media/libstagefright/codecs/avcdec/Android.bp
index 04e5dc1..d789096 100644
--- a/media/libstagefright/codecs/avcdec/Android.bp
+++ b/media/libstagefright/codecs/avcdec/Android.bp
@@ -13,6 +13,8 @@
"-Werror",
],
+ version_script: "exports.lds",
+
include_dirs: [
"external/libavc/decoder",
"external/libavc/common",
diff --git a/media/libstagefright/codecs/avcdec/C2SoftAvcDec.cpp b/media/libstagefright/codecs/avcdec/C2SoftAvcDec.cpp
index ffe6332..0da9cc7 100644
--- a/media/libstagefright/codecs/avcdec/C2SoftAvcDec.cpp
+++ b/media/libstagefright/codecs/avcdec/C2SoftAvcDec.cpp
@@ -448,7 +448,7 @@
}
c2_status_t C2SoftAvcDecIntf::query_vb(
- const std::vector<C2Param* const> & stackParams,
+ const std::vector<C2Param*> & stackParams,
const std::vector<C2Param::Index> & heapParamIndices,
c2_blocking_t mayBlock,
std::vector<std::unique_ptr<C2Param>>* const heapParams) const {
@@ -485,7 +485,7 @@
}
c2_status_t C2SoftAvcDecIntf::config_vb(
- const std::vector<C2Param* const> ¶ms,
+ const std::vector<C2Param*> ¶ms,
c2_blocking_t mayBlock,
std::vector<std::unique_ptr<C2SettingResult>>* const failures) {
(void)mayBlock;
diff --git a/media/libstagefright/codecs/avcdec/C2SoftAvcDec.h b/media/libstagefright/codecs/avcdec/C2SoftAvcDec.h
index 0e8cf77..6632bf3 100644
--- a/media/libstagefright/codecs/avcdec/C2SoftAvcDec.h
+++ b/media/libstagefright/codecs/avcdec/C2SoftAvcDec.h
@@ -84,12 +84,12 @@
virtual C2String getName() const override;
virtual c2_node_id_t getId() const override;
virtual c2_status_t query_vb(
- const std::vector<C2Param* const> &stackParams,
+ const std::vector<C2Param*> &stackParams,
const std::vector<C2Param::Index> &heapParamIndices,
c2_blocking_t mayBlock,
std::vector<std::unique_ptr<C2Param>>* const heapParams) const override;
virtual c2_status_t config_vb(
- const std::vector<C2Param* const> ¶ms,
+ const std::vector<C2Param*> ¶ms,
c2_blocking_t mayBlock,
std::vector<std::unique_ptr<C2SettingResult>>* const failures) override;
virtual c2_status_t createTunnel_sm(c2_node_id_t targetComponent) override;
diff --git a/media/libstagefright/codecs/avcdec/exports.lds b/media/libstagefright/codecs/avcdec/exports.lds
new file mode 100644
index 0000000..e24f3fa
--- /dev/null
+++ b/media/libstagefright/codecs/avcdec/exports.lds
@@ -0,0 +1,5 @@
+{
+ global:
+ _Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPEPvPP17OMX_COMPONENTTYPE;
+ local: *;
+};
diff --git a/media/libstagefright/codecs/avcenc/Android.bp b/media/libstagefright/codecs/avcenc/Android.bp
index 4a0411e..cefe77c 100644
--- a/media/libstagefright/codecs/avcenc/Android.bp
+++ b/media/libstagefright/codecs/avcenc/Android.bp
@@ -39,5 +39,8 @@
"-Wno-unused-variable",
],
ldflags: ["-Wl,-Bsymbolic"],
+
+ version_script: "exports.lds",
+
compile_multilib: "32",
}
diff --git a/media/libstagefright/codecs/avcenc/exports.lds b/media/libstagefright/codecs/avcenc/exports.lds
new file mode 100644
index 0000000..e24f3fa
--- /dev/null
+++ b/media/libstagefright/codecs/avcenc/exports.lds
@@ -0,0 +1,5 @@
+{
+ global:
+ _Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPEPvPP17OMX_COMPONENTTYPE;
+ local: *;
+};
diff --git a/media/libstagefright/codecs/flac/dec/Android.bp b/media/libstagefright/codecs/flac/dec/Android.bp
index 595cfdb..9af086b 100644
--- a/media/libstagefright/codecs/flac/dec/Android.bp
+++ b/media/libstagefright/codecs/flac/dec/Android.bp
@@ -18,6 +18,8 @@
cflags: ["-Werror"],
+ version_script: "exports.lds",
+
sanitize: {
misc_undefined: [
"signed-integer-overflow",
diff --git a/media/libstagefright/codecs/flac/dec/exports.lds b/media/libstagefright/codecs/flac/dec/exports.lds
new file mode 100644
index 0000000..e24f3fa
--- /dev/null
+++ b/media/libstagefright/codecs/flac/dec/exports.lds
@@ -0,0 +1,5 @@
+{
+ global:
+ _Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPEPvPP17OMX_COMPONENTTYPE;
+ local: *;
+};
diff --git a/media/libstagefright/codecs/flac/enc/Android.bp b/media/libstagefright/codecs/flac/enc/Android.bp
index 854f7ce..46b974d 100644
--- a/media/libstagefright/codecs/flac/enc/Android.bp
+++ b/media/libstagefright/codecs/flac/enc/Android.bp
@@ -10,6 +10,8 @@
cflags: ["-Werror"],
+ version_script: "exports.lds",
+
sanitize: {
misc_undefined: [
"signed-integer-overflow",
diff --git a/media/libstagefright/codecs/flac/enc/exports.lds b/media/libstagefright/codecs/flac/enc/exports.lds
new file mode 100644
index 0000000..e24f3fa
--- /dev/null
+++ b/media/libstagefright/codecs/flac/enc/exports.lds
@@ -0,0 +1,5 @@
+{
+ global:
+ _Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPEPvPP17OMX_COMPONENTTYPE;
+ local: *;
+};
diff --git a/media/libstagefright/codecs/g711/dec/Android.bp b/media/libstagefright/codecs/g711/dec/Android.bp
index 07e5052..3d97d8c 100644
--- a/media/libstagefright/codecs/g711/dec/Android.bp
+++ b/media/libstagefright/codecs/g711/dec/Android.bp
@@ -21,6 +21,8 @@
cflags: ["-Werror"],
+ version_script: "exports.lds",
+
sanitize: {
misc_undefined: [
"signed-integer-overflow",
diff --git a/media/libstagefright/codecs/g711/dec/exports.lds b/media/libstagefright/codecs/g711/dec/exports.lds
new file mode 100644
index 0000000..e24f3fa
--- /dev/null
+++ b/media/libstagefright/codecs/g711/dec/exports.lds
@@ -0,0 +1,5 @@
+{
+ global:
+ _Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPEPvPP17OMX_COMPONENTTYPE;
+ local: *;
+};
diff --git a/media/libstagefright/codecs/gsm/dec/Android.bp b/media/libstagefright/codecs/gsm/dec/Android.bp
index 0739ad4..1c3208b 100644
--- a/media/libstagefright/codecs/gsm/dec/Android.bp
+++ b/media/libstagefright/codecs/gsm/dec/Android.bp
@@ -15,6 +15,8 @@
cflags: ["-Werror"],
+ version_script: "exports.lds",
+
sanitize: {
misc_undefined: [
"signed-integer-overflow",
diff --git a/media/libstagefright/codecs/gsm/dec/exports.lds b/media/libstagefright/codecs/gsm/dec/exports.lds
new file mode 100644
index 0000000..e24f3fa
--- /dev/null
+++ b/media/libstagefright/codecs/gsm/dec/exports.lds
@@ -0,0 +1,5 @@
+{
+ global:
+ _Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPEPvPP17OMX_COMPONENTTYPE;
+ local: *;
+};
diff --git a/media/libstagefright/codecs/hevcdec/Android.bp b/media/libstagefright/codecs/hevcdec/Android.bp
index f19ba00..45920e6 100644
--- a/media/libstagefright/codecs/hevcdec/Android.bp
+++ b/media/libstagefright/codecs/hevcdec/Android.bp
@@ -14,6 +14,8 @@
"-Wno-unused-variable",
],
+ version_script: "exports.lds",
+
include_dirs: [
"external/libhevc/decoder",
"external/libhevc/common",
diff --git a/media/libstagefright/codecs/hevcdec/exports.lds b/media/libstagefright/codecs/hevcdec/exports.lds
new file mode 100644
index 0000000..e24f3fa
--- /dev/null
+++ b/media/libstagefright/codecs/hevcdec/exports.lds
@@ -0,0 +1,5 @@
+{
+ global:
+ _Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPEPvPP17OMX_COMPONENTTYPE;
+ local: *;
+};
diff --git a/media/libstagefright/codecs/m4v_h263/dec/Android.bp b/media/libstagefright/codecs/m4v_h263/dec/Android.bp
index e57bb78..ca70cc2 100644
--- a/media/libstagefright/codecs/m4v_h263/dec/Android.bp
+++ b/media/libstagefright/codecs/m4v_h263/dec/Android.bp
@@ -53,6 +53,8 @@
"-Werror",
],
+ version_script: "exports.lds",
+
sanitize: {
misc_undefined: [
"signed-integer-overflow",
diff --git a/media/libstagefright/codecs/m4v_h263/dec/SoftMPEG4.cpp b/media/libstagefright/codecs/m4v_h263/dec/SoftMPEG4.cpp
index 39b67ab..fda7028 100644
--- a/media/libstagefright/codecs/m4v_h263/dec/SoftMPEG4.cpp
+++ b/media/libstagefright/codecs/m4v_h263/dec/SoftMPEG4.cpp
@@ -353,7 +353,8 @@
bool portWillReset = false;
const bool fakeStride = true;
SoftVideoDecoderOMXComponent::handlePortSettingsChange(
- &portWillReset, buf_width, buf_height, cropSettingsMode, fakeStride);
+ &portWillReset, buf_width, buf_height,
+ OMX_COLOR_FormatYUV420Planar, cropSettingsMode, fakeStride);
if (portWillReset) {
if (mMode == MODE_H263) {
PVCleanUpVideoDecoder(mHandle);
diff --git a/media/libstagefright/codecs/m4v_h263/dec/exports.lds b/media/libstagefright/codecs/m4v_h263/dec/exports.lds
new file mode 100644
index 0000000..e24f3fa
--- /dev/null
+++ b/media/libstagefright/codecs/m4v_h263/dec/exports.lds
@@ -0,0 +1,5 @@
+{
+ global:
+ _Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPEPvPP17OMX_COMPONENTTYPE;
+ local: *;
+};
diff --git a/media/libstagefright/codecs/m4v_h263/enc/Android.bp b/media/libstagefright/codecs/m4v_h263/enc/Android.bp
index 8a3fe34..6be4036 100644
--- a/media/libstagefright/codecs/m4v_h263/enc/Android.bp
+++ b/media/libstagefright/codecs/m4v_h263/enc/Android.bp
@@ -30,6 +30,8 @@
"-Werror",
],
+ version_script: "exports.lds",
+
include_dirs: [
"frameworks/av/media/libstagefright/include",
"frameworks/native/include/media/openmax",
diff --git a/media/libstagefright/codecs/m4v_h263/enc/exports.lds b/media/libstagefright/codecs/m4v_h263/enc/exports.lds
new file mode 100644
index 0000000..e24f3fa
--- /dev/null
+++ b/media/libstagefright/codecs/m4v_h263/enc/exports.lds
@@ -0,0 +1,5 @@
+{
+ global:
+ _Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPEPvPP17OMX_COMPONENTTYPE;
+ local: *;
+};
diff --git a/media/libstagefright/codecs/mp3dec/Android.bp b/media/libstagefright/codecs/mp3dec/Android.bp
index 5a0e282..e6eb32b 100644
--- a/media/libstagefright/codecs/mp3dec/Android.bp
+++ b/media/libstagefright/codecs/mp3dec/Android.bp
@@ -96,6 +96,8 @@
cflags: ["-Werror"],
+ version_script: "exports.lds",
+
sanitize: {
misc_undefined: [
"signed-integer-overflow",
diff --git a/media/libstagefright/codecs/mp3dec/exports.lds b/media/libstagefright/codecs/mp3dec/exports.lds
new file mode 100644
index 0000000..e24f3fa
--- /dev/null
+++ b/media/libstagefright/codecs/mp3dec/exports.lds
@@ -0,0 +1,5 @@
+{
+ global:
+ _Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPEPvPP17OMX_COMPONENTTYPE;
+ local: *;
+};
diff --git a/media/libstagefright/codecs/mpeg2dec/Android.bp b/media/libstagefright/codecs/mpeg2dec/Android.bp
index 9b8a188..fb0db8f 100644
--- a/media/libstagefright/codecs/mpeg2dec/Android.bp
+++ b/media/libstagefright/codecs/mpeg2dec/Android.bp
@@ -14,6 +14,8 @@
"-Wno-unused-variable",
],
+ version_script: "exports.lds",
+
include_dirs: [
"external/libmpeg2/decoder",
"external/libmpeg2/common",
diff --git a/media/libstagefright/codecs/mpeg2dec/exports.lds b/media/libstagefright/codecs/mpeg2dec/exports.lds
new file mode 100644
index 0000000..e24f3fa
--- /dev/null
+++ b/media/libstagefright/codecs/mpeg2dec/exports.lds
@@ -0,0 +1,5 @@
+{
+ global:
+ _Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPEPvPP17OMX_COMPONENTTYPE;
+ local: *;
+};
diff --git a/media/libstagefright/codecs/on2/dec/Android.bp b/media/libstagefright/codecs/on2/dec/Android.bp
index a4eed8c..8a9399a 100644
--- a/media/libstagefright/codecs/on2/dec/Android.bp
+++ b/media/libstagefright/codecs/on2/dec/Android.bp
@@ -23,6 +23,8 @@
cflags: ["-Werror"],
+ version_script: "exports.lds",
+
sanitize: {
misc_undefined: [
"signed-integer-overflow",
diff --git a/media/libstagefright/codecs/on2/dec/SoftVPX.cpp b/media/libstagefright/codecs/on2/dec/SoftVPX.cpp
index 3490008..8d5f3e7 100644
--- a/media/libstagefright/codecs/on2/dec/SoftVPX.cpp
+++ b/media/libstagefright/codecs/on2/dec/SoftVPX.cpp
@@ -30,7 +30,9 @@
// Only need to declare the highest supported profile and level here.
static const CodecProfileLevel kVP9ProfileLevels[] = {
- { OMX_VIDEO_VP9Profile0, OMX_VIDEO_VP9Level5 },
+ { OMX_VIDEO_VP9Profile0, OMX_VIDEO_VP9Level5 },
+ { OMX_VIDEO_VP9Profile2, OMX_VIDEO_VP9Level5 },
+ { OMX_VIDEO_VP9Profile2HDR, OMX_VIDEO_VP9Level5 },
};
SoftVPX::SoftVPX(
@@ -78,6 +80,10 @@
return cpuCoreCount;
}
+bool SoftVPX::supportDescribeHdrStaticInfo() {
+ return true;
+}
+
status_t SoftVPX::initDecoder() {
mCtx = new vpx_codec_ctx_t;
vpx_codec_err_t vpx_err;
@@ -146,15 +152,21 @@
uint32_t height = mImg->d_h;
outInfo = *outQueue.begin();
outHeader = outInfo->mHeader;
- CHECK_EQ(mImg->fmt, VPX_IMG_FMT_I420);
- handlePortSettingsChange(portWillReset, width, height);
+ CHECK(mImg->fmt == VPX_IMG_FMT_I420 || mImg->fmt == VPX_IMG_FMT_I42016);
+ OMX_COLOR_FORMATTYPE outputColorFormat = OMX_COLOR_FormatYUV420Planar;
+ int32_t bpp = 1;
+ if (mImg->fmt == VPX_IMG_FMT_I42016) {
+ outputColorFormat = OMX_COLOR_FormatYUV420Planar16;
+ bpp = 2;
+ }
+ handlePortSettingsChange(portWillReset, width, height, outputColorFormat);
if (*portWillReset) {
return true;
}
outHeader->nOffset = 0;
outHeader->nFlags = 0;
- outHeader->nFilledLen = (outputBufferWidth() * outputBufferHeight() * 3) / 2;
+ outHeader->nFilledLen = (outputBufferWidth() * outputBufferHeight() * bpp * 3) / 2;
outHeader->nTimeStamp = *(OMX_TICKS *)mImg->user_priv;
if (outputBufferSafe(outHeader)) {
uint8_t *dst = outHeader->pBuffer;
diff --git a/media/libstagefright/codecs/on2/dec/SoftVPX.h b/media/libstagefright/codecs/on2/dec/SoftVPX.h
index d6bb902..a01a4f3 100644
--- a/media/libstagefright/codecs/on2/dec/SoftVPX.h
+++ b/media/libstagefright/codecs/on2/dec/SoftVPX.h
@@ -40,6 +40,7 @@
virtual void onQueueFilled(OMX_U32 portIndex);
virtual void onPortFlushCompleted(OMX_U32 portIndex);
virtual void onReset();
+ virtual bool supportDescribeHdrStaticInfo();
private:
enum {
diff --git a/media/libstagefright/codecs/on2/dec/exports.lds b/media/libstagefright/codecs/on2/dec/exports.lds
new file mode 100644
index 0000000..e24f3fa
--- /dev/null
+++ b/media/libstagefright/codecs/on2/dec/exports.lds
@@ -0,0 +1,5 @@
+{
+ global:
+ _Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPEPvPP17OMX_COMPONENTTYPE;
+ local: *;
+};
diff --git a/media/libstagefright/codecs/on2/enc/Android.bp b/media/libstagefright/codecs/on2/enc/Android.bp
index b21ffa1..3d9feeb 100644
--- a/media/libstagefright/codecs/on2/enc/Android.bp
+++ b/media/libstagefright/codecs/on2/enc/Android.bp
@@ -13,6 +13,8 @@
cflags: ["-Wall", "-Werror"],
+ version_script: "exports.lds",
+
include_dirs: [
"frameworks/av/media/libstagefright/include",
"frameworks/native/include/media/openmax",
diff --git a/media/libstagefright/codecs/on2/enc/exports.lds b/media/libstagefright/codecs/on2/enc/exports.lds
new file mode 100644
index 0000000..e24f3fa
--- /dev/null
+++ b/media/libstagefright/codecs/on2/enc/exports.lds
@@ -0,0 +1,5 @@
+{
+ global:
+ _Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPEPvPP17OMX_COMPONENTTYPE;
+ local: *;
+};
diff --git a/media/libstagefright/codecs/opus/dec/Android.bp b/media/libstagefright/codecs/opus/dec/Android.bp
index 32a4f32..43318f2 100644
--- a/media/libstagefright/codecs/opus/dec/Android.bp
+++ b/media/libstagefright/codecs/opus/dec/Android.bp
@@ -22,6 +22,8 @@
cflags: ["-Werror"],
+ version_script: "exports.lds",
+
sanitize: {
misc_undefined: [
"signed-integer-overflow",
diff --git a/media/libstagefright/codecs/opus/dec/exports.lds b/media/libstagefright/codecs/opus/dec/exports.lds
new file mode 100644
index 0000000..e24f3fa
--- /dev/null
+++ b/media/libstagefright/codecs/opus/dec/exports.lds
@@ -0,0 +1,5 @@
+{
+ global:
+ _Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPEPvPP17OMX_COMPONENTTYPE;
+ local: *;
+};
diff --git a/media/libstagefright/codecs/raw/Android.bp b/media/libstagefright/codecs/raw/Android.bp
index f21d46f..c8d7d00 100644
--- a/media/libstagefright/codecs/raw/Android.bp
+++ b/media/libstagefright/codecs/raw/Android.bp
@@ -14,6 +14,8 @@
cflags: ["-Werror"],
+ version_script: "exports.lds",
+
sanitize: {
misc_undefined: [
"signed-integer-overflow",
diff --git a/media/libstagefright/codecs/raw/exports.lds b/media/libstagefright/codecs/raw/exports.lds
new file mode 100644
index 0000000..e24f3fa
--- /dev/null
+++ b/media/libstagefright/codecs/raw/exports.lds
@@ -0,0 +1,5 @@
+{
+ global:
+ _Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPEPvPP17OMX_COMPONENTTYPE;
+ local: *;
+};
diff --git a/media/libstagefright/codecs/vorbis/dec/Android.bp b/media/libstagefright/codecs/vorbis/dec/Android.bp
index b7a6c1c..a9265cb 100644
--- a/media/libstagefright/codecs/vorbis/dec/Android.bp
+++ b/media/libstagefright/codecs/vorbis/dec/Android.bp
@@ -22,6 +22,8 @@
cflags: ["-Werror"],
+ version_script: "exports.lds",
+
sanitize: {
misc_undefined: [
"signed-integer-overflow",
diff --git a/media/libstagefright/codecs/vorbis/dec/exports.lds b/media/libstagefright/codecs/vorbis/dec/exports.lds
new file mode 100644
index 0000000..e24f3fa
--- /dev/null
+++ b/media/libstagefright/codecs/vorbis/dec/exports.lds
@@ -0,0 +1,5 @@
+{
+ global:
+ _Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPEPvPP17OMX_COMPONENTTYPE;
+ local: *;
+};
diff --git a/media/libstagefright/colorconversion/ColorConverter.cpp b/media/libstagefright/colorconversion/ColorConverter.cpp
index cbb38fd..c7f9001 100644
--- a/media/libstagefright/colorconversion/ColorConverter.cpp
+++ b/media/libstagefright/colorconversion/ColorConverter.cpp
@@ -19,13 +19,28 @@
#include <utils/Log.h>
#include <media/stagefright/foundation/ADebug.h>
+#include <media/stagefright/foundation/ALooper.h>
#include <media/stagefright/ColorConverter.h>
#include <media/stagefright/MediaErrors.h>
#include "libyuv/convert_from.h"
#include "libyuv/video_common.h"
+#include <sys/time.h>
+
#define USE_LIBYUV
+#define PERF_PROFILING 0
+
+
+#if defined(__aarch64__) || defined(__ARM_NEON__)
+#define USE_NEON_Y410 1
+#else
+#define USE_NEON_Y410 0
+#endif
+
+#if USE_NEON_Y410
+#include <arm_neon.h>
+#endif
namespace android {
@@ -48,6 +63,9 @@
|| mDstFormat == OMX_COLOR_Format32BitRGBA8888
|| mDstFormat == OMX_COLOR_Format32bitBGRA8888;
+ case OMX_COLOR_FormatYUV420Planar16:
+ return mDstFormat == OMX_COLOR_Format32BitRGBA1010102;
+
case OMX_COLOR_FormatCbYCrY:
case OMX_QCOM_COLOR_FormatYVU420SemiPlanar:
case OMX_COLOR_FormatYUV420SemiPlanar:
@@ -81,10 +99,16 @@
case OMX_COLOR_Format32bitBGRA8888:
case OMX_COLOR_Format32BitRGBA8888:
+ case OMX_COLOR_Format32BitRGBA1010102:
mBpp = 4;
mStride = 4 * mWidth;
break;
+ case OMX_COLOR_FormatYUV420Planar16:
+ mBpp = 2;
+ mStride = 2 * mWidth;
+ break;
+
case OMX_COLOR_FormatYUV420Planar:
case OMX_COLOR_FormatCbYCrY:
case OMX_QCOM_COLOR_FormatYVU420SemiPlanar:
@@ -146,6 +170,19 @@
#endif
break;
+ case OMX_COLOR_FormatYUV420Planar16:
+ {
+#if PERF_PROFILING
+ int64_t startTimeUs = ALooper::GetNowUs();
+#endif
+ err = convertYUV420Planar16(src, dst);
+#if PERF_PROFILING
+ int64_t endTimeUs = ALooper::GetNowUs();
+ ALOGD("convertYUV420Planar16 took %lld us", (long long) (endTimeUs - startTimeUs));
+#endif
+ break;
+ }
+
case OMX_COLOR_FormatCbYCrY:
err = convertCbYCrY(src, dst);
break;
@@ -406,6 +443,234 @@
return OK;
}
+/*
+ * Pack 10-bit YUV into RGBA_1010102.
+ *
+ * Media sends 10-bit YUV in a RGBA_1010102 format buffer. SF will handle
+ * the conversion to RGB using RenderEngine fallback.
+ *
+ * We do not perform a YUV->RGB conversion here, however the conversion with
+ * BT2020 to Full range is below for reference:
+ *
+ * B = 1.168 *(Y - 64) + 2.148 *(U - 512)
+ * G = 1.168 *(Y - 64) - 0.652 *(V - 512) - 0.188 *(U - 512)
+ * R = 1.168 *(Y - 64) + 1.683 *(V - 512)
+ *
+ * B = 1196/1024 *(Y - 64) + 2200/1024 *(U - 512)
+ * G = .................... - 668/1024 *(V - 512) - 192/1024 *(U - 512)
+ * R = .................... + 1723/1024 *(V - 512)
+ *
+ * min_B = (1196 *(- 64) + 2200 *(- 512)) / 1024 = -1175
+ * min_G = (1196 *(- 64) - 668 *(1023 - 512) - 192 *(1023 - 512)) / 1024 = -504
+ * min_R = (1196 *(- 64) + 1723 *(- 512)) / 1024 = -937
+ *
+ * max_B = (1196 *(1023 - 64) + 2200 *(1023 - 512)) / 1024 = 2218
+ * max_G = (1196 *(1023 - 64) - 668 *(- 512) - 192 *(- 512)) / 1024 = 1551
+ * max_R = (1196 *(1023 - 64) + 1723 *(1023 - 512)) / 1024 = 1980
+ *
+ * clip range -1175 .. 2218
+ *
+ */
+
+#if !USE_NEON_Y410
+
+status_t ColorConverter::convertYUV420Planar16(
+ const BitmapParams &src, const BitmapParams &dst) {
+ uint8_t *dst_ptr = (uint8_t *)dst.mBits
+ + dst.mCropTop * dst.mStride + dst.mCropLeft * dst.mBpp;
+
+ const uint8_t *src_y =
+ (const uint8_t *)src.mBits + src.mCropTop * src.mStride + src.mCropLeft * src.mBpp;
+
+ const uint8_t *src_u =
+ (const uint8_t *)src.mBits + src.mStride * src.mHeight
+ + (src.mCropTop / 2) * (src.mStride / 2) + (src.mCropLeft / 2) * src.mBpp;
+
+ const uint8_t *src_v =
+ src_u + (src.mStride / 2) * (src.mHeight / 2);
+
+ // Converting two lines at a time, slightly faster
+ for (size_t y = 0; y < src.cropHeight(); y += 2) {
+ uint32_t *dst_top = (uint32_t *) dst_ptr;
+ uint32_t *dst_bot = (uint32_t *) (dst_ptr + dst.mStride);
+ uint16_t *ptr_ytop = (uint16_t*) src_y;
+ uint16_t *ptr_ybot = (uint16_t*) (src_y + src.mStride);
+ uint16_t *ptr_u = (uint16_t*) src_u;
+ uint16_t *ptr_v = (uint16_t*) src_v;
+
+ uint32_t u01, v01, y01, y23, y45, y67, uv0, uv1;
+ size_t x = 0;
+ for (; x < src.cropWidth() - 3; x += 4) {
+ u01 = *((uint32_t*)ptr_u); ptr_u += 2;
+ v01 = *((uint32_t*)ptr_v); ptr_v += 2;
+
+ y01 = *((uint32_t*)ptr_ytop); ptr_ytop += 2;
+ y23 = *((uint32_t*)ptr_ytop); ptr_ytop += 2;
+ y45 = *((uint32_t*)ptr_ybot); ptr_ybot += 2;
+ y67 = *((uint32_t*)ptr_ybot); ptr_ybot += 2;
+
+ uv0 = (u01 & 0x3FF) | ((v01 & 0x3FF) << 20);
+ uv1 = (u01 >> 16) | ((v01 >> 16) << 20);
+
+ *dst_top++ = ((y01 & 0x3FF) << 10) | uv0;
+ *dst_top++ = ((y01 >> 16) << 10) | uv0;
+ *dst_top++ = ((y23 & 0x3FF) << 10) | uv1;
+ *dst_top++ = ((y23 >> 16) << 10) | uv1;
+
+ *dst_bot++ = ((y45 & 0x3FF) << 10) | uv0;
+ *dst_bot++ = ((y45 >> 16) << 10) | uv0;
+ *dst_bot++ = ((y67 & 0x3FF) << 10) | uv1;
+ *dst_bot++ = ((y67 >> 16) << 10) | uv1;
+ }
+
+ // There should be at most 2 more pixels to process. Note that we don't
+ // need to consider odd case as the buffer is always aligned to even.
+ if (x < src.cropWidth()) {
+ u01 = *ptr_u;
+ v01 = *ptr_v;
+ y01 = *((uint32_t*)ptr_ytop);
+ y45 = *((uint32_t*)ptr_ybot);
+ uv0 = (u01 & 0x3FF) | ((v01 & 0x3FF) << 20);
+ *dst_top++ = ((y01 & 0x3FF) << 10) | uv0;
+ *dst_top++ = ((y01 >> 16) << 10) | uv0;
+ *dst_bot++ = ((y45 & 0x3FF) << 10) | uv0;
+ *dst_bot++ = ((y45 >> 16) << 10) | uv0;
+ }
+
+ src_y += src.mStride * 2;
+ src_u += src.mStride / 2;
+ src_v += src.mStride / 2;
+ dst_ptr += dst.mStride * 2;
+ }
+
+ return OK;
+}
+
+#else
+
+status_t ColorConverter::convertYUV420Planar16(
+ const BitmapParams &src, const BitmapParams &dst) {
+ uint8_t *out = (uint8_t *)dst.mBits
+ + dst.mCropTop * dst.mStride + dst.mCropLeft * dst.mBpp;
+
+ const uint8_t *src_y =
+ (const uint8_t *)src.mBits + src.mCropTop * src.mStride + src.mCropLeft * src.mBpp;
+
+ const uint8_t *src_u =
+ (const uint8_t *)src.mBits + src.mStride * src.mHeight
+ + (src.mCropTop / 2) * (src.mStride / 2) + (src.mCropLeft / 2) * src.mBpp;
+
+ const uint8_t *src_v =
+ src_u + (src.mStride / 2) * (src.mHeight / 2);
+
+ for (size_t y = 0; y < src.cropHeight(); y++) {
+ uint16_t *ptr_y = (uint16_t*) src_y;
+ uint16_t *ptr_u = (uint16_t*) src_u;
+ uint16_t *ptr_v = (uint16_t*) src_v;
+ uint32_t *ptr_out = (uint32_t *) out;
+
+ // Process 16-pixel at a time.
+ uint32_t *ptr_limit = ptr_out + (src.cropWidth() & ~15);
+ while (ptr_out < ptr_limit) {
+ uint16x4_t u0123 = vld1_u16(ptr_u); ptr_u += 4;
+ uint16x4_t u4567 = vld1_u16(ptr_u); ptr_u += 4;
+ uint16x4_t v0123 = vld1_u16(ptr_v); ptr_v += 4;
+ uint16x4_t v4567 = vld1_u16(ptr_v); ptr_v += 4;
+ uint16x4_t y0123 = vld1_u16(ptr_y); ptr_y += 4;
+ uint16x4_t y4567 = vld1_u16(ptr_y); ptr_y += 4;
+ uint16x4_t y89ab = vld1_u16(ptr_y); ptr_y += 4;
+ uint16x4_t ycdef = vld1_u16(ptr_y); ptr_y += 4;
+
+ uint32x2_t uvtempl;
+ uint32x4_t uvtempq;
+
+ uvtempq = vaddw_u16(vshll_n_u16(v0123, 20), u0123);
+
+ uvtempl = vget_low_u32(uvtempq);
+ uint32x4_t uv0011 = vreinterpretq_u32_u64(
+ vaddw_u32(vshll_n_u32(uvtempl, 32), uvtempl));
+
+ uvtempl = vget_high_u32(uvtempq);
+ uint32x4_t uv2233 = vreinterpretq_u32_u64(
+ vaddw_u32(vshll_n_u32(uvtempl, 32), uvtempl));
+
+ uvtempq = vaddw_u16(vshll_n_u16(v4567, 20), u4567);
+
+ uvtempl = vget_low_u32(uvtempq);
+ uint32x4_t uv4455 = vreinterpretq_u32_u64(
+ vaddw_u32(vshll_n_u32(uvtempl, 32), uvtempl));
+
+ uvtempl = vget_high_u32(uvtempq);
+ uint32x4_t uv6677 = vreinterpretq_u32_u64(
+ vaddw_u32(vshll_n_u32(uvtempl, 32), uvtempl));
+
+ uint32x4_t dsttemp;
+
+ dsttemp = vorrq_u32(uv0011, vshll_n_u16(y0123, 10));
+ vst1q_u32(ptr_out, dsttemp); ptr_out += 4;
+
+ dsttemp = vorrq_u32(uv2233, vshll_n_u16(y4567, 10));
+ vst1q_u32(ptr_out, dsttemp); ptr_out += 4;
+
+ dsttemp = vorrq_u32(uv4455, vshll_n_u16(y89ab, 10));
+ vst1q_u32(ptr_out, dsttemp); ptr_out += 4;
+
+ dsttemp = vorrq_u32(uv6677, vshll_n_u16(ycdef, 10));
+ vst1q_u32(ptr_out, dsttemp); ptr_out += 4;
+ }
+
+ src_y += src.mStride;
+ if (y & 1) {
+ src_u += src.mStride / 2;
+ src_v += src.mStride / 2;
+ }
+ out += dst.mStride;
+ }
+
+ // Process the left-overs out-of-loop, 2-pixel at a time. Note that we don't
+ // need to consider odd case as the buffer is always aligned to even.
+ if (src.cropWidth() & 15) {
+ size_t xstart = (src.cropWidth() & ~15);
+
+ uint8_t *out = (uint8_t *)dst.mBits + dst.mCropTop * dst.mStride
+ + (dst.mCropLeft + xstart) * dst.mBpp;
+
+ const uint8_t *src_y = (const uint8_t *)src.mBits + src.mCropTop * src.mStride
+ + (src.mCropLeft + xstart) * src.mBpp;
+
+ const uint8_t *src_u = (const uint8_t *)src.mBits + src.mStride * src.mHeight
+ + (src.mCropTop / 2) * (src.mStride / 2)
+ + ((src.mCropLeft + xstart) / 2) * src.mBpp;
+
+ const uint8_t *src_v = src_u + (src.mStride / 2) * (src.mHeight / 2);
+
+ for (size_t y = 0; y < src.cropHeight(); y++) {
+ uint16_t *ptr_y = (uint16_t*) src_y;
+ uint16_t *ptr_u = (uint16_t*) src_u;
+ uint16_t *ptr_v = (uint16_t*) src_v;
+ uint32_t *ptr_out = (uint32_t *) out;
+ for (size_t x = xstart; x < src.cropWidth(); x += 2) {
+ uint16_t u = *ptr_u++;
+ uint16_t v = *ptr_v++;
+ uint32_t y01 = *((uint32_t*)ptr_y); ptr_y += 2;
+ uint32_t uv = u | (((uint32_t)v) << 20);
+ *ptr_out++ = ((y01 & 0x3FF) << 10) | uv;
+ *ptr_out++ = ((y01 >> 16) << 10) | uv;
+ }
+ src_y += src.mStride;
+ if (y & 1) {
+ src_u += src.mStride / 2;
+ src_v += src.mStride / 2;
+ }
+ out += dst.mStride;
+ }
+ }
+
+ return OK;
+}
+
+#endif // USE_NEON_Y410
+
status_t ColorConverter::convertQCOMYUV420SemiPlanar(
const BitmapParams &src, const BitmapParams &dst) {
uint8_t *kAdjustedClip = initClip();
diff --git a/media/libstagefright/colorconversion/SoftwareRenderer.cpp b/media/libstagefright/colorconversion/SoftwareRenderer.cpp
index a07787a..fca9c09 100644
--- a/media/libstagefright/colorconversion/SoftwareRenderer.cpp
+++ b/media/libstagefright/colorconversion/SoftwareRenderer.cpp
@@ -18,10 +18,11 @@
#include <utils/Log.h>
#include "../include/SoftwareRenderer.h"
-
#include <cutils/properties.h> // for property_get
#include <media/stagefright/foundation/ADebug.h>
#include <media/stagefright/foundation/AMessage.h>
+#include <media/stagefright/foundation/ColorUtils.h>
+#include <media/stagefright/SurfaceUtils.h>
#include <system/window.h>
#include <ui/Fence.h>
#include <ui/GraphicBufferMapper.h>
@@ -30,7 +31,6 @@
namespace android {
-
static int ALIGN(int x, int y) {
// y must be a power of 2.
return (x + y - 1) & ~(y - 1);
@@ -50,7 +50,9 @@
mCropBottom(0),
mCropWidth(0),
mCropHeight(0),
- mRotationDegrees(rotation) {
+ mRotationDegrees(rotation),
+ mDataSpace(HAL_DATASPACE_UNKNOWN) {
+ memset(&mHDRStaticInfo, 0, sizeof(mHDRStaticInfo));
}
SoftwareRenderer::~SoftwareRenderer() {
@@ -130,6 +132,13 @@
bufHeight = (mCropHeight + 1) & ~1;
break;
}
+ case OMX_COLOR_FormatYUV420Planar16:
+ {
+ halFormat = HAL_PIXEL_FORMAT_RGBA_1010102;
+ bufWidth = (mCropWidth + 1) & ~1;
+ bufHeight = (mCropHeight + 1) & ~1;
+ break;
+ }
default:
{
break;
@@ -141,6 +150,10 @@
mConverter = new ColorConverter(
mColorFormat, OMX_COLOR_Format16bitRGB565);
CHECK(mConverter->isValid());
+ } else if (mColorFormat == OMX_COLOR_FormatYUV420Planar16) {
+ mConverter = new ColorConverter(
+ mColorFormat, OMX_COLOR_Format32BitRGBA1010102);
+ CHECK(mConverter->isValid());
}
CHECK(mNativeWindow != NULL);
@@ -365,12 +378,29 @@
// color conversion to RGB. For now, just mark dataspace for YUV rendering.
android_dataspace dataSpace;
if (format->findInt32("android._dataspace", (int32_t *)&dataSpace) && dataSpace != mDataSpace) {
+ mDataSpace = dataSpace;
+
+ if (mConverter != NULL) {
+ // graphics only supports full range RGB. ColorConverter should have
+ // converted any YUV to full range.
+ dataSpace = (android_dataspace)
+ ((dataSpace & ~HAL_DATASPACE_RANGE_MASK) | HAL_DATASPACE_RANGE_FULL);
+ }
+
ALOGD("setting dataspace on output surface to #%x", dataSpace);
if ((err = native_window_set_buffers_data_space(mNativeWindow.get(), dataSpace))) {
ALOGW("failed to set dataspace on surface (%d)", err);
}
- mDataSpace = dataSpace;
}
+ if (format->contains("hdr-static-info")) {
+ HDRStaticInfo info;
+ if (ColorUtils::getHDRStaticInfoFromFormat(format, &info)
+ && memcmp(&mHDRStaticInfo, &info, sizeof(info))) {
+ setNativeWindowHdrMetadata(mNativeWindow.get(), &info);
+ mHDRStaticInfo = info;
+ }
+ }
+
if ((err = mNativeWindow->queueBuffer(mNativeWindow.get(), buf, -1)) != 0) {
ALOGW("Surface::queueBuffer returned error %d", err);
} else {
diff --git a/media/libstagefright/exports.lds b/media/libstagefright/exports.lds
new file mode 100644
index 0000000..59dfc49
--- /dev/null
+++ b/media/libstagefright/exports.lds
@@ -0,0 +1,508 @@
+{
+ global:
+ *;
+ local:
+ _ZN7android4ESDS*;
+ _ZNK7android4ESDS*;
+ _ZN7android14ColorConverter*;
+ _ZNK7android14ColorConverter*;
+ _ZN7android16SoftwareRenderer*;
+ ABGRToARGB;
+ ABGRToI420;
+ ABGRToUVRow_Any_NEON;
+ ABGRToUVRow_C;
+ ABGRToUVRow_NEON;
+ ABGRToYRow_Any_NEON;
+ ABGRToYRow_C;
+ ABGRToYRow_NEON;
+ Android420ToI420;
+ ARGB1555ToARGB;
+ ARGB1555ToARGBRow_Any_NEON;
+ ARGB1555ToARGBRow_C;
+ ARGB1555ToARGBRow_NEON;
+ ARGB1555ToI420;
+ ARGB1555ToUVRow_Any_NEON;
+ ARGB1555ToUVRow_C;
+ ARGB1555ToUVRow_NEON;
+ ARGB1555ToYRow_Any_NEON;
+ ARGB1555ToYRow_C;
+ ARGB1555ToYRow_NEON;
+ ARGB4444ToARGB;
+ ARGB4444ToARGBRow_Any_NEON;
+ ARGB4444ToARGBRow_C;
+ ARGB4444ToARGBRow_NEON;
+ ARGB4444ToI420;
+ ARGB4444ToUVRow_Any_NEON;
+ ARGB4444ToUVRow_C;
+ ARGB4444ToUVRow_NEON;
+ ARGB4444ToYRow_Any_NEON;
+ ARGB4444ToYRow_C;
+ ARGB4444ToYRow_NEON;
+ ARGBAdd;
+ ARGBAddRow_Any_NEON;
+ ARGBAddRow_C;
+ ARGBAddRow_NEON;
+ ARGBAffineRow_C;
+ ARGBAttenuate;
+ ARGBAttenuateRow_Any_NEON;
+ ARGBAttenuateRow_C;
+ ARGBAttenuateRow_NEON;
+ ARGBBlend;
+ ARGBBlendRow_C;
+ ARGBBlendRow_NEON;
+ ARGBBlur;
+ ARGBColorMatrix;
+ ARGBColorMatrixRow_C;
+ ARGBColorMatrixRow_NEON;
+ ARGBColorTable;
+ ARGBColorTableRow_C;
+ ARGBComputeCumulativeSum;
+ ARGBCopy;
+ ARGBCopyAlpha;
+ ARGBCopyAlphaRow_C;
+ ARGBCopyYToAlpha;
+ ARGBCopyYToAlphaRow_C;
+ ARGBExtractAlpha;
+ ARGBExtractAlphaRow_Any_NEON;
+ ARGBExtractAlphaRow_C;
+ ARGBExtractAlphaRow_NEON;
+ ARGBGray;
+ ARGBGrayRow_C;
+ ARGBGrayRow_NEON;
+ ARGBGrayTo;
+ ARGBInterpolate;
+ ARGBLumaColorTable;
+ ARGBLumaColorTableRow_C;
+ ARGBMirror;
+ ARGBMirrorRow_Any_NEON;
+ ARGBMirrorRow_C;
+ ARGBMirrorRow_NEON;
+ ARGBMultiply;
+ ARGBMultiplyRow_Any_NEON;
+ ARGBMultiplyRow_C;
+ ARGBMultiplyRow_NEON;
+ ARGBPolynomial;
+ ARGBPolynomialRow_C;
+ ARGBQuantize;
+ ARGBQuantizeRow_C;
+ ARGBQuantizeRow_NEON;
+ ARGBRect;
+ ARGBSepia;
+ ARGBSepiaRow_C;
+ ARGBSepiaRow_NEON;
+ ARGBSetRow_Any_NEON;
+ ARGBSetRow_C;
+ ARGBSetRow_NEON;
+ ARGBShade;
+ ARGBShadeRow_C;
+ ARGBShadeRow_NEON;
+ ARGBShuffle;
+ ARGBShuffleRow_Any_NEON;
+ ARGBShuffleRow_C;
+ ARGBShuffleRow_NEON;
+ ARGBSobel;
+ ARGBSobelToPlane;
+ ARGBSobelXY;
+ ARGBSubtract;
+ ARGBSubtractRow_Any_NEON;
+ ARGBSubtractRow_C;
+ ARGBSubtractRow_NEON;
+ ARGBToABGR;
+ ARGBToARGB1555Row_Any_NEON;
+ ARGBToARGB1555Row_C;
+ ARGBToARGB1555Row_NEON;
+ ARGBToARGB4444Row_Any_NEON;
+ ARGBToARGB4444Row_C;
+ ARGBToARGB4444Row_NEON;
+ ARGBToBGRA;
+ ARGBToI420;
+ ARGBToRAWRow_Any_NEON;
+ ARGBToRAWRow_C;
+ ARGBToRAWRow_NEON;
+ ARGBToRGB24Row_Any_NEON;
+ ARGBToRGB24Row_C;
+ ARGBToRGB24Row_NEON;
+ ARGBToRGB565DitherRow_Any_NEON;
+ ARGBToRGB565DitherRow_C;
+ ARGBToRGB565DitherRow_NEON;
+ ARGBToRGB565Row_Any_NEON;
+ ARGBToRGB565Row_C;
+ ARGBToRGB565Row_NEON;
+ ARGBToUV444Row_Any_NEON;
+ ARGBToUV444Row_C;
+ ARGBToUV444Row_NEON;
+ ARGBToUVJRow_Any_NEON;
+ ARGBToUVJRow_C;
+ ARGBToUVJRow_NEON;
+ ARGBToUVRow_Any_NEON;
+ ARGBToUVRow_C;
+ ARGBToUVRow_NEON;
+ ARGBToYJRow_Any_NEON;
+ ARGBToYJRow_C;
+ ARGBToYJRow_NEON;
+ ARGBToYRow_Any_NEON;
+ ARGBToYRow_C;
+ ARGBToYRow_NEON;
+ ARGBUnattenuate;
+ ARGBUnattenuateRow_C;
+ ArmCpuCaps;
+ BGRAToARGB;
+ BGRAToI420;
+ BGRAToUVRow_Any_NEON;
+ BGRAToUVRow_C;
+ BGRAToUVRow_NEON;
+ BGRAToYRow_Any_NEON;
+ BGRAToYRow_C;
+ BGRAToYRow_NEON;
+ BlendPlane;
+ BlendPlaneRow_C;
+ CanonicalFourCC;
+ ComputeCumulativeSumRow_C;
+ ConvertFromI420;
+ CopyPlane;
+ CopyPlane_16;
+ CopyRow_16_C;
+ CopyRow_Any_NEON;
+ CopyRow_C;
+ CopyRow_NEON;
+ CpuId;
+ cpu_info_;
+ CumulativeSumToAverageRow_C;
+ FixedDiv1_C;
+ FixedDiv_C;
+ fixed_invtbl8;
+ GetARGBBlend;
+ H420ToABGR;
+ H420ToARGB;
+ H422ToABGR;
+ H422ToARGB;
+ HalfFloat1Row_Any_NEON;
+ HalfFloat1Row_NEON;
+ HalfFloatPlane;
+ HalfFloatRow_Any_NEON;
+ HalfFloatRow_C;
+ HalfFloatRow_NEON;
+ I400Copy;
+ I400Mirror;
+ I400ToARGB;
+ I400ToARGBRow_Any_NEON;
+ I400ToARGBRow_C;
+ I400ToARGBRow_NEON;
+ I400ToI400;
+ I400ToI420;
+ I420AlphaToABGR;
+ I420AlphaToARGB;
+ I420Blend;
+ I420Copy;
+ I420Interpolate;
+ I420Mirror;
+ I420Rect;
+ I420Scale;
+ I420Scale_16;
+ I420ToABGR;
+ I420ToARGB;
+ I420ToARGB1555;
+ I420ToARGB4444;
+ I420ToBGRA;
+ I420ToI400;
+ I420ToI422;
+ I420ToI444;
+ I420ToNV12;
+ I420ToNV21;
+ I420ToRAW;
+ I420ToRGB24;
+ I420ToRGB565;
+ I420ToRGB565Dither;
+ I420ToRGBA;
+ I420ToUYVY;
+ I420ToYUY2;
+ I422AlphaToARGBRow_Any_NEON;
+ I422AlphaToARGBRow_C;
+ I422AlphaToARGBRow_NEON;
+ I422Copy;
+ I422ToABGR;
+ I422ToARGB;
+ I422ToARGB1555Row_Any_NEON;
+ I422ToARGB1555Row_C;
+ I422ToARGB1555Row_NEON;
+ I422ToARGB4444Row_Any_NEON;
+ I422ToARGB4444Row_C;
+ I422ToARGB4444Row_NEON;
+ I422ToARGBRow_Any_NEON;
+ I422ToARGBRow_C;
+ I422ToARGBRow_NEON;
+ I422ToBGRA;
+ I422ToI420;
+ I422ToRGB24Row_Any_NEON;
+ I422ToRGB24Row_C;
+ I422ToRGB24Row_NEON;
+ I422ToRGB565;
+ I422ToRGB565Row_Any_NEON;
+ I422ToRGB565Row_C;
+ I422ToRGB565Row_NEON;
+ I422ToRGBA;
+ I422ToRGBARow_Any_NEON;
+ I422ToRGBARow_C;
+ I422ToRGBARow_NEON;
+ I422ToUYVY;
+ I422ToUYVYRow_Any_NEON;
+ I422ToUYVYRow_C;
+ I422ToUYVYRow_NEON;
+ I422ToYUY2;
+ I422ToYUY2Row_Any_NEON;
+ I422ToYUY2Row_C;
+ I422ToYUY2Row_NEON;
+ I444Copy;
+ I444ToABGR;
+ I444ToARGB;
+ I444ToARGBRow_Any_NEON;
+ I444ToARGBRow_C;
+ I444ToARGBRow_NEON;
+ I444ToI420;
+ InitCpuFlags;
+ InterpolatePlane;
+ InterpolateRow_16_C;
+ InterpolateRow_Any_NEON;
+ InterpolateRow_C;
+ InterpolateRow_NEON;
+ J400ToARGB;
+ J400ToARGBRow_Any_NEON;
+ J400ToARGBRow_C;
+ J400ToARGBRow_NEON;
+ J420ToABGR;
+ J420ToARGB;
+ J422ToABGR;
+ J422ToARGB;
+ J444ToARGB;
+ kYuvH709Constants;
+ kYuvI601Constants;
+ kYuvJPEGConstants;
+ kYvuH709Constants;
+ kYvuI601Constants;
+ kYvuJPEGConstants;
+ M420ToARGB;
+ M420ToI420;
+ MaskCpuFlags;
+ MergeUVPlane;
+ MergeUVRow_Any_NEON;
+ MergeUVRow_C;
+ MergeUVRow_NEON;
+ MipsCpuCaps;
+ MirrorPlane;
+ MirrorRow_Any_NEON;
+ MirrorRow_C;
+ MirrorRow_NEON;
+ MirrorUVRow_C;
+ MirrorUVRow_NEON;
+ NV12ToARGBRow_Any_NEON;
+ NV12ToARGBRow_C;
+ NV12ToARGBRow_NEON;
+ NV12ToI420;
+ NV12ToRGB565;
+ NV12ToRGB565Row_Any_NEON;
+ NV12ToRGB565Row_C;
+ NV12ToRGB565Row_NEON;
+ NV21ToARGB;
+ NV21ToARGBRow_Any_NEON;
+ NV21ToARGBRow_C;
+ NV21ToARGBRow_NEON;
+ NV21ToI420;
+ RAWToARGB;
+ RAWToARGBRow_Any_NEON;
+ RAWToARGBRow_C;
+ RAWToARGBRow_NEON;
+ RAWToI420;
+ RAWToRGB24;
+ RAWToRGB24Row_Any_NEON;
+ RAWToRGB24Row_C;
+ RAWToRGB24Row_NEON;
+ RAWToUVRow_Any_NEON;
+ RAWToUVRow_C;
+ RAWToUVRow_NEON;
+ RAWToYRow_Any_NEON;
+ RAWToYRow_C;
+ RAWToYRow_NEON;
+ RGB24ToARGB;
+ RGB24ToARGBRow_Any_NEON;
+ RGB24ToARGBRow_C;
+ RGB24ToARGBRow_NEON;
+ RGB24ToI420;
+ RGB24ToUVRow_Any_NEON;
+ RGB24ToUVRow_C;
+ RGB24ToUVRow_NEON;
+ RGB24ToYRow_Any_NEON;
+ RGB24ToYRow_C;
+ RGB24ToYRow_NEON;
+ RGB565ToARGB;
+ RGB565ToARGBRow_Any_NEON;
+ RGB565ToARGBRow_C;
+ RGB565ToARGBRow_NEON;
+ RGB565ToI420;
+ RGB565ToUVRow_Any_NEON;
+ RGB565ToUVRow_C;
+ RGB565ToUVRow_NEON;
+ RGB565ToYRow_Any_NEON;
+ RGB565ToYRow_C;
+ RGB565ToYRow_NEON;
+ RGBAToARGB;
+ RGBAToI420;
+ RGBAToUVRow_Any_NEON;
+ RGBAToUVRow_C;
+ RGBAToUVRow_NEON;
+ RGBAToYRow_Any_NEON;
+ RGBAToYRow_C;
+ RGBAToYRow_NEON;
+ RGBColorMatrix;
+ RGBColorTable;
+ RGBColorTableRow_C;
+ Scale;
+ ScaleAddRow_16_C;
+ ScaleAddRow_C;
+ ScaleAddRows_NEON;
+ ScaleARGBCols64_C;
+ ScaleARGBCols_Any_NEON;
+ ScaleARGBCols_C;
+ ScaleARGBCols_NEON;
+ ScaleARGBColsUp2_C;
+ ScaleARGBFilterCols64_C;
+ ScaleARGBFilterCols_Any_NEON;
+ ScaleARGBFilterCols_C;
+ ScaleARGBFilterCols_NEON;
+ ScaleARGBRowDown2_Any_NEON;
+ ScaleARGBRowDown2Box_Any_NEON;
+ ScaleARGBRowDown2Box_C;
+ ScaleARGBRowDown2Box_NEON;
+ ScaleARGBRowDown2_C;
+ ScaleARGBRowDown2Linear_Any_NEON;
+ ScaleARGBRowDown2Linear_C;
+ ScaleARGBRowDown2Linear_NEON;
+ ScaleARGBRowDown2_NEON;
+ ScaleARGBRowDownEven_Any_NEON;
+ ScaleARGBRowDownEvenBox_Any_NEON;
+ ScaleARGBRowDownEvenBox_C;
+ ScaleARGBRowDownEvenBox_NEON;
+ ScaleARGBRowDownEven_C;
+ ScaleARGBRowDownEven_NEON;
+ ScaleCols_16_C;
+ ScaleCols_C;
+ ScaleColsUp2_16_C;
+ ScaleColsUp2_C;
+ ScaleFilterCols_16_C;
+ ScaleFilterCols64_16_C;
+ ScaleFilterCols64_C;
+ ScaleFilterCols_Any_NEON;
+ ScaleFilterCols_C;
+ ScaleFilterCols_NEON;
+ ScaleFilterReduce;
+ ScaleFilterRows_NEON;
+ ScaleOffset;
+ ScalePlane;
+ ScalePlane_16;
+ ScalePlaneBilinearDown;
+ ScalePlaneBilinearDown_16;
+ ScalePlaneBilinearUp;
+ ScalePlaneBilinearUp_16;
+ ScalePlaneVertical;
+ ScalePlaneVertical_16;
+ ScaleRowDown2_16_C;
+ ScaleRowDown2_Any_NEON;
+ ScaleRowDown2Box_16_C;
+ ScaleRowDown2Box_Any_NEON;
+ ScaleRowDown2Box_C;
+ ScaleRowDown2Box_NEON;
+ ScaleRowDown2Box_Odd_C;
+ ScaleRowDown2Box_Odd_NEON;
+ ScaleRowDown2_C;
+ ScaleRowDown2Linear_16_C;
+ ScaleRowDown2Linear_Any_NEON;
+ ScaleRowDown2Linear_C;
+ ScaleRowDown2Linear_NEON;
+ ScaleRowDown2_NEON;
+ ScaleRowDown34_0_Box_16_C;
+ ScaleRowDown34_0_Box_Any_NEON;
+ ScaleRowDown34_0_Box_C;
+ ScaleRowDown34_0_Box_NEON;
+ ScaleRowDown34_16_C;
+ ScaleRowDown34_1_Box_16_C;
+ ScaleRowDown34_1_Box_Any_NEON;
+ ScaleRowDown34_1_Box_C;
+ ScaleRowDown34_1_Box_NEON;
+ ScaleRowDown34_Any_NEON;
+ ScaleRowDown34_C;
+ ScaleRowDown34_NEON;
+ ScaleRowDown38_16_C;
+ ScaleRowDown38_2_Box_16_C;
+ ScaleRowDown38_2_Box_Any_NEON;
+ ScaleRowDown38_2_Box_C;
+ ScaleRowDown38_2_Box_NEON;
+ ScaleRowDown38_3_Box_16_C;
+ ScaleRowDown38_3_Box_Any_NEON;
+ ScaleRowDown38_3_Box_C;
+ ScaleRowDown38_3_Box_NEON;
+ ScaleRowDown38_Any_NEON;
+ ScaleRowDown38_C;
+ ScaleRowDown38_NEON;
+ ScaleRowDown4_16_C;
+ ScaleRowDown4_Any_NEON;
+ ScaleRowDown4Box_16_C;
+ ScaleRowDown4Box_Any_NEON;
+ ScaleRowDown4Box_C;
+ ScaleRowDown4Box_NEON;
+ ScaleRowDown4_C;
+ ScaleRowDown4_NEON;
+ ScaleSlope;
+ SetPlane;
+ SetRow_Any_NEON;
+ SetRow_C;
+ SetRow_NEON;
+ SobelRow_Any_NEON;
+ SobelRow_C;
+ SobelRow_NEON;
+ SobelToPlaneRow_Any_NEON;
+ SobelToPlaneRow_C;
+ SobelToPlaneRow_NEON;
+ SobelXRow_C;
+ SobelXRow_NEON;
+ SobelXYRow_Any_NEON;
+ SobelXYRow_C;
+ SobelXYRow_NEON;
+ SobelYRow_C;
+ SobelYRow_NEON;
+ SplitUVPlane;
+ SplitUVRow_Any_NEON;
+ SplitUVRow_C;
+ SplitUVRow_NEON;
+ UYVYToARGB;
+ UYVYToARGBRow_Any_NEON;
+ UYVYToARGBRow_C;
+ UYVYToARGBRow_NEON;
+ UYVYToI420;
+ UYVYToI422;
+ UYVYToNV12;
+ UYVYToUV422Row_Any_NEON;
+ UYVYToUV422Row_C;
+ UYVYToUV422Row_NEON;
+ UYVYToUVRow_Any_NEON;
+ UYVYToUVRow_C;
+ UYVYToUVRow_NEON;
+ UYVYToYRow_Any_NEON;
+ UYVYToYRow_C;
+ UYVYToYRow_NEON;
+ YUY2ToARGB;
+ YUY2ToARGBRow_Any_NEON;
+ YUY2ToARGBRow_C;
+ YUY2ToARGBRow_NEON;
+ YUY2ToI420;
+ YUY2ToI422;
+ YUY2ToNV12;
+ YUY2ToUV422Row_Any_NEON;
+ YUY2ToUV422Row_C;
+ YUY2ToUV422Row_NEON;
+ YUY2ToUVRow_Any_NEON;
+ YUY2ToUVRow_C;
+ YUY2ToUVRow_NEON;
+ YUY2ToY;
+ YUY2ToYRow_Any_NEON;
+ YUY2ToYRow_C;
+ YUY2ToYRow_NEON;
+};
diff --git a/media/libstagefright/include/SoftwareRenderer.h b/media/libstagefright/include/SoftwareRenderer.h
index 258511a..e04b59f 100644
--- a/media/libstagefright/include/SoftwareRenderer.h
+++ b/media/libstagefright/include/SoftwareRenderer.h
@@ -22,6 +22,7 @@
#include <media/stagefright/FrameRenderTracker.h>
#include <utils/RefBase.h>
#include <system/window.h>
+#include <media/hardware/VideoAPI.h>
#include <list>
@@ -55,6 +56,7 @@
int32_t mCropWidth, mCropHeight;
int32_t mRotationDegrees;
android_dataspace mDataSpace;
+ HDRStaticInfo mHDRStaticInfo;
FrameRenderTracker mRenderTracker;
SoftwareRenderer(const SoftwareRenderer &);
diff --git a/media/libstagefright/include/media/stagefright/ACodec.h b/media/libstagefright/include/media/stagefright/ACodec.h
index d1a9d25..3196b10 100644
--- a/media/libstagefright/include/media/stagefright/ACodec.h
+++ b/media/libstagefright/include/media/stagefright/ACodec.h
@@ -235,6 +235,7 @@
int mNativeWindowUsageBits;
android_native_rect_t mLastNativeWindowCrop;
int32_t mLastNativeWindowDataSpace;
+ HDRStaticInfo mLastHDRStaticInfo;
sp<AMessage> mConfigFormat;
sp<AMessage> mInputFormat;
sp<AMessage> mOutputFormat;
diff --git a/media/libstagefright/include/media/stagefright/ColorConverter.h b/media/libstagefright/include/media/stagefright/ColorConverter.h
index 7ac9b37..f6bd353 100644
--- a/media/libstagefright/include/media/stagefright/ColorConverter.h
+++ b/media/libstagefright/include/media/stagefright/ColorConverter.h
@@ -73,6 +73,9 @@
status_t convertYUV420Planar(
const BitmapParams &src, const BitmapParams &dst);
+ status_t convertYUV420Planar16(
+ const BitmapParams &src, const BitmapParams &dst);
+
status_t convertYUV420PlanarUseLibYUV(
const BitmapParams &src, const BitmapParams &dst);
diff --git a/media/libstagefright/include/media/stagefright/MediaExtractorFactory.h b/media/libstagefright/include/media/stagefright/MediaExtractorFactory.h
index 7fddf80..55654f1 100644
--- a/media/libstagefright/include/media/stagefright/MediaExtractorFactory.h
+++ b/media/libstagefright/include/media/stagefright/MediaExtractorFactory.h
@@ -41,6 +41,7 @@
static sp<IMediaExtractor> CreateFromService(
const sp<DataSource> &source, const char *mime = NULL);
static void LoadPlugins(const ::std::string& apkPath);
+ static status_t dump(int fd, const Vector<String16>& args);
private:
static Mutex gPluginMutex;
diff --git a/media/libstagefright/include/media/stagefright/SurfaceUtils.h b/media/libstagefright/include/media/stagefright/SurfaceUtils.h
index a7747c7..689e458 100644
--- a/media/libstagefright/include/media/stagefright/SurfaceUtils.h
+++ b/media/libstagefright/include/media/stagefright/SurfaceUtils.h
@@ -24,6 +24,8 @@
namespace android {
+struct HDRStaticInfo;
+
/**
* Configures |nativeWindow| for given |width|x|height|, pixel |format|, |rotation| and |usage|.
* If |reconnect| is true, reconnects to the native window before hand.
@@ -32,6 +34,8 @@
status_t setNativeWindowSizeFormatAndUsage(
ANativeWindow *nativeWindow /* nonnull */,
int width, int height, int format, int rotation, int usage, bool reconnect);
+void setNativeWindowHdrMetadata(
+ ANativeWindow *nativeWindow /* nonnull */, HDRStaticInfo *info /* nonnull */);
status_t pushBlankBuffersToNativeWindow(ANativeWindow *nativeWindow /* nonnull */);
status_t nativeWindowConnect(ANativeWindow *surface, const char *reason);
status_t nativeWindowDisconnect(ANativeWindow *surface, const char *reason);
diff --git a/media/libstagefright/omx/OMXUtils.cpp b/media/libstagefright/omx/OMXUtils.cpp
index e032985..f597e02 100644
--- a/media/libstagefright/omx/OMXUtils.cpp
+++ b/media/libstagefright/omx/OMXUtils.cpp
@@ -215,6 +215,9 @@
fmt != OMX_COLOR_FormatYUV420PackedSemiPlanar &&
fmt != (OMX_COLOR_FORMATTYPE)HAL_PIXEL_FORMAT_YV12) {
ALOGW("do not know color format 0x%x = %d", fmt, fmt);
+ if (fmt == OMX_COLOR_FormatYUV420Planar16) {
+ ALOGW("Cannot describe color format OMX_COLOR_FormatYUV420Planar16");
+ }
return false;
}
diff --git a/media/libstagefright/omx/SoftVideoDecoderOMXComponent.cpp b/media/libstagefright/omx/SoftVideoDecoderOMXComponent.cpp
index 8e92539..8ef7620 100644
--- a/media/libstagefright/omx/SoftVideoDecoderOMXComponent.cpp
+++ b/media/libstagefright/omx/SoftVideoDecoderOMXComponent.cpp
@@ -61,6 +61,7 @@
mCropTop(0),
mCropWidth(width),
mCropHeight(height),
+ mOutputFormat(OMX_COLOR_FormatYUV420Planar),
mOutputPortSettingsChange(NONE),
mUpdateColorAspects(false),
mMinInputBufferSize(384), // arbitrary, using one uncompressed macroblock
@@ -74,6 +75,7 @@
memset(&mDefaultColorAspects, 0, sizeof(ColorAspects));
memset(&mBitstreamColorAspects, 0, sizeof(ColorAspects));
memset(&mFinalColorAspects, 0, sizeof(ColorAspects));
+ memset(&mHdrStaticInfo, 0, sizeof(HDRStaticInfo));
}
void SoftVideoDecoderOMXComponent::initPorts(
@@ -140,7 +142,6 @@
def.format.video.xFramerate = 0;
def.format.video.bFlagErrorConcealment = OMX_FALSE;
def.format.video.eCompressionFormat = OMX_VIDEO_CodingUnused;
- def.format.video.eColorFormat = OMX_COLOR_FormatYUV420Planar;
def.format.video.pNativeWindow = NULL;
addPort(def);
@@ -152,11 +153,13 @@
OMX_PARAM_PORTDEFINITIONTYPE *outDef = &editPortInfo(kOutputPortIndex)->mDef;
outDef->format.video.nFrameWidth = outputBufferWidth();
outDef->format.video.nFrameHeight = outputBufferHeight();
+ outDef->format.video.eColorFormat = mOutputFormat;
outDef->format.video.nStride = outDef->format.video.nFrameWidth;
outDef->format.video.nSliceHeight = outDef->format.video.nFrameHeight;
+ int32_t bpp = (mOutputFormat == OMX_COLOR_FormatYUV420Planar16) ? 2 : 1;
outDef->nBufferSize =
- (outDef->format.video.nStride * outDef->format.video.nSliceHeight * 3) / 2;
+ (outDef->format.video.nStride * outDef->format.video.nSliceHeight * bpp * 3) / 2;
OMX_PARAM_PORTDEFINITIONTYPE *inDef = &editPortInfo(kInputPortIndex)->mDef;
inDef->format.video.nFrameWidth = mWidth;
@@ -191,9 +194,11 @@
void SoftVideoDecoderOMXComponent::handlePortSettingsChange(
bool *portWillReset, uint32_t width, uint32_t height,
+ OMX_COLOR_FORMATTYPE outputFormat,
CropSettingsMode cropSettingsMode, bool fakeStride) {
*portWillReset = false;
bool sizeChanged = (width != mWidth || height != mHeight);
+ bool formatChanged = (outputFormat != mOutputFormat);
bool updateCrop = (cropSettingsMode == kCropUnSet);
bool cropChanged = (cropSettingsMode == kCropChanged);
bool strideChanged = false;
@@ -205,13 +210,18 @@
}
}
- if (sizeChanged || cropChanged || strideChanged) {
+ if (formatChanged || sizeChanged || cropChanged || strideChanged) {
+ if (formatChanged) {
+ ALOGD("formatChanged: 0x%08x -> 0x%08x", mOutputFormat, outputFormat);
+ }
+ mOutputFormat = outputFormat;
mWidth = width;
mHeight = height;
if ((sizeChanged && !mIsAdaptive)
|| width > mAdaptiveMaxWidth
- || height > mAdaptiveMaxHeight) {
+ || height > mAdaptiveMaxHeight
+ || formatChanged) {
if (mIsAdaptive) {
if (width > mAdaptiveMaxWidth) {
mAdaptiveMaxWidth = width;
@@ -305,27 +315,30 @@
void SoftVideoDecoderOMXComponent::copyYV12FrameToOutputBuffer(
uint8_t *dst, const uint8_t *srcY, const uint8_t *srcU, const uint8_t *srcV,
size_t srcYStride, size_t srcUStride, size_t srcVStride) {
- size_t dstYStride = outputBufferWidth();
+ OMX_PARAM_PORTDEFINITIONTYPE *outDef = &editPortInfo(kOutputPortIndex)->mDef;
+ int32_t bpp = (outDef->format.video.eColorFormat == OMX_COLOR_FormatYUV420Planar16) ? 2 : 1;
+
+ size_t dstYStride = outputBufferWidth() * bpp;
size_t dstUVStride = dstYStride / 2;
size_t dstHeight = outputBufferHeight();
uint8_t *dstStart = dst;
for (size_t i = 0; i < mHeight; ++i) {
- memcpy(dst, srcY, mWidth);
+ memcpy(dst, srcY, mWidth * bpp);
srcY += srcYStride;
dst += dstYStride;
}
dst = dstStart + dstYStride * dstHeight;
for (size_t i = 0; i < mHeight / 2; ++i) {
- memcpy(dst, srcU, mWidth / 2);
+ memcpy(dst, srcU, mWidth / 2 * bpp);
srcU += srcUStride;
dst += dstUVStride;
}
dst = dstStart + (5 * dstYStride * dstHeight) / 4;
for (size_t i = 0; i < mHeight / 2; ++i) {
- memcpy(dst, srcV, mWidth / 2);
+ memcpy(dst, srcV, mWidth / 2 * bpp);
srcV += srcVStride;
dst += dstUVStride;
}
@@ -562,6 +575,24 @@
return OMX_ErrorNone;
}
+ case kDescribeHdrStaticInfoIndex:
+ {
+ if (!supportDescribeHdrStaticInfo()) {
+ return OMX_ErrorUnsupportedIndex;
+ }
+
+ DescribeHDRStaticInfoParams* hdrStaticInfoParams =
+ (DescribeHDRStaticInfoParams *)params;
+
+ if (hdrStaticInfoParams->nPortIndex != kOutputPortIndex) {
+ return OMX_ErrorBadPortIndex;
+ }
+
+ hdrStaticInfoParams->sInfo = mHdrStaticInfo;
+
+ return OMX_ErrorNone;
+ }
+
default:
return OMX_ErrorUnsupportedIndex;
}
@@ -595,6 +626,28 @@
return OMX_ErrorNone;
}
+ case kDescribeHdrStaticInfoIndex:
+ {
+ if (!supportDescribeHdrStaticInfo()) {
+ return OMX_ErrorUnsupportedIndex;
+ }
+
+ const DescribeHDRStaticInfoParams* hdrStaticInfoParams =
+ (DescribeHDRStaticInfoParams *)params;
+
+ if (hdrStaticInfoParams->nPortIndex != kOutputPortIndex) {
+ return OMX_ErrorBadPortIndex;
+ }
+
+ if (hdrStaticInfoParams != NULL) {
+ mOutputFormat = OMX_COLOR_FormatYUV420Planar16;
+ mHdrStaticInfo = hdrStaticInfoParams->sInfo;
+ updatePortDefinitions(false);
+ }
+
+ return OMX_ErrorNone;
+ }
+
default:
return OMX_ErrorUnsupportedIndex;
}
@@ -610,6 +663,10 @@
&& supportsDescribeColorAspects()) {
*(int32_t*)index = kDescribeColorAspectsIndex;
return OMX_ErrorNone;
+ } else if (!strcmp(name, "OMX.google.android.index.describeHDRStaticInfo")
+ && supportDescribeHdrStaticInfo()) {
+ *(int32_t*)index = kDescribeHdrStaticInfoIndex;
+ return OMX_ErrorNone;
}
return SimpleSoftOMXComponent::getExtensionIndex(name, index);
@@ -623,6 +680,10 @@
return kNotSupported;
}
+bool SoftVideoDecoderOMXComponent::supportDescribeHdrStaticInfo() {
+ return false;
+}
+
void SoftVideoDecoderOMXComponent::onReset() {
mOutputPortSettingsChange = NONE;
}
diff --git a/media/libstagefright/omx/include/media/stagefright/omx/SoftVideoDecoderOMXComponent.h b/media/libstagefright/omx/include/media/stagefright/omx/SoftVideoDecoderOMXComponent.h
index c9fd745..56fc691 100644
--- a/media/libstagefright/omx/include/media/stagefright/omx/SoftVideoDecoderOMXComponent.h
+++ b/media/libstagefright/omx/include/media/stagefright/omx/SoftVideoDecoderOMXComponent.h
@@ -23,6 +23,7 @@
#include <media/stagefright/foundation/AHandlerReflector.h>
#include <media/stagefright/foundation/ColorUtils.h>
#include <media/IOMX.h>
+#include <media/hardware/HardwareAPI.h>
#include <utils/RefBase.h>
#include <utils/threads.h>
@@ -46,6 +47,7 @@
protected:
enum {
kDescribeColorAspectsIndex = kPrepareForAdaptivePlaybackIndex + 1,
+ kDescribeHdrStaticInfoIndex = kPrepareForAdaptivePlaybackIndex + 2,
};
enum {
@@ -76,6 +78,8 @@
virtual int getColorAspectPreference();
+ virtual bool supportDescribeHdrStaticInfo();
+
// This function sets both minimum buffer count and actual buffer count of
// input port to be |numInputBuffers|. It will also set both minimum buffer
// count and actual buffer count of output port to be |numOutputBuffers|.
@@ -113,7 +117,9 @@
// It will trigger OMX_EventPortSettingsChanged event if necessary.
void handlePortSettingsChange(
bool *portWillReset, uint32_t width, uint32_t height,
- CropSettingsMode cropSettingsMode = kCropUnSet, bool fakeStride = false);
+ OMX_COLOR_FORMATTYPE outputFormat = OMX_COLOR_FormatYUV420Planar,
+ CropSettingsMode cropSettingsMode = kCropUnSet,
+ bool fakeStride = false);
void copyYV12FrameToOutputBuffer(
uint8_t *dst, const uint8_t *srcY, const uint8_t *srcU, const uint8_t *srcV,
@@ -129,7 +135,8 @@
uint32_t mAdaptiveMaxWidth, mAdaptiveMaxHeight;
uint32_t mWidth, mHeight;
uint32_t mCropLeft, mCropTop, mCropWidth, mCropHeight;
-
+ OMX_COLOR_FORMATTYPE mOutputFormat;
+ HDRStaticInfo mHdrStaticInfo;
enum {
NONE,
AWAITING_DISABLED,
diff --git a/packages/MediaComponents/Android.mk b/packages/MediaComponents/Android.mk
index 2fa4a82..bbd2afa 100644
--- a/packages/MediaComponents/Android.mk
+++ b/packages/MediaComponents/Android.mk
@@ -32,6 +32,8 @@
LOCAL_MULTILIB := first
+LOCAL_JAVA_LIBRARIES += android-support-annotations
+
# Embed native libraries in package, rather than installing to /system/lib*.
# TODO: Find a right way to include libs in the apk. b/72066556
LOCAL_MODULE_TAGS := samples
@@ -49,4 +51,11 @@
liboggextractor \
libwavextractor \
+# TODO: Remove dependency with other support libraries.
+LOCAL_STATIC_ANDROID_LIBRARIES += \
+ android-support-v4 \
+ android-support-v7-appcompat \
+ android-support-v7-palette
+LOCAL_USE_AAPT2 := true
+
include $(BUILD_PACKAGE)
diff --git a/packages/MediaComponents/proguard.cfg b/packages/MediaComponents/proguard.cfg
index 874dbf5..43f2e63 100644
--- a/packages/MediaComponents/proguard.cfg
+++ b/packages/MediaComponents/proguard.cfg
@@ -16,5 +16,5 @@
# Keep entry point for updatable Java classes
-keep public class com.android.media.update.ApiFactory {
- public static java.lang.Object initialize(android.content.Context);
+ public static java.lang.Object initialize(android.content.res.Resources, android.content.res.Resources$Theme);
}
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_audiotrack_dark.png b/packages/MediaComponents/res/drawable-hdpi/ic_audiotrack_dark.png
new file mode 100644
index 0000000..17fd51f
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_audiotrack_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_audiotrack_light.png b/packages/MediaComponents/res/drawable-hdpi/ic_audiotrack_light.png
new file mode 100644
index 0000000..d7c8252
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_audiotrack_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_dialog_close_dark.png b/packages/MediaComponents/res/drawable-hdpi/ic_dialog_close_dark.png
new file mode 100644
index 0000000..928ddea
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_dialog_close_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_dialog_close_light.png b/packages/MediaComponents/res/drawable-hdpi/ic_dialog_close_light.png
new file mode 100644
index 0000000..1a9cd75
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_dialog_close_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_media_cc_disabled.png b/packages/MediaComponents/res/drawable-hdpi/ic_media_cc_disabled.png
new file mode 100644
index 0000000..0354f61
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_media_cc_disabled.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_media_cc_enabled.png b/packages/MediaComponents/res/drawable-hdpi/ic_media_cc_enabled.png
new file mode 100644
index 0000000..5f8febe
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_media_cc_enabled.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_media_pause_dark.png b/packages/MediaComponents/res/drawable-hdpi/ic_media_pause_dark.png
new file mode 100644
index 0000000..7192ad4
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_media_pause_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_media_pause_light.png b/packages/MediaComponents/res/drawable-hdpi/ic_media_pause_light.png
new file mode 100644
index 0000000..bb707ea
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_media_pause_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_media_play_dark.png b/packages/MediaComponents/res/drawable-hdpi/ic_media_play_dark.png
new file mode 100644
index 0000000..0c32d00
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_media_play_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_media_play_light.png b/packages/MediaComponents/res/drawable-hdpi/ic_media_play_light.png
new file mode 100644
index 0000000..5345ee3
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_media_play_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_media_stop_dark.png b/packages/MediaComponents/res/drawable-hdpi/ic_media_stop_dark.png
new file mode 100644
index 0000000..801d341
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_media_stop_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_media_stop_light.png b/packages/MediaComponents/res/drawable-hdpi/ic_media_stop_light.png
new file mode 100644
index 0000000..9d6b65d
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_media_stop_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_mr_button_disabled_dark.png b/packages/MediaComponents/res/drawable-hdpi/ic_mr_button_disabled_dark.png
new file mode 100644
index 0000000..8ad305d
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_mr_button_disabled_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_mr_button_disabled_light.png b/packages/MediaComponents/res/drawable-hdpi/ic_mr_button_disabled_light.png
new file mode 100644
index 0000000..887fde4
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_mr_button_disabled_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_mr_button_disconnected_dark.png b/packages/MediaComponents/res/drawable-hdpi/ic_mr_button_disconnected_dark.png
new file mode 100644
index 0000000..5739df7
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_mr_button_disconnected_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_mr_button_disconnected_light.png b/packages/MediaComponents/res/drawable-hdpi/ic_mr_button_disconnected_light.png
new file mode 100644
index 0000000..58c344a
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_mr_button_disconnected_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_mr_button_grey.png b/packages/MediaComponents/res/drawable-hdpi/ic_mr_button_grey.png
new file mode 100644
index 0000000..1a03420
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_mr_button_grey.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_vol_type_speaker_dark.png b/packages/MediaComponents/res/drawable-hdpi/ic_vol_type_speaker_dark.png
new file mode 100755
index 0000000..723e455
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_vol_type_speaker_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_vol_type_speaker_group_dark.png b/packages/MediaComponents/res/drawable-hdpi/ic_vol_type_speaker_group_dark.png
new file mode 100755
index 0000000..40c25a3
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_vol_type_speaker_group_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_vol_type_speaker_group_light.png b/packages/MediaComponents/res/drawable-hdpi/ic_vol_type_speaker_group_light.png
new file mode 100755
index 0000000..afdb9c1
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_vol_type_speaker_group_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_vol_type_speaker_light.png b/packages/MediaComponents/res/drawable-hdpi/ic_vol_type_speaker_light.png
new file mode 100755
index 0000000..846c109
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_vol_type_speaker_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_vol_type_tv_dark.png b/packages/MediaComponents/res/drawable-hdpi/ic_vol_type_tv_dark.png
new file mode 100755
index 0000000..33bf484
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_vol_type_tv_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-hdpi/ic_vol_type_tv_light.png b/packages/MediaComponents/res/drawable-hdpi/ic_vol_type_tv_light.png
new file mode 100755
index 0000000..c911b5c
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-hdpi/ic_vol_type_tv_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_audiotrack_dark.png b/packages/MediaComponents/res/drawable-mdpi/ic_audiotrack_dark.png
new file mode 100644
index 0000000..e94ed50
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_audiotrack_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_audiotrack_light.png b/packages/MediaComponents/res/drawable-mdpi/ic_audiotrack_light.png
new file mode 100644
index 0000000..2cf7e0c
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_audiotrack_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_dialog_close_dark.png b/packages/MediaComponents/res/drawable-mdpi/ic_dialog_close_dark.png
new file mode 100644
index 0000000..66558a8
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_dialog_close_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_dialog_close_light.png b/packages/MediaComponents/res/drawable-mdpi/ic_dialog_close_light.png
new file mode 100644
index 0000000..40a1a84
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_dialog_close_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_media_cc_disabled.png b/packages/MediaComponents/res/drawable-mdpi/ic_media_cc_disabled.png
new file mode 100644
index 0000000..0354f61
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_media_cc_disabled.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_media_cc_enabled.png b/packages/MediaComponents/res/drawable-mdpi/ic_media_cc_enabled.png
new file mode 100644
index 0000000..5f8febe
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_media_cc_enabled.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_media_pause_dark.png b/packages/MediaComponents/res/drawable-mdpi/ic_media_pause_dark.png
new file mode 100644
index 0000000..f49aed7
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_media_pause_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_media_pause_light.png b/packages/MediaComponents/res/drawable-mdpi/ic_media_pause_light.png
new file mode 100644
index 0000000..74068ea
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_media_pause_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_media_play_dark.png b/packages/MediaComponents/res/drawable-mdpi/ic_media_play_dark.png
new file mode 100644
index 0000000..9cc777c
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_media_play_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_media_play_light.png b/packages/MediaComponents/res/drawable-mdpi/ic_media_play_light.png
new file mode 100644
index 0000000..f208795
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_media_play_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_media_stop_dark.png b/packages/MediaComponents/res/drawable-mdpi/ic_media_stop_dark.png
new file mode 100644
index 0000000..3ad2c9c
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_media_stop_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_media_stop_light.png b/packages/MediaComponents/res/drawable-mdpi/ic_media_stop_light.png
new file mode 100644
index 0000000..b002ab7
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_media_stop_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_mr_button_disabled_dark.png b/packages/MediaComponents/res/drawable-mdpi/ic_mr_button_disabled_dark.png
new file mode 100644
index 0000000..4446ea4
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_mr_button_disabled_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_mr_button_disabled_light.png b/packages/MediaComponents/res/drawable-mdpi/ic_mr_button_disabled_light.png
new file mode 100644
index 0000000..4d790c6
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_mr_button_disabled_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_mr_button_disconnected_dark.png b/packages/MediaComponents/res/drawable-mdpi/ic_mr_button_disconnected_dark.png
new file mode 100644
index 0000000..c401dc0
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_mr_button_disconnected_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_mr_button_disconnected_light.png b/packages/MediaComponents/res/drawable-mdpi/ic_mr_button_disconnected_light.png
new file mode 100644
index 0000000..e24d586
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_mr_button_disconnected_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_mr_button_grey.png b/packages/MediaComponents/res/drawable-mdpi/ic_mr_button_grey.png
new file mode 100644
index 0000000..ccbb772
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_mr_button_grey.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_vol_type_speaker_dark.png b/packages/MediaComponents/res/drawable-mdpi/ic_vol_type_speaker_dark.png
new file mode 100755
index 0000000..7cc9845
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_vol_type_speaker_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_vol_type_speaker_group_dark.png b/packages/MediaComponents/res/drawable-mdpi/ic_vol_type_speaker_group_dark.png
new file mode 100755
index 0000000..22617e1
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_vol_type_speaker_group_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_vol_type_speaker_group_light.png b/packages/MediaComponents/res/drawable-mdpi/ic_vol_type_speaker_group_light.png
new file mode 100755
index 0000000..cefef3c
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_vol_type_speaker_group_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_vol_type_speaker_light.png b/packages/MediaComponents/res/drawable-mdpi/ic_vol_type_speaker_light.png
new file mode 100755
index 0000000..9a0047c
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_vol_type_speaker_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_vol_type_tv_dark.png b/packages/MediaComponents/res/drawable-mdpi/ic_vol_type_tv_dark.png
new file mode 100755
index 0000000..ca5d6a2
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_vol_type_tv_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-mdpi/ic_vol_type_tv_light.png b/packages/MediaComponents/res/drawable-mdpi/ic_vol_type_tv_light.png
new file mode 100755
index 0000000..8134310
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-mdpi/ic_vol_type_tv_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_audiotrack_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_audiotrack_dark.png
new file mode 100644
index 0000000..b5c899f
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_audiotrack_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_audiotrack_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_audiotrack_light.png
new file mode 100644
index 0000000..4778e00
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_audiotrack_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_dialog_close_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_dialog_close_dark.png
new file mode 100644
index 0000000..f992fc5
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_dialog_close_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_dialog_close_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_dialog_close_light.png
new file mode 100644
index 0000000..d3884e6
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_dialog_close_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_media_pause_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_media_pause_dark.png
new file mode 100644
index 0000000..660ac65
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_media_pause_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_media_pause_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_media_pause_light.png
new file mode 100644
index 0000000..792104f
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_media_pause_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_media_play_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_media_play_dark.png
new file mode 100644
index 0000000..be5c062
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_media_play_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_media_play_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_media_play_light.png
new file mode 100644
index 0000000..d12d495
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_media_play_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_media_stop_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_media_stop_dark.png
new file mode 100644
index 0000000..5239336
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_media_stop_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_media_stop_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_media_stop_light.png
new file mode 100644
index 0000000..5bc5a6c
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_media_stop_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_00_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_00_dark.png
new file mode 100644
index 0000000..f6dd214
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_00_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_00_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_00_light.png
new file mode 100644
index 0000000..6b7bdcd
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_00_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_01_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_01_dark.png
new file mode 100644
index 0000000..c7fe576
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_01_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_01_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_01_light.png
new file mode 100644
index 0000000..0a5d6aa
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_01_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_02_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_02_dark.png
new file mode 100644
index 0000000..0aadfa3
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_02_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_02_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_02_light.png
new file mode 100644
index 0000000..125fe0b
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_02_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_03_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_03_dark.png
new file mode 100644
index 0000000..05c48a7
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_03_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_03_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_03_light.png
new file mode 100644
index 0000000..741e911
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_03_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_04_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_04_dark.png
new file mode 100644
index 0000000..ae4218a
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_04_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_04_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_04_light.png
new file mode 100644
index 0000000..8b30fab
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_04_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_05_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_05_dark.png
new file mode 100644
index 0000000..d7aa903
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_05_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_05_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_05_light.png
new file mode 100644
index 0000000..f7e2f29
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_05_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_06_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_06_dark.png
new file mode 100644
index 0000000..e7871e2
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_06_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_06_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_06_light.png
new file mode 100644
index 0000000..8c57f63
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_06_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_07_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_07_dark.png
new file mode 100644
index 0000000..0041b01
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_07_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_07_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_07_light.png
new file mode 100644
index 0000000..6dbb694
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_07_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_08_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_08_dark.png
new file mode 100644
index 0000000..08e1013
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_08_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_08_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_08_light.png
new file mode 100644
index 0000000..5c352c3
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_08_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_09_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_09_dark.png
new file mode 100644
index 0000000..70532e9
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_09_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_09_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_09_light.png
new file mode 100644
index 0000000..9c6ba30
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_09_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_10_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_10_dark.png
new file mode 100644
index 0000000..9ba3b5f
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_10_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_10_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_10_light.png
new file mode 100644
index 0000000..bd4bb22
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_10_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_11_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_11_dark.png
new file mode 100644
index 0000000..2156127
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_11_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_11_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_11_light.png
new file mode 100644
index 0000000..b417a9f
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_11_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_12_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_12_dark.png
new file mode 100644
index 0000000..9bf633e
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_12_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_12_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_12_light.png
new file mode 100644
index 0000000..ba51811
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_12_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_13_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_13_dark.png
new file mode 100644
index 0000000..756a53c
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_13_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_13_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_13_light.png
new file mode 100644
index 0000000..4705dca
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_13_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_14_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_14_dark.png
new file mode 100644
index 0000000..50e4ea3
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_14_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_14_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_14_light.png
new file mode 100644
index 0000000..bc6724f
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_14_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_15_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_15_dark.png
new file mode 100644
index 0000000..9e3b410
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_15_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_15_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_15_light.png
new file mode 100644
index 0000000..2f18abd
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_15_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_16_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_16_dark.png
new file mode 100644
index 0000000..de81133
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_16_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_16_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_16_light.png
new file mode 100644
index 0000000..b80b191
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_16_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_17_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_17_dark.png
new file mode 100644
index 0000000..48aba3d
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_17_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_17_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_17_light.png
new file mode 100644
index 0000000..ca34d5b
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_17_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_18_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_18_dark.png
new file mode 100644
index 0000000..e9957b3
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_18_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_18_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_18_light.png
new file mode 100644
index 0000000..a5d384f
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_18_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_19_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_19_dark.png
new file mode 100644
index 0000000..ddc6297
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_19_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_19_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_19_light.png
new file mode 100644
index 0000000..28ab684
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_19_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_20_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_20_dark.png
new file mode 100644
index 0000000..51e7f75
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_20_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_20_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_20_light.png
new file mode 100644
index 0000000..4aa3ca3
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_20_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_21_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_21_dark.png
new file mode 100644
index 0000000..9caecde
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_21_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_21_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_21_light.png
new file mode 100644
index 0000000..1b8d0b6
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_21_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_22_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_22_dark.png
new file mode 100644
index 0000000..400be3c
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_22_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_22_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_22_light.png
new file mode 100644
index 0000000..c14f1bf
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_22_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_23_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_23_dark.png
new file mode 100644
index 0000000..4e18b46
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_23_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_23_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_23_light.png
new file mode 100644
index 0000000..c4c2c00
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_23_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_24_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_24_dark.png
new file mode 100644
index 0000000..98fae44
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_24_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_24_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_24_light.png
new file mode 100644
index 0000000..d64c289
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_24_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_25_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_25_dark.png
new file mode 100644
index 0000000..91f9327
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_25_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_25_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_25_light.png
new file mode 100644
index 0000000..f5e1f69
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_25_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_26_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_26_dark.png
new file mode 100644
index 0000000..3e6fafd
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_26_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_26_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_26_light.png
new file mode 100644
index 0000000..ae2bd87
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_26_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_27_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_27_dark.png
new file mode 100644
index 0000000..f73a1f8
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_27_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_27_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_27_light.png
new file mode 100644
index 0000000..78c1069
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_27_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_28_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_28_dark.png
new file mode 100644
index 0000000..562b803
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_28_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_28_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_28_light.png
new file mode 100644
index 0000000..ddfba02
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_28_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_29_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_29_dark.png
new file mode 100644
index 0000000..257f2d2
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_29_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_29_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_29_light.png
new file mode 100644
index 0000000..38f5478
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_29_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_30_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_30_dark.png
new file mode 100644
index 0000000..f995af0
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_30_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_30_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_30_light.png
new file mode 100644
index 0000000..c50b7f0
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connected_30_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_00_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_00_dark.png
new file mode 100644
index 0000000..f6dd214
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_00_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_00_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_00_light.png
new file mode 100644
index 0000000..6b7bdcd
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_00_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_01_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_01_dark.png
new file mode 100644
index 0000000..c7fe576
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_01_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_01_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_01_light.png
new file mode 100644
index 0000000..0a5d6aa
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_01_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_02_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_02_dark.png
new file mode 100644
index 0000000..0aadfa3
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_02_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_02_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_02_light.png
new file mode 100644
index 0000000..125fe0b
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_02_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_03_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_03_dark.png
new file mode 100644
index 0000000..05c48a7
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_03_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_03_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_03_light.png
new file mode 100644
index 0000000..741e911
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_03_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_04_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_04_dark.png
new file mode 100644
index 0000000..ae4218a
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_04_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_04_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_04_light.png
new file mode 100644
index 0000000..8b30fab
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_04_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_05_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_05_dark.png
new file mode 100644
index 0000000..d7aa903
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_05_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_05_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_05_light.png
new file mode 100644
index 0000000..f7e2f29
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_05_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_06_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_06_dark.png
new file mode 100644
index 0000000..e7871e2
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_06_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_06_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_06_light.png
new file mode 100644
index 0000000..8c57f63
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_06_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_07_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_07_dark.png
new file mode 100644
index 0000000..0041b01
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_07_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_07_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_07_light.png
new file mode 100644
index 0000000..6dbb694
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_07_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_08_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_08_dark.png
new file mode 100644
index 0000000..08e1013
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_08_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_08_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_08_light.png
new file mode 100644
index 0000000..5c352c3
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_08_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_09_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_09_dark.png
new file mode 100644
index 0000000..70532e9
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_09_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_09_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_09_light.png
new file mode 100644
index 0000000..9c6ba30
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_09_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_10_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_10_dark.png
new file mode 100644
index 0000000..9ba3b5f
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_10_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_10_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_10_light.png
new file mode 100644
index 0000000..bd4bb22
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_10_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_11_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_11_dark.png
new file mode 100644
index 0000000..f3570f4
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_11_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_11_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_11_light.png
new file mode 100644
index 0000000..65a403e
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_11_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_12_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_12_dark.png
new file mode 100644
index 0000000..f644bfd
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_12_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_12_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_12_light.png
new file mode 100644
index 0000000..c7d6048
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_12_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_13_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_13_dark.png
new file mode 100644
index 0000000..6e0d558
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_13_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_13_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_13_light.png
new file mode 100644
index 0000000..f3bc48d
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_13_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_14_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_14_dark.png
new file mode 100644
index 0000000..14d8f8e
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_14_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_14_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_14_light.png
new file mode 100644
index 0000000..98b90e5
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_14_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_15_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_15_dark.png
new file mode 100644
index 0000000..83234a7
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_15_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_15_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_15_light.png
new file mode 100644
index 0000000..47d452f
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_15_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_16_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_16_dark.png
new file mode 100644
index 0000000..b81cf5a
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_16_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_16_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_16_light.png
new file mode 100644
index 0000000..20d08b4
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_16_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_17_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_17_dark.png
new file mode 100644
index 0000000..6feb3f1
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_17_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_17_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_17_light.png
new file mode 100644
index 0000000..e6ae8b3
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_17_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_18_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_18_dark.png
new file mode 100644
index 0000000..0b0fc08
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_18_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_18_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_18_light.png
new file mode 100644
index 0000000..c2a16ac
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_18_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_19_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_19_dark.png
new file mode 100644
index 0000000..a3598cc
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_19_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_19_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_19_light.png
new file mode 100644
index 0000000..846d16d
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_19_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_20_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_20_dark.png
new file mode 100644
index 0000000..2070455
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_20_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_20_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_20_light.png
new file mode 100644
index 0000000..ae6db13
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_20_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_21_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_21_dark.png
new file mode 100644
index 0000000..7f3828a
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_21_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_21_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_21_light.png
new file mode 100644
index 0000000..aaccc73
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_21_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_22_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_22_dark.png
new file mode 100644
index 0000000..5c8ced9
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_22_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_22_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_22_light.png
new file mode 100644
index 0000000..ad01b9e
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_22_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_23_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_23_dark.png
new file mode 100644
index 0000000..ce31dd3
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_23_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_23_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_23_light.png
new file mode 100644
index 0000000..9ef78e4
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_23_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_24_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_24_dark.png
new file mode 100644
index 0000000..a7c2cdb
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_24_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_24_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_24_light.png
new file mode 100644
index 0000000..e7c5bea
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_24_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_25_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_25_dark.png
new file mode 100644
index 0000000..ecad0d4
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_25_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_25_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_25_light.png
new file mode 100644
index 0000000..5fa5923
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_25_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_26_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_26_dark.png
new file mode 100644
index 0000000..f687e25
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_26_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_26_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_26_light.png
new file mode 100644
index 0000000..9c06db8
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_26_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_27_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_27_dark.png
new file mode 100644
index 0000000..90225ba
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_27_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_27_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_27_light.png
new file mode 100644
index 0000000..19697de
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_27_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_28_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_28_dark.png
new file mode 100644
index 0000000..d37ec21
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_28_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_28_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_28_light.png
new file mode 100644
index 0000000..21840bf
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_28_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_29_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_29_dark.png
new file mode 100644
index 0000000..5445e3a
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_29_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_29_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_29_light.png
new file mode 100644
index 0000000..2337c65
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_29_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_30_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_30_dark.png
new file mode 100644
index 0000000..f6dd214
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_30_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_30_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_30_light.png
new file mode 100644
index 0000000..6b7bdcd
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_connecting_30_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_disabled_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_disabled_dark.png
new file mode 100644
index 0000000..c4dc132
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_disabled_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_disabled_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_disabled_light.png
new file mode 100644
index 0000000..b14617c
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_disabled_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_disconnected_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_disconnected_dark.png
new file mode 100644
index 0000000..bb30773
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_disconnected_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_disconnected_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_disconnected_light.png
new file mode 100644
index 0000000..a05d7d7
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_disconnected_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_grey.png b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_grey.png
new file mode 100644
index 0000000..2238d58
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_mr_button_grey.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_vol_type_speaker_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_vol_type_speaker_dark.png
new file mode 100755
index 0000000..e40349d
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_vol_type_speaker_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_vol_type_speaker_group_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_vol_type_speaker_group_dark.png
new file mode 100755
index 0000000..f67c463
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_vol_type_speaker_group_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_vol_type_speaker_group_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_vol_type_speaker_group_light.png
new file mode 100755
index 0000000..7fcebf5
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_vol_type_speaker_group_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_vol_type_speaker_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_vol_type_speaker_light.png
new file mode 100755
index 0000000..ea32a7a
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_vol_type_speaker_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_vol_type_tv_dark.png b/packages/MediaComponents/res/drawable-xhdpi/ic_vol_type_tv_dark.png
new file mode 100755
index 0000000..d62ca37
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_vol_type_tv_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xhdpi/ic_vol_type_tv_light.png b/packages/MediaComponents/res/drawable-xhdpi/ic_vol_type_tv_light.png
new file mode 100755
index 0000000..3131256
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xhdpi/ic_vol_type_tv_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_audiotrack_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_audiotrack_dark.png
new file mode 100644
index 0000000..f131e1b
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_audiotrack_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_audiotrack_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_audiotrack_light.png
new file mode 100644
index 0000000..e5946a2
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_audiotrack_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_dialog_close_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_dialog_close_dark.png
new file mode 100644
index 0000000..b85e87f
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_dialog_close_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_dialog_close_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_dialog_close_light.png
new file mode 100644
index 0000000..51b4401
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_dialog_close_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_media_pause_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_media_pause_dark.png
new file mode 100644
index 0000000..3ea7e03
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_media_pause_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_media_pause_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_media_pause_light.png
new file mode 100644
index 0000000..dc63538
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_media_pause_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_media_play_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_media_play_dark.png
new file mode 100644
index 0000000..2745c3a
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_media_play_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_media_play_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_media_play_light.png
new file mode 100644
index 0000000..eda3ba5
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_media_play_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_media_stop_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_media_stop_dark.png
new file mode 100644
index 0000000..035ca18
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_media_stop_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_media_stop_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_media_stop_light.png
new file mode 100644
index 0000000..eac183d
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_media_stop_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_00_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_00_dark.png
new file mode 100644
index 0000000..0db679e
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_00_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_00_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_00_light.png
new file mode 100644
index 0000000..51c6051
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_00_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_01_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_01_dark.png
new file mode 100644
index 0000000..c083914
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_01_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_01_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_01_light.png
new file mode 100644
index 0000000..c3c3caf
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_01_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_02_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_02_dark.png
new file mode 100644
index 0000000..fc444cf
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_02_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_02_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_02_light.png
new file mode 100644
index 0000000..abd6377
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_02_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_03_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_03_dark.png
new file mode 100644
index 0000000..6dbd1da
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_03_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_03_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_03_light.png
new file mode 100644
index 0000000..d2e7108
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_03_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_04_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_04_dark.png
new file mode 100644
index 0000000..d9f596b
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_04_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_04_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_04_light.png
new file mode 100644
index 0000000..4f32e1a
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_04_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_05_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_05_dark.png
new file mode 100644
index 0000000..c568e04
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_05_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_05_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_05_light.png
new file mode 100644
index 0000000..ed20dd9
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_05_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_06_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_06_dark.png
new file mode 100644
index 0000000..bbe39e7
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_06_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_06_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_06_light.png
new file mode 100644
index 0000000..1edc15f
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_06_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_07_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_07_dark.png
new file mode 100644
index 0000000..78aebaf
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_07_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_07_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_07_light.png
new file mode 100644
index 0000000..b5a6a4f
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_07_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_08_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_08_dark.png
new file mode 100644
index 0000000..44b91ce
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_08_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_08_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_08_light.png
new file mode 100644
index 0000000..85f66f9
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_08_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_09_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_09_dark.png
new file mode 100644
index 0000000..51ea34b
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_09_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_09_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_09_light.png
new file mode 100644
index 0000000..952de04
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_09_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_10_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_10_dark.png
new file mode 100644
index 0000000..8b1aa21
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_10_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_10_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_10_light.png
new file mode 100644
index 0000000..534bcc0
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_10_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_11_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_11_dark.png
new file mode 100644
index 0000000..f666b35
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_11_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_11_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_11_light.png
new file mode 100644
index 0000000..145a8fb
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_11_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_12_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_12_dark.png
new file mode 100644
index 0000000..edeb132
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_12_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_12_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_12_light.png
new file mode 100644
index 0000000..9da2b60
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_12_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_13_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_13_dark.png
new file mode 100644
index 0000000..ab80aa9
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_13_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_13_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_13_light.png
new file mode 100644
index 0000000..115efe4
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_13_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_14_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_14_dark.png
new file mode 100644
index 0000000..8c0cc31
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_14_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_14_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_14_light.png
new file mode 100644
index 0000000..e6ae6fc
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_14_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_15_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_15_dark.png
new file mode 100644
index 0000000..b8816c9
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_15_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_15_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_15_light.png
new file mode 100644
index 0000000..bd42931
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_15_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_16_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_16_dark.png
new file mode 100644
index 0000000..10d5b7f
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_16_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_16_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_16_light.png
new file mode 100644
index 0000000..303a0fe
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_16_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_17_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_17_dark.png
new file mode 100644
index 0000000..3c2a655
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_17_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_17_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_17_light.png
new file mode 100644
index 0000000..90debc2
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_17_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_18_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_18_dark.png
new file mode 100644
index 0000000..d3e78a7
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_18_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_18_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_18_light.png
new file mode 100644
index 0000000..3a3f991
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_18_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_19_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_19_dark.png
new file mode 100644
index 0000000..63fad9e
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_19_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_19_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_19_light.png
new file mode 100644
index 0000000..d6dd8d4
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_19_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_20_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_20_dark.png
new file mode 100644
index 0000000..890fd5f
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_20_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_20_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_20_light.png
new file mode 100644
index 0000000..6b0b5c1
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_20_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_21_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_21_dark.png
new file mode 100644
index 0000000..9ce1ef1
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_21_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_21_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_21_light.png
new file mode 100644
index 0000000..81710d4
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_21_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_22_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_22_dark.png
new file mode 100644
index 0000000..861c080
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_22_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_22_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_22_light.png
new file mode 100644
index 0000000..1c4aa21
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_22_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_23_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_23_dark.png
new file mode 100644
index 0000000..59a6b30
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_23_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_23_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_23_light.png
new file mode 100644
index 0000000..c6e8fe0
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_23_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_24_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_24_dark.png
new file mode 100644
index 0000000..57b840e
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_24_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_24_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_24_light.png
new file mode 100644
index 0000000..bf24050
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_24_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_25_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_25_dark.png
new file mode 100644
index 0000000..01c18c1
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_25_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_25_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_25_light.png
new file mode 100644
index 0000000..be9753e
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_25_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_26_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_26_dark.png
new file mode 100644
index 0000000..3f291b1
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_26_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_26_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_26_light.png
new file mode 100644
index 0000000..dc1c619
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_26_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_27_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_27_dark.png
new file mode 100644
index 0000000..6504a70
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_27_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_27_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_27_light.png
new file mode 100644
index 0000000..a7e0a60
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_27_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_28_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_28_dark.png
new file mode 100644
index 0000000..57b1f3e
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_28_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_28_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_28_light.png
new file mode 100644
index 0000000..5c551ec
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_28_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_29_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_29_dark.png
new file mode 100644
index 0000000..238667e
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_29_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_29_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_29_light.png
new file mode 100644
index 0000000..ffb8183
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_29_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_30_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_30_dark.png
new file mode 100644
index 0000000..4893f18
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_30_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_30_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_30_light.png
new file mode 100644
index 0000000..ac5e156
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connected_30_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_00_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_00_dark.png
new file mode 100644
index 0000000..0db679e
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_00_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_00_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_00_light.png
new file mode 100644
index 0000000..51c6051
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_00_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_01_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_01_dark.png
new file mode 100644
index 0000000..c083914
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_01_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_01_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_01_light.png
new file mode 100644
index 0000000..c3c3caf
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_01_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_02_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_02_dark.png
new file mode 100644
index 0000000..fc444cf
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_02_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_02_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_02_light.png
new file mode 100644
index 0000000..abd6377
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_02_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_03_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_03_dark.png
new file mode 100644
index 0000000..6dbd1da
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_03_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_03_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_03_light.png
new file mode 100644
index 0000000..d2e7108
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_03_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_04_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_04_dark.png
new file mode 100644
index 0000000..d9f596b
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_04_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_04_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_04_light.png
new file mode 100644
index 0000000..4f32e1a
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_04_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_05_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_05_dark.png
new file mode 100644
index 0000000..c568e04
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_05_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_05_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_05_light.png
new file mode 100644
index 0000000..ed20dd9
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_05_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_06_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_06_dark.png
new file mode 100644
index 0000000..bbe39e7
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_06_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_06_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_06_light.png
new file mode 100644
index 0000000..1edc15f
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_06_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_07_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_07_dark.png
new file mode 100644
index 0000000..78aebaf
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_07_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_07_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_07_light.png
new file mode 100644
index 0000000..b5a6a4f
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_07_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_08_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_08_dark.png
new file mode 100644
index 0000000..44b91ce
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_08_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_08_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_08_light.png
new file mode 100644
index 0000000..85f66f9
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_08_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_09_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_09_dark.png
new file mode 100644
index 0000000..51ea34b
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_09_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_09_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_09_light.png
new file mode 100644
index 0000000..952de04
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_09_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_10_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_10_dark.png
new file mode 100644
index 0000000..8b1aa21
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_10_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_10_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_10_light.png
new file mode 100644
index 0000000..534bcc0
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_10_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_11_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_11_dark.png
new file mode 100644
index 0000000..1fffa01
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_11_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_11_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_11_light.png
new file mode 100644
index 0000000..0ff7e57
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_11_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_12_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_12_dark.png
new file mode 100644
index 0000000..06ac4dc
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_12_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_12_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_12_light.png
new file mode 100644
index 0000000..42a86f5
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_12_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_13_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_13_dark.png
new file mode 100644
index 0000000..0301090
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_13_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_13_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_13_light.png
new file mode 100644
index 0000000..4396f0e
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_13_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_14_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_14_dark.png
new file mode 100644
index 0000000..e19001b
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_14_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_14_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_14_light.png
new file mode 100644
index 0000000..2271581
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_14_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_15_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_15_dark.png
new file mode 100644
index 0000000..5e96208
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_15_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_15_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_15_light.png
new file mode 100644
index 0000000..0f69500
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_15_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_16_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_16_dark.png
new file mode 100644
index 0000000..07e1bd6
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_16_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_16_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_16_light.png
new file mode 100644
index 0000000..cde8f19
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_16_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_17_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_17_dark.png
new file mode 100644
index 0000000..b632e95
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_17_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_17_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_17_light.png
new file mode 100644
index 0000000..11d5d2e
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_17_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_18_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_18_dark.png
new file mode 100644
index 0000000..660d527
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_18_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_18_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_18_light.png
new file mode 100644
index 0000000..2761ae1
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_18_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_19_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_19_dark.png
new file mode 100644
index 0000000..0aa3f84
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_19_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_19_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_19_light.png
new file mode 100644
index 0000000..27d166f
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_19_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_20_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_20_dark.png
new file mode 100644
index 0000000..ebe527e
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_20_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_20_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_20_light.png
new file mode 100644
index 0000000..aeb2a8e
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_20_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_21_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_21_dark.png
new file mode 100644
index 0000000..7337af5
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_21_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_21_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_21_light.png
new file mode 100644
index 0000000..f3f31ef
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_21_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_22_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_22_dark.png
new file mode 100644
index 0000000..20d9f57
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_22_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_22_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_22_light.png
new file mode 100644
index 0000000..bf8eb77
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_22_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_23_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_23_dark.png
new file mode 100644
index 0000000..56a0e14
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_23_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_23_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_23_light.png
new file mode 100644
index 0000000..67425e1
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_23_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_24_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_24_dark.png
new file mode 100644
index 0000000..7c76e19
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_24_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_24_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_24_light.png
new file mode 100644
index 0000000..e02f1ed
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_24_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_25_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_25_dark.png
new file mode 100644
index 0000000..f5fdcdd
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_25_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_25_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_25_light.png
new file mode 100644
index 0000000..8ce9b819
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_25_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_26_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_26_dark.png
new file mode 100644
index 0000000..a29e443
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_26_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_26_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_26_light.png
new file mode 100644
index 0000000..349ca89
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_26_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_27_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_27_dark.png
new file mode 100644
index 0000000..0fc75d5
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_27_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_27_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_27_light.png
new file mode 100644
index 0000000..5cbd27c
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_27_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_28_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_28_dark.png
new file mode 100644
index 0000000..0ebb0ac
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_28_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_28_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_28_light.png
new file mode 100644
index 0000000..5b514aa
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_28_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_29_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_29_dark.png
new file mode 100644
index 0000000..8e7fe5c
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_29_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_29_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_29_light.png
new file mode 100644
index 0000000..efb2c10
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_29_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_30_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_30_dark.png
new file mode 100644
index 0000000..0db679e
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_30_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_30_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_30_light.png
new file mode 100644
index 0000000..51c6051
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_connecting_30_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_disabled_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_disabled_dark.png
new file mode 100644
index 0000000..fdb2121
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_disabled_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_disabled_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_disabled_light.png
new file mode 100644
index 0000000..9ce7e3a
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_disabled_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_disconnected_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_disconnected_dark.png
new file mode 100644
index 0000000..e8601ce
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_disconnected_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_disconnected_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_disconnected_light.png
new file mode 100644
index 0000000..34928d7
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_disconnected_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_grey.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_grey.png
new file mode 100644
index 0000000..792fd77
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_mr_button_grey.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_vol_type_speaker_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_vol_type_speaker_dark.png
new file mode 100755
index 0000000..f171a8c
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_vol_type_speaker_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_vol_type_speaker_group_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_vol_type_speaker_group_dark.png
new file mode 100755
index 0000000..c8cb6ca
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_vol_type_speaker_group_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_vol_type_speaker_group_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_vol_type_speaker_group_light.png
new file mode 100755
index 0000000..9c8863d
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_vol_type_speaker_group_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_vol_type_speaker_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_vol_type_speaker_light.png
new file mode 100755
index 0000000..9335038
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_vol_type_speaker_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_vol_type_tv_dark.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_vol_type_tv_dark.png
new file mode 100755
index 0000000..a6a4858
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_vol_type_tv_dark.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxhdpi/ic_vol_type_tv_light.png b/packages/MediaComponents/res/drawable-xxhdpi/ic_vol_type_tv_light.png
new file mode 100755
index 0000000..4ca6787
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxhdpi/ic_vol_type_tv_light.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_00.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_00.png
new file mode 100644
index 0000000..b2305d2
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_00.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_01.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_01.png
new file mode 100644
index 0000000..59395d4
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_01.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_02.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_02.png
new file mode 100644
index 0000000..70a7282
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_02.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_03.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_03.png
new file mode 100644
index 0000000..b3f0f53
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_03.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_04.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_04.png
new file mode 100644
index 0000000..66a80d9
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_04.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_05.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_05.png
new file mode 100644
index 0000000..8ec3939
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_05.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_06.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_06.png
new file mode 100644
index 0000000..0f02536
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_06.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_07.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_07.png
new file mode 100644
index 0000000..ba228f4
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_07.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_08.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_08.png
new file mode 100644
index 0000000..304277e
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_08.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_09.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_09.png
new file mode 100644
index 0000000..f865bfb
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_09.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_10.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_10.png
new file mode 100644
index 0000000..17c5d6b
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_10.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_11.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_11.png
new file mode 100644
index 0000000..a2f4ad5
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_11.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_12.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_12.png
new file mode 100644
index 0000000..c230648
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_12.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_13.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_13.png
new file mode 100644
index 0000000..b99324e
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_13.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_14.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_14.png
new file mode 100644
index 0000000..c8618f0
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_14.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_15.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_15.png
new file mode 100644
index 0000000..4a0d770
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_collapse_15.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_00.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_00.png
new file mode 100644
index 0000000..4a0d770
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_00.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_01.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_01.png
new file mode 100644
index 0000000..4db4e50
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_01.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_02.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_02.png
new file mode 100644
index 0000000..82b5f03
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_02.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_03.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_03.png
new file mode 100644
index 0000000..b05c758
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_03.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_04.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_04.png
new file mode 100644
index 0000000..fa5c7fa
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_04.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_05.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_05.png
new file mode 100644
index 0000000..2c287e4
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_05.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_06.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_06.png
new file mode 100644
index 0000000..eb7d0cf
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_06.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_07.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_07.png
new file mode 100644
index 0000000..95fa72b
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_07.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_08.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_08.png
new file mode 100644
index 0000000..5650eea
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_08.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_09.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_09.png
new file mode 100644
index 0000000..6f44355
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_09.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_10.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_10.png
new file mode 100644
index 0000000..4e877c3
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_10.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_11.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_11.png
new file mode 100644
index 0000000..7927f0a
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_11.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_12.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_12.png
new file mode 100644
index 0000000..71b19bb
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_12.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_13.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_13.png
new file mode 100644
index 0000000..bf5921e
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_13.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_14.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_14.png
new file mode 100644
index 0000000..14b76b1
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_14.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_15.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_15.png
new file mode 100644
index 0000000..b2305d2
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_group_expand_15.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable-xxxhdpi/ic_mr_button_grey.png b/packages/MediaComponents/res/drawable-xxxhdpi/ic_mr_button_grey.png
new file mode 100644
index 0000000..04a9525
--- /dev/null
+++ b/packages/MediaComponents/res/drawable-xxxhdpi/ic_mr_button_grey.png
Binary files differ
diff --git a/packages/MediaComponents/res/drawable/ic_arrow_back.xml b/packages/MediaComponents/res/drawable/ic_arrow_back.xml
new file mode 100644
index 0000000..5aba8c6
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/ic_arrow_back.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40dp"
+ android:height="40dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/packages/MediaComponents/res/drawable/ic_cast.xml b/packages/MediaComponents/res/drawable/ic_cast.xml
new file mode 100644
index 0000000..ac22a4b
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/ic_cast.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:pathData="M21,3L3,3c-1.1,0 -2,0.9 -2,2v3h2L3,5h18v14h-7v2h7c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM1,18v3h3c0,-1.66 -1.34,-3 -3,-3zM1,14v2c2.76,0 5,2.24 5,5h2c0,-3.87 -3.13,-7 -7,-7zM1,10v2c4.97,0 9,4.03 9,9h2c0,-6.08 -4.93,-11 -11,-11z"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/packages/MediaComponents/res/drawable/ic_chevron_left.xml b/packages/MediaComponents/res/drawable/ic_chevron_left.xml
new file mode 100644
index 0000000..8336d17
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/ic_chevron_left.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:pathData="M15.41,7.41L14,6l-6,6 6,6 1.41,-1.41L10.83,12z"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/packages/MediaComponents/res/drawable/ic_chevron_right.xml b/packages/MediaComponents/res/drawable/ic_chevron_right.xml
new file mode 100644
index 0000000..fb2ce09
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/ic_chevron_right.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/packages/MediaComponents/res/drawable/ic_forward_30.xml b/packages/MediaComponents/res/drawable/ic_forward_30.xml
new file mode 100644
index 0000000..7efdf16
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/ic_forward_30.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40dp"
+ android:height="40dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <group>
+ <clip-path android:pathData="M24,24H0V0h24v24z M 0,0" />
+ <path
+ android:pathData="M9.6 13.5h.4c.2 0 .4,-.1.5,-.2s.2,-.2.2,-.4v-.2s-.1,-.1,-.1,-.2,-.1,-.1,-.2,-.1h-.5s-.1.1,-.2.1,-.1.1,-.1.2v.2h-1c0,-.2 0,-.3.1,-.5s.2,-.3.3,-.4.3,-.2.4,-.2.4,-.1.5,-.1c.2 0 .4 0 .6.1s.3.1.5.2.2.2.3.4.1.3.1.5v.3s-.1.2,-.1.3,-.1.2,-.2.2,-.2.1,-.3.2c.2.1.4.2.5.4s.2.4.2.6c0 .2 0 .4,-.1.5s-.2.3,-.3.4,-.3.2,-.5.2,-.4.1,-.6.1c-.2 0,-.4 0,-.5,-.1s-.3,-.1,-.5,-.2,-.2,-.2,-.3,-.4,-.1,-.4,-.1,-.6h.8v.2s.1.1.1.2.1.1.2.1h.5s.1,-.1.2,-.1.1,-.1.1,-.2v-.5s-.1,-.1,-.1,-.2,-.1,-.1,-.2,-.1h-.6v-.7zm5.7.7c0 .3 0 .6,-.1.8l-.3.6s-.3.3,-.5.3,-.4.1,-.6.1,-.4 0,-.6,-.1,-.3,-.2,-.5,-.3,-.2,-.3,-.3,-.6,-.1,-.5,-.1,-.8v-.7c0,-.3 0,-.6.1,-.8l.3,-.6s.3,-.3.5,-.3.4,-.1.6,-.1.4 0 .6.1.3.2.5.3.2.3.3.6.1.5.1.8v.7zm-.9,-.8v-.5s-.1,-.2,-.1,-.3,-.1,-.1,-.2,-.2,-.2,-.1,-.3,-.1,-.2 0,-.3.1l-.2.2s-.1.2,-.1.3v2s.1.2.1.3.1.1.2.2.2.1.3.1.2 0 .3,-.1l.2,-.2s.1,-.2.1,-.3v-1.5zM4 13c0 4.4 3.6 8 8 8s8,-3.6 8,-8h-2c0 3.3,-2.7 6,-6 6s-6,-2.7,-6,-6 2.7,-6 6,-6v4l5,-5,-5,-5v4c-4.4 0,-8 3.6,-8 8z"
+ android:fillColor="#FFFFFF"/>
+ </group>
+</vector>
diff --git a/packages/MediaComponents/res/drawable/ic_fullscreen.xml b/packages/MediaComponents/res/drawable/ic_fullscreen.xml
new file mode 100644
index 0000000..4b4f6bc
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/ic_fullscreen.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:pathData="M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/packages/MediaComponents/res/drawable/ic_fullscreen_exit.xml b/packages/MediaComponents/res/drawable/ic_fullscreen_exit.xml
new file mode 100644
index 0000000..bc204e2
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/ic_fullscreen_exit.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:pathData="M5,16h3v3h2v-5L5,14v2zM8,8L5,8v2h5L10,5L8,5v3zM14,19h2v-3h3v-2h-5v5zM16,8L16,5h-2v5h5L19,8h-3z"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/packages/MediaComponents/res/drawable/ic_pause_circle_filled.xml b/packages/MediaComponents/res/drawable/ic_pause_circle_filled.xml
new file mode 100644
index 0000000..73be228
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/ic_pause_circle_filled.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40dp"
+ android:height="40dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,16L9,16L9,8h2v8zM15,16h-2L13,8h2v8z"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/packages/MediaComponents/res/drawable/ic_play_circle_filled.xml b/packages/MediaComponents/res/drawable/ic_play_circle_filled.xml
new file mode 100644
index 0000000..9d39def
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/ic_play_circle_filled.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40dp"
+ android:height="40dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,16.5v-9l6,4.5 -6,4.5z"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/packages/MediaComponents/res/drawable/ic_replay.xml b/packages/MediaComponents/res/drawable/ic_replay.xml
new file mode 100644
index 0000000..2bde120
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/ic_replay.xml
@@ -0,0 +1,4 @@
+<vector android:height="40dp" android:viewportHeight="48.0"
+ android:viewportWidth="48.0" android:width="40dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#FFFFFF" android:pathData="M24,10V2L14,12l10,10v-8c6.63,0 12,5.37 12,12s-5.37,12 -12,12 -12,-5.37 -12,-12H8c0,8.84 7.16,16 16,16s16,-7.16 16,-16 -7.16,-16 -16,-16z"/>
+</vector>
diff --git a/packages/MediaComponents/res/drawable/ic_rewind_10.xml b/packages/MediaComponents/res/drawable/ic_rewind_10.xml
new file mode 100644
index 0000000..ae586b4
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/ic_rewind_10.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40dp"
+ android:height="40dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <group>
+ <clip-path android:pathData="M0,0h24v24H0V0z M 0,0" />
+ <path
+ android:pathData="M12 5V1L7 6l5 5V7c3.3 0 6 2.7 6 6s-2.7 6,-6 6,-6,-2.7,-6,-6H4c0 4.4 3.6 8 8 8s8,-3.6 8,-8,-3.6,-8,-8,-8zm-1.1 11H10v-3.3L9 13v-.7l1.8,-.6h.1V16zm4.3,-1.8c0 .3 0 .6,-.1.8l-.3.6s-.3.3,-.5.3,-.4.1,-.6.1,-.4 0,-.6,-.1,-.3,-.2,-.5,-.3,-.2,-.3,-.3,-.6,-.1,-.5,-.1,-.8v-.7c0,-.3 0,-.6.1,-.8l.3,-.6s.3,-.3.5,-.3.4,-.1.6,-.1.4 0 .6.1c.2.1.3.2.5.3s.2.3.3.6.1.5.1.8v.7zm-.9,-.8v-.5s-.1,-.2,-.1,-.3,-.1,-.1,-.2,-.2,-.2,-.1,-.3,-.1,-.2 0,-.3.1l-.2.2s-.1.2,-.1.3v2s.1.2.1.3.1.1.2.2.2.1.3.1.2 0 .3,-.1l.2,-.2s.1,-.2.1,-.3v-1.5z"
+ android:fillColor="#FFFFFF"/>
+ </group>
+</vector>
diff --git a/packages/MediaComponents/res/drawable/ic_skip_next.xml b/packages/MediaComponents/res/drawable/ic_skip_next.xml
new file mode 100644
index 0000000..b1f2812
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/ic_skip_next.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40dp"
+ android:height="40dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/packages/MediaComponents/res/drawable/ic_skip_previous.xml b/packages/MediaComponents/res/drawable/ic_skip_previous.xml
new file mode 100644
index 0000000..81da314
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/ic_skip_previous.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40dp"
+ android:height="40dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:pathData="M6,6h2v12L6,18zM9.5,12l8.5,6L18,6z"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/packages/MediaComponents/res/drawable/mr_button_connected_dark.xml b/packages/MediaComponents/res/drawable/mr_button_connected_dark.xml
new file mode 100644
index 0000000..110ff13
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/mr_button_connected_dark.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<animation-list
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:oneshot="true">
+ <item android:drawable="@drawable/ic_mr_button_connected_00_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_01_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_02_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_03_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_04_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_05_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_06_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_07_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_08_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_09_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_10_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_11_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_12_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_13_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_14_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_15_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_16_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_17_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_18_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_19_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_20_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_21_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_22_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_23_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_24_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_25_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_26_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_27_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_28_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_29_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_30_dark" android:duration="42" />
+</animation-list>
diff --git a/packages/MediaComponents/res/drawable/mr_button_connected_light.xml b/packages/MediaComponents/res/drawable/mr_button_connected_light.xml
new file mode 100644
index 0000000..bcfc7fe
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/mr_button_connected_light.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<animation-list
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:oneshot="true">
+ <item android:drawable="@drawable/ic_mr_button_connected_00_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_01_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_02_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_03_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_04_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_05_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_06_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_07_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_08_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_09_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_10_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_11_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_12_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_13_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_14_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_15_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_16_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_17_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_18_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_19_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_20_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_21_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_22_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_23_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_24_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_25_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_26_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_27_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_28_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_29_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connected_30_light" android:duration="42" />
+</animation-list>
diff --git a/packages/MediaComponents/res/drawable/mr_button_connecting_dark.xml b/packages/MediaComponents/res/drawable/mr_button_connecting_dark.xml
new file mode 100644
index 0000000..55af7b3
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/mr_button_connecting_dark.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<animation-list
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:oneshot="false">
+ <item android:drawable="@drawable/ic_mr_button_connecting_00_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_01_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_02_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_03_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_04_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_05_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_06_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_07_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_08_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_09_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_10_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_11_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_12_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_13_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_14_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_15_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_16_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_17_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_18_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_19_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_20_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_21_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_22_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_23_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_24_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_25_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_26_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_27_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_28_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_29_dark" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_30_dark" android:duration="42" />
+</animation-list>
diff --git a/packages/MediaComponents/res/drawable/mr_button_connecting_light.xml b/packages/MediaComponents/res/drawable/mr_button_connecting_light.xml
new file mode 100644
index 0000000..93b4170
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/mr_button_connecting_light.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<animation-list
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:oneshot="false">
+ <item android:drawable="@drawable/ic_mr_button_connecting_00_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_01_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_02_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_03_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_04_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_05_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_06_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_07_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_08_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_09_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_10_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_11_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_12_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_13_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_14_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_15_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_16_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_17_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_18_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_19_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_20_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_21_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_22_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_23_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_24_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_25_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_26_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_27_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_28_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_29_light" android:duration="42" />
+ <item android:drawable="@drawable/ic_mr_button_connecting_30_light" android:duration="42" />
+</animation-list>
diff --git a/packages/MediaComponents/res/drawable/mr_button_dark.xml b/packages/MediaComponents/res/drawable/mr_button_dark.xml
new file mode 100644
index 0000000..8f1dfaa
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/mr_button_dark.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:state_enabled="true"
+ android:drawable="@drawable/mr_button_connected_dark" />
+ <item android:state_checkable="true" android:state_enabled="true"
+ android:drawable="@drawable/mr_button_connecting_dark" />
+ <item android:state_enabled="true"
+ android:drawable="@drawable/ic_mr_button_disconnected_dark" />
+ <item android:drawable="@drawable/ic_mr_button_disabled_dark" />
+</selector>
diff --git a/packages/MediaComponents/res/drawable/mr_button_light.xml b/packages/MediaComponents/res/drawable/mr_button_light.xml
new file mode 100644
index 0000000..1d3d84e
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/mr_button_light.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" android:state_enabled="true"
+ android:drawable="@drawable/mr_button_connected_light" />
+ <item android:state_checkable="true" android:state_enabled="true"
+ android:drawable="@drawable/mr_button_connecting_light" />
+ <item android:state_enabled="true"
+ android:drawable="@drawable/ic_mr_button_disconnected_light" />
+ <item android:drawable="@drawable/ic_mr_button_disabled_light" />
+</selector>
diff --git a/packages/MediaComponents/res/drawable/mr_dialog_close_dark.xml b/packages/MediaComponents/res/drawable/mr_dialog_close_dark.xml
new file mode 100644
index 0000000..288c8c7
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/mr_dialog_close_dark.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_dialog_close_dark" />
+</selector>
diff --git a/packages/MediaComponents/res/drawable/mr_dialog_close_light.xml b/packages/MediaComponents/res/drawable/mr_dialog_close_light.xml
new file mode 100644
index 0000000..cd50e0f
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/mr_dialog_close_light.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <bitmap
+ android:src="@drawable/ic_dialog_close_light"
+ android:alpha="0.87" />
+ </item>
+
+</selector>
diff --git a/packages/MediaComponents/res/drawable/mr_dialog_material_background_dark.xml b/packages/MediaComponents/res/drawable/mr_dialog_material_background_dark.xml
new file mode 100644
index 0000000..ebc7eca
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/mr_dialog_material_background_dark.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<!-- This is the copy of @drawable/abc_dialog_material_background_dark except for inset
+ which includes unnecessary padding. -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="2dp" />
+ <solid android:color="@color/background_floating_material_dark" />
+</shape>
\ No newline at end of file
diff --git a/packages/MediaComponents/res/drawable/mr_dialog_material_background_light.xml b/packages/MediaComponents/res/drawable/mr_dialog_material_background_light.xml
new file mode 100644
index 0000000..c1b235a
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/mr_dialog_material_background_light.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<!-- This is the copy of @drawable/abc_dialog_material_background_light except for inset
+ which includes unnecessary padding. -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="2dp" />
+ <solid android:color="@color/background_floating_material_light" />
+</shape>
\ No newline at end of file
diff --git a/packages/MediaComponents/res/drawable/mr_group_collapse.xml b/packages/MediaComponents/res/drawable/mr_group_collapse.xml
new file mode 100644
index 0000000..8f72bc8
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/mr_group_collapse.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
+ android:oneshot="true">
+ <item android:drawable="@drawable/ic_group_collapse_00" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_collapse_01" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_collapse_02" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_collapse_03" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_collapse_04" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_collapse_05" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_collapse_06" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_collapse_07" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_collapse_08" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_collapse_09" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_collapse_10" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_collapse_11" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_collapse_12" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_collapse_13" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_collapse_14" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_collapse_15" android:duration="13" />
+</animation-list>
diff --git a/packages/MediaComponents/res/drawable/mr_group_expand.xml b/packages/MediaComponents/res/drawable/mr_group_expand.xml
new file mode 100644
index 0000000..6b3fdb6
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/mr_group_expand.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
+ android:oneshot="true">
+ <item android:drawable="@drawable/ic_group_expand_00" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_expand_01" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_expand_02" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_expand_03" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_expand_04" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_expand_05" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_expand_06" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_expand_07" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_expand_08" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_expand_09" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_expand_10" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_expand_11" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_expand_12" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_expand_13" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_expand_14" android:duration="13" />
+ <item android:drawable="@drawable/ic_group_expand_15" android:duration="13" />
+</animation-list>
diff --git a/packages/MediaComponents/res/drawable/mr_media_pause_dark.xml b/packages/MediaComponents/res/drawable/mr_media_pause_dark.xml
new file mode 100644
index 0000000..86218a7
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/mr_media_pause_dark.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_media_pause_dark" />
+</selector>
diff --git a/packages/MediaComponents/res/drawable/mr_media_pause_light.xml b/packages/MediaComponents/res/drawable/mr_media_pause_light.xml
new file mode 100644
index 0000000..2dd1f02
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/mr_media_pause_light.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <bitmap
+ android:src="@drawable/ic_media_pause_light"
+ android:alpha="0.87" />
+ </item>
+</selector>
diff --git a/packages/MediaComponents/res/drawable/mr_media_play_dark.xml b/packages/MediaComponents/res/drawable/mr_media_play_dark.xml
new file mode 100644
index 0000000..9d45a33
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/mr_media_play_dark.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_media_play_dark" />
+</selector>
diff --git a/packages/MediaComponents/res/drawable/mr_media_play_light.xml b/packages/MediaComponents/res/drawable/mr_media_play_light.xml
new file mode 100644
index 0000000..f1fb7a6
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/mr_media_play_light.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <bitmap
+ android:src="@drawable/ic_media_play_light"
+ android:alpha="0.87" />
+ </item>
+</selector>
diff --git a/packages/MediaComponents/res/drawable/mr_media_stop_dark.xml b/packages/MediaComponents/res/drawable/mr_media_stop_dark.xml
new file mode 100644
index 0000000..3e108a9
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/mr_media_stop_dark.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_media_stop_dark" />
+</selector>
diff --git a/packages/MediaComponents/res/drawable/mr_media_stop_light.xml b/packages/MediaComponents/res/drawable/mr_media_stop_light.xml
new file mode 100644
index 0000000..b2c6ce8
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/mr_media_stop_light.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <bitmap
+ android:src="@drawable/ic_media_stop_light"
+ android:alpha="0.87" />
+ </item>
+</selector>
diff --git a/packages/MediaComponents/res/drawable/mr_vol_type_audiotrack_dark.xml b/packages/MediaComponents/res/drawable/mr_vol_type_audiotrack_dark.xml
new file mode 100644
index 0000000..44f4fd6
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/mr_vol_type_audiotrack_dark.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_audiotrack_dark" />
+</selector>
diff --git a/packages/MediaComponents/res/drawable/mr_vol_type_audiotrack_light.xml b/packages/MediaComponents/res/drawable/mr_vol_type_audiotrack_light.xml
new file mode 100644
index 0000000..5c9dbc0
--- /dev/null
+++ b/packages/MediaComponents/res/drawable/mr_vol_type_audiotrack_light.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <bitmap
+ android:src="@drawable/ic_audiotrack_light"
+ android:alpha="0.87" />
+ </item>
+</selector>
diff --git a/packages/MediaComponents/res/interpolator/mr_fast_out_slow_in.xml b/packages/MediaComponents/res/interpolator/mr_fast_out_slow_in.xml
new file mode 100644
index 0000000..6b6a171
--- /dev/null
+++ b/packages/MediaComponents/res/interpolator/mr_fast_out_slow_in.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2018 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
+ -->
+
+<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:controlX1="0.4"
+ android:controlY1="0"
+ android:controlX2="0.2"
+ android:controlY2="1"/>
diff --git a/packages/MediaComponents/res/interpolator/mr_linear_out_slow_in.xml b/packages/MediaComponents/res/interpolator/mr_linear_out_slow_in.xml
new file mode 100644
index 0000000..20bf298
--- /dev/null
+++ b/packages/MediaComponents/res/interpolator/mr_linear_out_slow_in.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2018 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
+ -->
+
+<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:controlX1="0"
+ android:controlY1="0"
+ android:controlX2="0.2"
+ android:controlY2="1"/>
diff --git a/packages/MediaComponents/res/layout/media_controller.xml b/packages/MediaComponents/res/layout/media_controller.xml
new file mode 100644
index 0000000..ff4f12a
--- /dev/null
+++ b/packages/MediaComponents/res/layout/media_controller.xml
@@ -0,0 +1,136 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="#55000000"
+ android:orientation="vertical"
+ android:layoutDirection="ltr">
+
+ <RelativeLayout
+ android:id="@+id/title_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <RadioButton
+ android:id="@+id/back"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_centerVertical="true"
+ android:checked="true"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/title_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_toRightOf="@id/back"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="15dp"
+ android:paddingTop="4dp"
+ android:paddingStart="4dp"
+ android:paddingEnd="4dp"
+ android:textSize="20sp"
+ android:text="North by Northwest"
+ android:textColor="#FFFFFFFF" />
+
+ <ImageButton
+ android:id="@+id/cast"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ style="@style/TitleBarButton.MediaRouteButton"/>
+
+ </RelativeLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:gravity="center"
+ android:paddingTop="4dp"
+ android:orientation="horizontal">
+
+ <ImageButton android:id="@+id/prev" style="@style/TransportControlsButton.Previous" />
+ <ImageButton android:id="@+id/rew" style="@style/TransportControlsButton.Rew" />
+ <ImageButton android:id="@+id/pause" style="@style/TransportControlsButton.Pause" />
+ <ImageButton android:id="@+id/ffwd" style="@style/TransportControlsButton.Ffwd" />
+ <ImageButton android:id="@+id/next" style="@style/TransportControlsButton.Next" />
+
+ </LinearLayout>
+
+ <SeekBar
+ android:id="@+id/mediacontroller_progress"
+ android:layout_width="match_parent"
+ android:layout_height="32dp"
+ android:padding="0dp"
+ android:progressTint="#FFFFFFFF"
+ android:thumbTint="#FFFFFFFF"/>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="15dp"
+ android:paddingRight="15dp"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/time_current"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_alignParentStart="true"
+ android:paddingEnd="4dp"
+ android:paddingStart="4dp"
+ android:textSize="14sp"
+ android:textStyle="bold"
+ android:textColor="#FFFFFF" />
+
+ <TextView
+ android:id="@+id/time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_toRightOf="@id/time_current"
+ android:paddingStart="4dp"
+ android:paddingEnd="4dp"
+ android:textSize="14sp"
+ android:textStyle="bold"
+ android:textColor="#BBBBBB" />
+
+ <ImageButton
+ android:id="@+id/overflow"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ style="@style/BottomBarButton.Overflow"/>
+
+ <ImageButton
+ android:id="@+id/fullscreen"
+ android:layout_toLeftOf="@id/overflow"
+ android:layout_centerVertical="true"
+ style="@style/BottomBarButton.FullScreen"/>
+
+ <ImageButton
+ android:id="@+id/cc"
+ android:scaleType="fitCenter"
+ android:layout_toLeftOf="@id/fullscreen"
+ android:layout_centerVertical="true"
+ style="@style/BottomBarButton.CC" />
+
+ </RelativeLayout>
+
+</LinearLayout>
diff --git a/packages/MediaComponents/res/layout/mr_chooser_dialog.xml b/packages/MediaComponents/res/layout/mr_chooser_dialog.xml
new file mode 100644
index 0000000..ee89e16
--- /dev/null
+++ b/packages/MediaComponents/res/layout/mr_chooser_dialog.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ <TextView android:id="@+id/mr_chooser_title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="24dp"
+ android:paddingRight="24dp"
+ android:paddingTop="24dp"
+ android:text="@string/mr_chooser_title"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:textAppearance="@style/TextAppearance.MediaRouter.Title" />
+ <ListView android:id="@+id/mr_chooser_list"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:divider="@android:color/transparent"
+ android:dividerHeight="0dp" />
+ <LinearLayout android:id="@android:id/empty"
+ android:layout_width="fill_parent"
+ android:layout_height="240dp"
+ android:orientation="vertical"
+ android:paddingTop="90dp"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:visibility="gone">
+ <TextView android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:text="@string/mr_chooser_searching"
+ android:textAppearance="@style/TextAppearance.MediaRouter.SecondaryText" />
+ <ProgressBar android:layout_width="150dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:indeterminate="true"
+ style="?android:attr/progressBarStyleHorizontal" />
+ </LinearLayout>
+</LinearLayout>
diff --git a/packages/MediaComponents/res/layout/mr_chooser_list_item.xml b/packages/MediaComponents/res/layout/mr_chooser_list_item.xml
new file mode 100644
index 0000000..958879b
--- /dev/null
+++ b/packages/MediaComponents/res/layout/mr_chooser_list_item.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="56dp"
+ android:paddingLeft="24dp"
+ android:paddingRight="24dp"
+ android:orientation="horizontal"
+ android:gravity="center_vertical" >
+
+ <ImageView android:id="@+id/mr_chooser_route_icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_marginRight="24dp" />
+
+ <LinearLayout android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="1dp"
+ android:orientation="vertical" >
+
+ <TextView android:id="@+id/mr_chooser_route_name"
+ android:layout_width="fill_parent"
+ android:layout_height="32dp"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:textAppearance="@style/TextAppearance.MediaRouter.PrimaryText" />
+
+ <TextView android:id="@+id/mr_chooser_route_desc"
+ android:layout_width="fill_parent"
+ android:layout_height="24dp"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:textAppearance="@style/TextAppearance.MediaRouter.SecondaryText" />
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/packages/MediaComponents/res/layout/mr_controller_material_dialog_b.xml b/packages/MediaComponents/res/layout/mr_controller_material_dialog_b.xml
new file mode 100644
index 0000000..3751002
--- /dev/null
+++ b/packages/MediaComponents/res/layout/mr_controller_material_dialog_b.xml
@@ -0,0 +1,206 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/mr_expandable_area"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+ <LinearLayout android:id="@+id/mr_dialog_area"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:orientation="vertical"
+ android:background="?attr/colorBackgroundFloating">
+ <LinearLayout android:id="@+id/mr_title_bar"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="24dp"
+ android:paddingRight="12dp"
+ android:orientation="horizontal" >
+ <TextView android:id="@+id/mr_name"
+ android:layout_width="0dp"
+ android:layout_height="72dp"
+ android:layout_weight="1"
+ android:gravity="center_vertical"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:textAppearance="@style/TextAppearance.MediaRouter.Title" />
+ <ImageButton android:id="@+id/mr_close"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_gravity="center_vertical"
+ android:contentDescription="@string/mr_controller_close_description"
+ android:src="?attr/mediaRouteCloseDrawable"
+ android:background="?attr/selectableItemBackgroundBorderless" />
+ </LinearLayout>
+ <FrameLayout android:id="@+id/mr_custom_control"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone" />
+ <FrameLayout android:id="@+id/mr_default_control"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+ <ImageView android:id="@+id/mr_art"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:adjustViewBounds="true"
+ android:scaleType="fitXY"
+ android:background="?attr/colorPrimary"
+ android:layout_gravity="top"
+ android:contentDescription="@string/mr_controller_album_art"
+ android:visibility="gone" />
+ <LinearLayout android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_gravity="bottom"
+ android:splitMotionEvents="false">
+ <LinearLayout android:id="@+id/mr_media_main_control"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp"
+ android:layout_gravity="bottom"
+ android:theme="?attr/mediaRouteControlPanelThemeOverlay">
+ <RelativeLayout
+ android:id="@+id/mr_playback_control"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingLeft="24dp"
+ android:paddingRight="12dp" >
+ <ImageButton android:id="@+id/mr_control_playback_ctrl"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="12dp"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:contentDescription="@string/mr_controller_play"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:visibility="gone" />
+ <LinearLayout android:id="@+id/mr_control_title_container"
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_toLeftOf="@id/mr_control_playback_ctrl"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true">
+ <TextView android:id="@+id/mr_control_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.MediaRouter.PrimaryText"
+ android:singleLine="true" />
+ <TextView android:id="@+id/mr_control_subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.MediaRouter.SecondaryText"
+ android:singleLine="true" />
+ </LinearLayout>
+ </RelativeLayout>
+ <View android:id="@+id/mr_control_divider"
+ android:layout_width="fill_parent"
+ android:layout_height="8dp"
+ android:visibility="gone" />
+ <LinearLayout
+ android:id="@+id/mr_volume_control"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="48dp"
+ android:gravity="center_vertical"
+ android:paddingLeft="24dp"
+ android:paddingRight="12dp"
+ android:splitMotionEvents="false">
+ <ImageView
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:src="?attr/mediaRouteAudioTrackDrawable"
+ android:gravity="center"
+ android:scaleType="center"/>
+ <!-- Since dialog's top layout mr_expandable_area is clickable, it propagates pressed state
+ to its non-clickable children. Specify android:clickable="true" to prevent volume slider
+ from having false pressed state. -->
+ <com.android.support.mediarouter.app.MediaRouteVolumeSlider
+ android:id="@+id/mr_volume_slider"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:minHeight="48dp"
+ android:maxHeight="48dp"
+ android:layout_weight="1"
+ android:clickable="true"
+ android:contentDescription="@string/mr_controller_volume_slider" />
+ <com.android.support.mediarouter.app.MediaRouteExpandCollapseButton
+ android:id="@+id/mr_group_expand_collapse"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:padding="12dp"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:visibility="gone"/>
+ </LinearLayout>
+ </LinearLayout>
+ <com.android.support.mediarouter.app.OverlayListView
+ android:id="@+id/mr_volume_group_list"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/mr_controller_volume_group_list_padding_top"
+ android:scrollbarStyle="outsideOverlay"
+ android:clipToPadding="false"
+ android:visibility="gone"
+ android:splitMotionEvents="false"
+ android:theme="?attr/mediaRouteControlPanelThemeOverlay" />
+ </LinearLayout>
+ </FrameLayout>
+ <ScrollView
+ android:id="@+id/buttonPanel"
+ style="?attr/buttonBarStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fillViewport="true"
+ android:scrollIndicators="top|bottom">
+ <android.support.v7.widget.ButtonBarLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="bottom"
+ android:layoutDirection="locale"
+ android:orientation="horizontal"
+ android:paddingBottom="4dp"
+ android:paddingLeft="12dp"
+ android:paddingRight="12dp"
+ android:paddingTop="4dp">
+ <Button
+ android:id="@android:id/button3"
+ style="?attr/buttonBarNeutralButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+ <android.support.v4.widget.Space
+ android:id="@+id/spacer"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:visibility="invisible"/>
+ <Button
+ android:id="@android:id/button2"
+ style="?android:attr/buttonBarNegativeButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+ <Button
+ android:id="@android:id/button1"
+ style="?attr/buttonBarPositiveButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+ </android.support.v7.widget.ButtonBarLayout>
+ </ScrollView>
+ </LinearLayout>
+</FrameLayout>
diff --git a/packages/MediaComponents/res/layout/mr_controller_volume_item.xml b/packages/MediaComponents/res/layout/mr_controller_volume_item.xml
new file mode 100644
index 0000000..a89058b
--- /dev/null
+++ b/packages/MediaComponents/res/layout/mr_controller_volume_item.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+ <LinearLayout android:id="@+id/volume_item_container"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/mr_controller_volume_group_list_item_height"
+ android:paddingLeft="24dp"
+ android:paddingRight="60dp"
+ android:paddingBottom="8dp"
+ android:orientation="vertical" >
+ <TextView android:id="@+id/mr_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.MediaRouter.SecondaryText"
+ android:singleLine="true" />
+ <LinearLayout android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+ <ImageView android:id="@+id/mr_volume_item_icon"
+ android:layout_width="@dimen/mr_controller_volume_group_list_item_icon_size"
+ android:layout_height="@dimen/mr_controller_volume_group_list_item_icon_size"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:scaleType="fitCenter"
+ android:src="?attr/mediaRouteAudioTrackDrawable" />
+ <android.support.v7.app.MediaRouteVolumeSlider
+ android:id="@+id/mr_volume_slider"
+ android:layout_width="fill_parent"
+ android:layout_height="40dp"
+ android:minHeight="40dp"
+ android:maxHeight="40dp"
+ android:contentDescription="@string/mr_controller_volume_slider" />
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout>
diff --git a/packages/MediaComponents/res/values-af/strings.xml b/packages/MediaComponents/res/values-af/strings.xml
new file mode 100644
index 0000000..47230ad
--- /dev/null
+++ b/packages/MediaComponents/res/values-af/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Stelsel"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Toestelle"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Cast-knoppie"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Uitsaai-knoppie. Ontkoppel"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Uitsaai-knoppie. Koppel tans"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Uitsaai-knoppie. Gekoppel"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Saai uit na"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Vind tans toestelle"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Ontkoppel"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Hou op uitsaai"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Maak toe"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Speel"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Laat wag"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Stop"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Vou uit"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Vou in"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Albumkunswerk"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Volumeglyer"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Geen media is gekies nie"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Geen inligting beskikbaar nie"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Saai tans skerm uit"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-am/strings.xml b/packages/MediaComponents/res/values-am/strings.xml
new file mode 100644
index 0000000..39a1903
--- /dev/null
+++ b/packages/MediaComponents/res/values-am/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"ስርዓት"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"መሣሪያዎች"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"የCast አዝራር"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Cast አዝራር። ግንኙነት ተቋርጧል"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Cast አዝራር በማገናኘት ላይ"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Cast አዝራር። ተገናኝቷል"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Cast አድርግ ወደ"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"መሣሪያዎችን በማግኘት ላይ"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"ግንኙነት አቋርጥ"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Cast ማድረግ አቁም"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"ዝጋ"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"አጫውት"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"ለአፍታ አቁም"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"አቁም"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"አስፋ"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"ሰብስብ"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"የአልበም ስነ-ጥበብ"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"ተንሸራታች የድምፅ መቆጣጠሪያ"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"ምንም ማህደረመረጃ አልተመረጠም"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"ምንም መረጃ አይገኝም"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"ማያ ገጽን በመውሰድ ላይ"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-ar/strings.xml b/packages/MediaComponents/res/values-ar/strings.xml
new file mode 100644
index 0000000..f8fb97d
--- /dev/null
+++ b/packages/MediaComponents/res/values-ar/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"النظام"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"الأجهزة"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"زر الإرسال"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"زر الإرسال. تم قطع الاتصال"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"زر الإرسال. جارٍ الاتصال"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"زر الإرسال. تم الاتصال"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"إرسال إلى"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"جارٍ البحث عن أجهزة"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"قطع الاتصال"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"إيقاف الإرسال"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"إغلاق"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"تشغيل"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"إيقاف مؤقت"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"إيقاف"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"توسيع"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"تصغير"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"صورة الألبوم"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"شريط تمرير مستوى الصوت"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"لم يتم اختيار أي وسائط"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"لا تتوفر أي معلومات"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"جارٍ إرسال الشاشة"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-az/strings.xml b/packages/MediaComponents/res/values-az/strings.xml
new file mode 100644
index 0000000..a3c60ab
--- /dev/null
+++ b/packages/MediaComponents/res/values-az/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistem"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Cihazlar"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Yayım düyməsi"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Yayım düyməsi. Bağlantı kəsildi"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Yayım düyməsi. Qoşulur"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Yayım düyməsi. Qoşuldu"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Bura yayımlayın"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Cihazlar axtarılır"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Bağlantını kəsin"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Yayımı dayandırın"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Qapadın"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Oynadın"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Durdurun"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Dayandırın"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Genişləndirin"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Yığcamlaşdırın"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Albom incəsənəti"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Səs hərmi diyircəyi"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Heç bir media seçilməyib"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Əlçatan məlumat yoxdur"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Ekran yayımlanır"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-b+sr+Latn/strings.xml b/packages/MediaComponents/res/values-b+sr+Latn/strings.xml
new file mode 100644
index 0000000..e25bd6e
--- /dev/null
+++ b/packages/MediaComponents/res/values-b+sr+Latn/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistem"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Uređaji"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Dugme Prebaci"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Dugme Prebaci. Veza je prekinuta"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Dugme Prebaci. Povezuje se"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Dugme Prebaci. Povezan je"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Prebacuj na"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Pronalaženje uređaja"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Prekini vezu"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Zaustavi prebacivanje"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Zatvori"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Pusti"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Pauziraj"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Zaustavi"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Proširi"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Skupi"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Omot albuma"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Klizač za jačinu zvuka"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Nema izabranih medija"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Nisu dostupne nikakve informacije"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Prebacuje se ekran"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-be/strings.xml b/packages/MediaComponents/res/values-be/strings.xml
new file mode 100644
index 0000000..ac391c1
--- /dev/null
+++ b/packages/MediaComponents/res/values-be/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Сістэма"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Прылады"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Кнопка трансляцыі"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Кнопка трансляцыі. Адключана"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Кнопка трансляцыі. Ідзе падключэнне"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Кнопка трансляцыі. Падключана"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Трансліраваць на"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Пошук прылад"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Адлучыць"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Спыніць трансляцыю"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Закрыць"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Прайграць"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Прыпыніць"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Спыніць"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Разгарнуць"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Згарнуць"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Вокладка альбома"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Паўзунок гучнасці"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Медыяфайл не выбраны"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Інфармацыя адсутнічае"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Экран трансляцыі"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-bg/strings.xml b/packages/MediaComponents/res/values-bg/strings.xml
new file mode 100644
index 0000000..76712d4
--- /dev/null
+++ b/packages/MediaComponents/res/values-bg/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Система"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Устройства"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Бутон за предаване"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Бутон за предаване. Връзката е прекратена"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Бутон за предаване. Свързва се"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Бутон за предаване. Установена е връзка"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Предаване към"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Търсят се устройства"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Прекратяване на връзката"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Спиране на предаването"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Затваряне"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Пускане"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Поставяне на пауза"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Спиране"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Разгъване"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Свиване"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Обложка на албума"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Плъзгач за силата на звука"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Няма избрана мултимедия"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Няма налична информация"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Екранът се предава"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-bn/strings.xml b/packages/MediaComponents/res/values-bn/strings.xml
new file mode 100644
index 0000000..1bf5932
--- /dev/null
+++ b/packages/MediaComponents/res/values-bn/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"সিস্টেম"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"ডিভাইসগুলি"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"কাস্ট করার বোতাম"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"কাস্ট করার বোতাম৷ সংযোগ বিচ্ছিন্ন হয়েছে"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"কাস্ট করার বোতাম৷ সংযোগ করা হচ্ছে"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"কাস্ট করার বোতাম৷ সংযুক্ত হয়েছে"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"এতে কাস্ট করুন"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"ডিভাইসগুলিকে খোঁজা হচ্ছে"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"সংযোগ বিচ্ছিন্ন করুন"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"কাস্ট করা বন্ধ করুন"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"বন্ধ করুন"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"চালান"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"বিরাম দিন"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"থামান"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"বড় করুন"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"সঙ্কুচিত করুন"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"অ্যালবাম শৈলি"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"ভলিউম স্লাইডার"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"কোনো মিডিয়া নির্বাচন করা হয়নি"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"কোনো তথ্য উপলব্ধ নেই"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"স্ক্রীন কাস্ট করা হচ্ছে"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-bs/strings.xml b/packages/MediaComponents/res/values-bs/strings.xml
new file mode 100644
index 0000000..711c742
--- /dev/null
+++ b/packages/MediaComponents/res/values-bs/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistem"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Uređaji"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Dugme za emitiranje"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Dugme za emitiranje. Veza je prekinuta"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Dugme za emitiranje. Povezivanje"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Dugme za emitiranje. Povezano"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Emitiranje na"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Traženje uređaja"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Prekini vezu"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Zaustavi prebacivanje"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Zatvori"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Reproduciraj"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Pauziraj"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Zaustavi"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Proširi"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Skupi"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Omot albuma"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Klizač za jačinu zvuka"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Nijedan medij nije odabran"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Nema dostupnih informacija"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Prebacuje se ekran"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-ca/strings.xml b/packages/MediaComponents/res/values-ca/strings.xml
new file mode 100644
index 0000000..bf85acf
--- /dev/null
+++ b/packages/MediaComponents/res/values-ca/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistema"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Dispositius"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Botó d\'emetre"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Botó Emet. Desconnectat."</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Botó Emet. S\'està connectant."</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Botó Emet. Connectat."</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Emet a"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"S\'estan cercant dispositius"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Desconnecta"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Atura l\'emissió"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Tanca"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Reprodueix"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Posa en pausa"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Atura"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Desplega"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Replega"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Imatge de l\'àlbum"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Control lliscant de volum"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"No s\'ha seleccionat cap fitxer multimèdia"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"No hi ha informació disponible"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Emissió de pantalla"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-cs/strings.xml b/packages/MediaComponents/res/values-cs/strings.xml
new file mode 100644
index 0000000..09a8920
--- /dev/null
+++ b/packages/MediaComponents/res/values-cs/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Systém"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Zařízení"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Tlačítko odesílání"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Tlačítko odesílání. Odpojeno"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Tlačítko odesílání. Připojování"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Tlačítko odesílání. Připojeno"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Odesílat do"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Hledání zařízení"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Odpojit"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Zastavit odesílání"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Zavřít"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Přehrát"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Pozastavit"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Zastavit"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Rozbalit"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Sbalit"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Obal alba"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Posuvník hlasitosti"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Nebyla vybrána žádná média"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Nejsou k dispozici žádné informace"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Odesílání obsahu obrazovky"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-da/strings.xml b/packages/MediaComponents/res/values-da/strings.xml
new file mode 100644
index 0000000..8e7a790
--- /dev/null
+++ b/packages/MediaComponents/res/values-da/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"System"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Enheder"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Cast-knap"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Cast-knap. Forbindelsen er afbrudt"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Cast-knap. Opretter forbindelse"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Cast-knap. Tilsluttet"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Cast til"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Finder enheder"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Afbryd"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Stop med at caste"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Luk"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Afspil"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Sæt på pause"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Stop"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Udvid"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Skjul"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Albumgrafik"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Lydstyrkeskyder"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Ingen medier er markeret"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Der er ingen tilgængelige oplysninger"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Skærmen castes"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-de/strings.xml b/packages/MediaComponents/res/values-de/strings.xml
new file mode 100644
index 0000000..26bf57c
--- /dev/null
+++ b/packages/MediaComponents/res/values-de/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"System"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Geräte"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Cast-Symbol"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Streaming-Schaltfläche. Nicht verbunden"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Streaming-Schaltfläche. Verbindung wird hergestellt"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Streaming-Schaltfläche. Verbunden"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Streamen auf"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Geräte werden gesucht."</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Verbindung trennen"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Streaming beenden"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Schließen"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Wiedergeben"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Pausieren"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Beenden"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Maximieren"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Minimieren"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Albumcover"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Schieberegler für die Lautstärke"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Keine Medien ausgewählt"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Keine Informationen verfügbar"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Bildschirm wird gestreamt."</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-el/strings.xml b/packages/MediaComponents/res/values-el/strings.xml
new file mode 100644
index 0000000..d82f69b
--- /dev/null
+++ b/packages/MediaComponents/res/values-el/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Σύστημα"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Συσκευές"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Κουμπί Cast"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Κουμπί μετάδοσης. Αποσυνδέθηκε"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Κουμπί μετάδοση. Σύνδεση σε εξέλιξη"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Κουμπί μετάδοσης. Συνδέθηκε"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Μετάδοση σε"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Εύρεση συσκευών"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Αποσύνδεση"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Διακοπή μετάδοσης"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Κλείσιμο"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Αναπαραγωγή"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Παύση"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Διακοπή"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Ανάπτυξη"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Σύμπτυξη"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Εξώφυλλο άλμπουμ"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Ρυθμιστικό έντασης ήχου"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Δεν έχουν επιλεγεί μέσα"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Δεν υπάρχουν διαθέσιμες πληροφορίες"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Μετάδοση οθόνης"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-en-rAU/strings.xml b/packages/MediaComponents/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000..dd3f219
--- /dev/null
+++ b/packages/MediaComponents/res/values-en-rAU/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"System"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Devices"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Cast button"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Cast button. Disconnected"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Cast button. Connecting"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Cast button. Connected"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Cast to"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Finding devices"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Disconnect"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Stop casting"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Close"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Play"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Pause"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Stop"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Expand"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Collapse"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Album art"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Volume slider"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"No media selected"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"No info available"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Casting screen"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-en-rCA/strings.xml b/packages/MediaComponents/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000..dd3f219
--- /dev/null
+++ b/packages/MediaComponents/res/values-en-rCA/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"System"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Devices"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Cast button"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Cast button. Disconnected"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Cast button. Connecting"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Cast button. Connected"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Cast to"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Finding devices"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Disconnect"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Stop casting"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Close"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Play"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Pause"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Stop"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Expand"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Collapse"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Album art"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Volume slider"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"No media selected"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"No info available"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Casting screen"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-en-rGB/strings.xml b/packages/MediaComponents/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..dd3f219
--- /dev/null
+++ b/packages/MediaComponents/res/values-en-rGB/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"System"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Devices"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Cast button"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Cast button. Disconnected"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Cast button. Connecting"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Cast button. Connected"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Cast to"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Finding devices"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Disconnect"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Stop casting"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Close"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Play"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Pause"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Stop"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Expand"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Collapse"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Album art"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Volume slider"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"No media selected"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"No info available"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Casting screen"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-en-rIN/strings.xml b/packages/MediaComponents/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..dd3f219
--- /dev/null
+++ b/packages/MediaComponents/res/values-en-rIN/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"System"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Devices"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Cast button"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Cast button. Disconnected"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Cast button. Connecting"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Cast button. Connected"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Cast to"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Finding devices"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Disconnect"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Stop casting"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Close"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Play"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Pause"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Stop"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Expand"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Collapse"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Album art"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Volume slider"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"No media selected"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"No info available"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Casting screen"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-en-rXC/strings.xml b/packages/MediaComponents/res/values-en-rXC/strings.xml
new file mode 100644
index 0000000..a87007e
--- /dev/null
+++ b/packages/MediaComponents/res/values-en-rXC/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"System"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Devices"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Cast button"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Cast button. Disconnected"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Cast button. Connecting"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Cast button. Connected"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Cast to"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Finding devices"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Disconnect"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Stop casting"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Close"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Play"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Pause"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Stop"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Expand"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Collapse"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Album art"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Volume slider"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"No media selected"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"No info available"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Casting screen"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-es-rUS/strings.xml b/packages/MediaComponents/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..441ead1
--- /dev/null
+++ b/packages/MediaComponents/res/values-es-rUS/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistema"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Dispositivos"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Botón para transmitir"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Botón para transmitir (desconectado)"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Botón para transmitir (conectando)"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Botón para transmitir (conectado)"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Transmitir a"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Buscando dispositivos"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Desconectar"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Detener la transmisión"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Cerrar"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Reproducir"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Pausar"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Detener"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Mostrar"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Ocultar"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Imagen del álbum"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Control deslizante del volumen"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"No se seleccionó ningún contenido multimedia"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Sin información disponible"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Transmitiendo pantalla"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-es/strings.xml b/packages/MediaComponents/res/values-es/strings.xml
new file mode 100644
index 0000000..ff43008
--- /dev/null
+++ b/packages/MediaComponents/res/values-es/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistema"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Dispositivos"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Botón de enviar"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Botón de enviar. Desconectado"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Botón de enviar. Conectando"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Botón de enviar. Conectado"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Enviar a"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Buscando dispositivos"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Desconectar"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Detener envío de contenido"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Cerrar"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Reproducir"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Pausa"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Detener"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Mostrar"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Ocultar"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Portada del álbum"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Control deslizante de volumen"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"No se ha seleccionado ningún medio"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"No hay información disponible"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Enviando pantalla"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-et/strings.xml b/packages/MediaComponents/res/values-et/strings.xml
new file mode 100644
index 0000000..453235b
--- /dev/null
+++ b/packages/MediaComponents/res/values-et/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Süsteem"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Seadmed"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Ülekandenupp"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Ülekandenupp. Ühendus on katkestatud"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Ülekandenupp. Ühendamine"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Ülekandenupp. Ühendatud"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Ülekandmine seadmesse"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Seadmete otsimine"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Katkesta ühendus"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Peata ülekandmine"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Sulgemine"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Esitamine"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Peatamine"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Peata"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Laiendamine"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Ahendamine"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Albumi kujundus"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Helitugevuse liugur"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Meediat pole valitud"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Teave puudub"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Ekraanikuva ülekandmine"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-eu/strings.xml b/packages/MediaComponents/res/values-eu/strings.xml
new file mode 100644
index 0000000..dba19e4
--- /dev/null
+++ b/packages/MediaComponents/res/values-eu/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistema"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Gailuak"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Igorri botoia"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Igortzeko botoia. Deskonektatuta"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Igortzeko botoia. Konektatzen"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Igortzeko botoia. Konektatuta"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Igorri hona:"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Gailuak bilatzen"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Deskonektatu"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Utzi igortzeari"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Itxi"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Erreproduzitu"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Pausatu"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Gelditu"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Zabaldu"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Tolestu"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Albumaren azala"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Bolumenaren graduatzailea"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Ez da hautatu multimedia-edukirik"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Ez dago informaziorik"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Pantaila igortzen"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-fa/strings.xml b/packages/MediaComponents/res/values-fa/strings.xml
new file mode 100644
index 0000000..4c6c779
--- /dev/null
+++ b/packages/MediaComponents/res/values-fa/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"سیستم"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"دستگاهها"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"دکمه ارسال محتوا"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"دکمه فرستادن. ارتباط قطع شد"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"دکمه فرستادن. درحال مرتبطسازی"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"دکمه فرستادن. مرتبط شد"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"ارسال محتوا به"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"پیدا کردن دستگاهها"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"قطع ارتباط"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"توقف ارسال محتوا"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"بستن"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"پخش"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"مکث"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"توقف"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"بزرگ کردن"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"کوچک کردن"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"عکس روی جلد آلبوم"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"لغزنده میزان صدا"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"رسانه انتخاب نشده است"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"اطلاعات در دسترس نیست"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"درحال فرستادن صفحه"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-fi/strings.xml b/packages/MediaComponents/res/values-fi/strings.xml
new file mode 100644
index 0000000..d683435
--- /dev/null
+++ b/packages/MediaComponents/res/values-fi/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Järjestelmä"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Laitteet"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Cast-painike"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Cast-painike. Yhteys katkaistu"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Cast-painike. Yhdistetään"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Cast-painike. Yhdistetty"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Suoratoiston kohde"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Etsitään laitteita"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Katkaise yhteys"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Lopeta suoratoisto"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Sulje"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Toista"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Keskeytä"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Pysäytä"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Laajenna"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Tiivistä"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Albumin kansikuva"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Äänenvoimakkuuden liukusäädin"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Ei valittua mediaa."</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Tietoja ei ole saatavilla"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Suoratoistetaan näyttöä"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-fr-rCA/strings.xml b/packages/MediaComponents/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..c4f984b
--- /dev/null
+++ b/packages/MediaComponents/res/values-fr-rCA/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Système"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Appareils"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Bouton Diffuser"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Bouton Diffuser. Déconnecté"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Bouton Diffuser. Connexion en cours…"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Bouton Diffuser. Connecté"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Diffuser sur"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Recherche d\'appareils"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Se déconnecter"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Arrêter la diffusion"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Fermer"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Lire"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Interrompre"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Arrêter"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Développer"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Réduire"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Image de l\'album"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Curseur de réglage du volume"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Aucun média sélectionné"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Aucune information disponible"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Diffusion de l\'écran en cours"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-fr/strings.xml b/packages/MediaComponents/res/values-fr/strings.xml
new file mode 100644
index 0000000..12c312f
--- /dev/null
+++ b/packages/MediaComponents/res/values-fr/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Système"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Appareils"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Icône Cast"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Icône Cast. Déconnecté"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Icône Cast. Connexion…"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Icône Cast. Connecté"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Caster sur"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Recherche d\'appareils…"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Déconnecter"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Arrêter la diffusion"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Fermer"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Lecture"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Pause"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Arrêter"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Développer"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Réduire"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Image de l\'album"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Curseur de volume"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Aucun média sélectionné"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Aucune information disponible"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Diffusion de l\'écran en cours…"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-gl/strings.xml b/packages/MediaComponents/res/values-gl/strings.xml
new file mode 100644
index 0000000..1b2c354
--- /dev/null
+++ b/packages/MediaComponents/res/values-gl/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistema"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Dispositivos"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Botón de emitir"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Botón de emitir. Desconectado"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Botón de emitir. Conectando"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Botón de emitir. Conectado"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Emitir a"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Buscando dispositivos"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Desconectar"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Deter emisión"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Pechar"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Reproduce"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Pausa"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Deter"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Ampliar"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Contraer"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Portada do álbum"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Control desprazable do volume"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Non se seleccionaron recursos"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Non hai información dispoñible"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Emisión de pantalla"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-gu/strings.xml b/packages/MediaComponents/res/values-gu/strings.xml
new file mode 100644
index 0000000..2cd5f3f
--- /dev/null
+++ b/packages/MediaComponents/res/values-gu/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"સિસ્ટમ"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"ઉપકરણો"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"કાસ્ટ કરો બટન"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"કાસ્ટ કરો બટન. ડિસ્કનેક્ટ કર્યું"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"કાસ્ટ કરો બટન. કનેક્ટ થઈ રહ્યું છે"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"કાસ્ટ કરો બટન. કનેક્ટ થયું"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"આના પર કાસ્ટ કરો"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"ઉપકરણો શોધી રહ્યાં છીએ"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"ડિસ્કનેક્ટ કરો"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"કાસ્ટ કરવાનું રોકો"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"બંધ કરો"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"ચલાવો"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"થોભાવો"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"રોકો"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"વિસ્તૃત કરો"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"સંકુચિત કરો"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"આલ્બમ કલા"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"વૉલ્યુમ સ્લાઇડર"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"કોઈ મીડિયા પસંદ કરેલ નથી"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"કોઈ માહિતી ઉપલબ્ધ નથી"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"સ્ક્રીનને કાસ્ટ કરી રહ્યાં છે"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-hi/strings.xml b/packages/MediaComponents/res/values-hi/strings.xml
new file mode 100644
index 0000000..9552a59
--- /dev/null
+++ b/packages/MediaComponents/res/values-hi/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"सिस्टम"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"डिवाइस"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"कास्ट करें बटन"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"कास्ट करें बटन. डिसकनेक्ट है"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"कास्ट करें बटन. कनेक्ट हो रहा है"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"कास्ट करें बटन. कनेक्ट है"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"इस पर कास्ट करें"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"डिवाइस ढूंढ रहा है"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"डिसकनेक्ट करें"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"कास्ट करना बंद करें"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"बंद करें"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"चलाएं"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"रोकें"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"बंद करें"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"विस्तार करें"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"छोटा करें"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"एल्बम आर्ट"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"वॉल्यूम स्लाइडर"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"कोई मीडिया चयनित नहीं है"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"कोई जानकारी मौजूद नहीं है"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"स्क्रीन कास्ट हो रही है"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-hr/strings.xml b/packages/MediaComponents/res/values-hr/strings.xml
new file mode 100644
index 0000000..3c43ee7
--- /dev/null
+++ b/packages/MediaComponents/res/values-hr/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sustav"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Uređaji"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Gumb za emitiranje"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Gumb za emitiranje. Veza prekinuta"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Gumb za emitiranje. Povezivanje"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Gumb za emitiranje. Povezan"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Emitiranje na"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Traženje uređaja"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Prekini vezu"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Zaustavi emitiranje"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Zatvaranje"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Reprodukcija"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Pauziranje"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Zaustavi"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Proširivanje"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Sažimanje"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Naslovnica albuma"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Klizač za glasnoću"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Nije odabran nijedan medij"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Informacije nisu dostupne"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Emitiranje zaslona"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-hu/strings.xml b/packages/MediaComponents/res/values-hu/strings.xml
new file mode 100644
index 0000000..a36bdfe
--- /dev/null
+++ b/packages/MediaComponents/res/values-hu/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Rendszer"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Eszközök"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Átküldés gomb"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Átküldés gomb. Kapcsolat bontva"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Átküldés gomb. Csatlakozás"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Átküldés gomb. Csatlakoztatva"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Átküldés ide"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Eszközök keresése"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Leválasztás"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Átküldés leállítása"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Bezárás"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Lejátszás"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Szüneteltetés"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Leállítás"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Kibontás"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Összecsukás"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Lemezborító"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Hangerőszabályzó"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Nincs média kiválasztva"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Nincs információ"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Képernyőtartalom átküldése"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-hy/strings.xml b/packages/MediaComponents/res/values-hy/strings.xml
new file mode 100644
index 0000000..8ec82b7
--- /dev/null
+++ b/packages/MediaComponents/res/values-hy/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Համակարգ"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Սարքեր"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Հեռարձակման կոճակ"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Հեռարձակման կոճակ: Սարքն անջատված է"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Հեռարձակման կոճակ: Սարքը կապակցվում է"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Հեռարձակման կոճակ: Սարքը կապակցված է"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Ընտրեք սարքը"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Սարքերի որոնում"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Անջատել"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Դադարեցնել հեռարձակումը"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Փակել"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Նվագարկել"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Դադար"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Դադարեցնել"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Ընդարձակել"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Կոծկել"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Ալբոմի շապիկ"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Ձայնի ուժգնության կարգավորիչ"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Մեդիա ֆայլեր չեն ընտրվել"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Տեղեկությունները հասանելի չեն"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Էկրանը հեռարձակվում է"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-in/strings.xml b/packages/MediaComponents/res/values-in/strings.xml
new file mode 100644
index 0000000..6b2752e
--- /dev/null
+++ b/packages/MediaComponents/res/values-in/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistem"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Perangkat"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Tombol Cast"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Tombol Cast. Terputus"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Tombol Cast. Menghubungkan"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Tombol Cast. Terhubung"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Transmisikan ke"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Mencari perangkat"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Putuskan sambungan"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Hentikan cast"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Tutup"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Putar"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Jeda"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Berhenti"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Luaskan"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Ciutkan"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Sampul album"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Bilah geser volume"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Tidak ada media yang dipilih"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Tidak ada info yang tersedia"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Transmisi layar"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-is/strings.xml b/packages/MediaComponents/res/values-is/strings.xml
new file mode 100644
index 0000000..6a35ea6
--- /dev/null
+++ b/packages/MediaComponents/res/values-is/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Kerfi"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Tæki"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Útsendingarhnappur"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Útsendingarhnappur. Aftengt"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Útsendingarhnappur. Tengist"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Útsendingarhnappur. Tengt"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Senda út í"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Leitað að tækjum"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Aftengjast"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Stöðva útsendingu"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Loka"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Spila"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Hlé"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Stöðva"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Stækka"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Minnka"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Plötuumslag"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Hljóðstyrkssleði"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Enginn miðill valinn"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Engar upplýsingar í boði"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Skjár sendur út"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-it/strings.xml b/packages/MediaComponents/res/values-it/strings.xml
new file mode 100644
index 0000000..716e3ac
--- /dev/null
+++ b/packages/MediaComponents/res/values-it/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistema"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Dispositivi"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Pulsante Trasmetti"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Pulsante Trasmetti. Disconnesso"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Pulsante Trasmetti. Connessione in corso"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Pulsante Trasmetti. Connesso"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Trasmetti a"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Ricerca di dispositivi in corso"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Scollega"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Interrompi trasmissione"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Chiudi"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Riproduci"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Pausa"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Interrompi"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Espandi"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Comprimi"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Copertina"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Dispositivo di scorrimento del volume"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Nessun contenuto multimediale selezionato"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Nessuna informazione disponibile"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Trasmissione dello schermo in corso"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-iw/strings.xml b/packages/MediaComponents/res/values-iw/strings.xml
new file mode 100644
index 0000000..252b0ce
--- /dev/null
+++ b/packages/MediaComponents/res/values-iw/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"מערכת"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"מכשירים"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"לחצן הפעלת Cast"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"לחצן הפעלת Cast. מנותק"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"לחצן הפעלת Cast. מתחבר"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"לחצן הפעלת Cast. מחובר"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"העברה אל"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"מחפש מכשירים"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"נתק"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"הפסק את ההעברה"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"סגור"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"הפעל"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"השהה"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"הפסק"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"הרחב"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"כווץ"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"עטיפת אלבום"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"מחוון עוצמה"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"לא נבחרה מדיה"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"אין מידע זמין"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"העברת מסך מתבצעת"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-ja/strings.xml b/packages/MediaComponents/res/values-ja/strings.xml
new file mode 100644
index 0000000..a149727
--- /dev/null
+++ b/packages/MediaComponents/res/values-ja/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"システム"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"端末"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"キャストアイコン"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"キャスト アイコン。接続解除済み"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"キャスト アイコン。接続中"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"キャスト アイコン。接続済み"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"キャストするデバイス"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"端末を検索しています"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"接続を解除"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"キャストを停止"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"閉じる"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"再生"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"一時停止"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"停止"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"展開"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"折りたたむ"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"アルバムアート"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"音量スライダー"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"メディアが選択されていません"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"情報がありません"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"画面をキャストしています"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-ka/strings.xml b/packages/MediaComponents/res/values-ka/strings.xml
new file mode 100644
index 0000000..3da081a
--- /dev/null
+++ b/packages/MediaComponents/res/values-ka/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"სისტემა"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"მოწყობილობები"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"ტრანსლირების ღილაკი"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"ტრანსლირების ღილაკი. გათიშული"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"ტრანსლირების ღილაკი. მიმდინარეობს დაკავშირება"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"ტრანსლირების ღილაკი. დაკავშირებული"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"ტრანსლირება:"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"მოწყობილობების მოძიება..."</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"კავშირის გაწყვეტა"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"ტრანსლირების შეწყვეტა"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"დახურვა"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"დაკვრა"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"პაუზა"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"შეწყვეტა"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"გაშლა"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"ჩაკეცვა"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"ალბომის გარეკანი"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"ხმის სლაიდერი"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"მედია არჩეული არ არის"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"ინფორმაცია არ არის ხელმისაწვდომი"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"მიმდინარეობს ეკრანის გადაცემა"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-kk/strings.xml b/packages/MediaComponents/res/values-kk/strings.xml
new file mode 100644
index 0000000..94dcbb3
--- /dev/null
+++ b/packages/MediaComponents/res/values-kk/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Жүйе"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Құрылғылар"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Трансляциялау түймесі"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"\"Трансляциялау\" түймесі. Ажыратулы"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"\"Трансляциялау\" түймесі. Қосылуда"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"\"Трансляциялау\" түймесі. Қосылды"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Келесіге трансляциялау"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Құрылғыларды табу"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Ажырату"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Трансляциялауды тоқтату"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Жабу"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Ойнату"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Кідірту"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Тоқтату"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Жаю"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Жию"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Альбом шебері"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Дыбыс деңгейінің жүгірткісі"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Ешбір тасушы таңдалмаған"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Қол жетімді ақпарат жоқ"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Экранды трансляциялау"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-km/strings.xml b/packages/MediaComponents/res/values-km/strings.xml
new file mode 100644
index 0000000..e44780e
--- /dev/null
+++ b/packages/MediaComponents/res/values-km/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"ប្រព័ន្ធ"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"ឧបករណ៍"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"ប៊ូតុងខាស"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"ខាសប៊ូតុង៖ បានកាត់ផ្តាច់"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"ខាសប៊ូតុង៖ កំពុងភ្ជាប់"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"ខាសប៊ូតុង៖ បានភ្ជាប់ហើយ"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"បញ្ជូនទៅ"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"កំពុងស្វែងរកឧបករណ៍"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"ផ្ដាច់"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"ឈប់ភ្ជាប់"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"បិទ"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"ចាក់"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"ផ្អាក"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"ឈប់"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"ពង្រីក"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"បង្រួម"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"ស្នាដៃសិល្បៈអាល់ប៊ុម"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"របារកម្រិតសំឡេង"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"គ្មានការជ្រើសមេឌៀទេ"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"មិនមានព័ត៌មានទេ"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"កំពុងខាសអេក្រង់"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-kn/strings.xml b/packages/MediaComponents/res/values-kn/strings.xml
new file mode 100644
index 0000000..4237fdd
--- /dev/null
+++ b/packages/MediaComponents/res/values-kn/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"ಸಿಸ್ಟಂ"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"ಸಾಧನಗಳು"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"ಬಿತ್ತರಿಸು ಬಟನ್"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"ಬಿತ್ತರಿಸು ಬಟನ್. ಸಂಪರ್ಕ ಕಡಿತಗೊಳಿಸಲಾಗಿದೆ"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"ಬಿತ್ತರಿಸು ಬಟನ್. ಸಂಪರ್ಕಿಸಲಾಗುತ್ತಿದೆ"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"ಬಿತ್ತರಿಸು ಬಟನ್. ಸಂಪರ್ಕಿತಗೊಂಡಿದೆ"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"ಇದಕ್ಕೆ ಬಿತ್ತರಿಸಿ"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"ಸಾಧನಗಳನ್ನು ಹುಡುಕಲಾಗುತ್ತಿದೆ"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"ಸಂಪರ್ಕ ಕಡಿತಗೊಳಿಸು"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"ಬಿತ್ತರಿಸುವಿಕೆ ನಿಲ್ಲಿಸಿ"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"ಮುಚ್ಚು"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"ಪ್ಲೇ ಮಾಡಿ"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"ವಿರಾಮ"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"ನಿಲ್ಲಿಸಿ"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"ವಿಸ್ತರಿಸು"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"ಸಂಕುಚಿಸು"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"ಆಲ್ಬಮ್ ಕಲೆ"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"ವಾಲ್ಯೂಮ್ ಸ್ಲೈಡರ್"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"ಯಾವುದೇ ಮಾಧ್ಯಮ ಆಯ್ಕೆಮಾಡಲಾಗಿಲ್ಲ"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"ಯಾವುದೇ ಮಾಹಿತಿ ಲಭ್ಯವಿಲ್ಲ"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"ಪರದೆಯನ್ನು ಬಿತ್ತರಿಸಲಾಗುತ್ತಿದೆ"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-ko/strings.xml b/packages/MediaComponents/res/values-ko/strings.xml
new file mode 100644
index 0000000..be893a9
--- /dev/null
+++ b/packages/MediaComponents/res/values-ko/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"시스템"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"기기"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"전송 버튼"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"전송 버튼. 연결 해제됨"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"전송 버튼. 연결 중"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"전송 버튼. 연결됨"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"전송할 기기"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"기기를 찾는 중"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"연결 해제"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"전송 중지"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"닫기"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"재생"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"일시중지"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"중지"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"펼치기"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"접기"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"앨범아트"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"볼륨 슬라이더"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"선택한 미디어 없음"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"정보가 없습니다."</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"화면 전송 중"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-ky/strings.xml b/packages/MediaComponents/res/values-ky/strings.xml
new file mode 100644
index 0000000..57813af
--- /dev/null
+++ b/packages/MediaComponents/res/values-ky/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Тутум"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Түзмөктөр"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Тышкы экранга чыгаруу баскычы"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Тышкы экранга чыгаруу баскычы. Түзмөк ажырап турат."</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Тышкы экранга чыгаруу баскычы. Түзмөк туташууда"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Тышкы экранга чыгаруу баскычы. Түзмөк туташып турат"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Төмөнкүгө чыгаруу"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Түзмөктөр изделүүдө"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Ажыратуу"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Тышк экранга чыгарну токтотуу"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Жабуу"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Ойнотуу"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Тындыруу"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Токтотуу"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Жайып көрсөтүү"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Жыйыштыруу"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Альбом мукабасы"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Үндү катуулатуучу сыдырма"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Бир да медиа файл тандалган жок"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Эч маалымат жок"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Тышкы экранга чыгарылууда"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-land/dimens.xml b/packages/MediaComponents/res/values-land/dimens.xml
new file mode 100644
index 0000000..29f1e1d
--- /dev/null
+++ b/packages/MediaComponents/res/values-land/dimens.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<resources>
+ <!-- MediaRouteController's volume group list -->
+ <eat-comment />
+ <!-- Maximum height of volume group list. -->
+ <dimen name="mr_controller_volume_group_list_max_height">132dp</dimen>
+ <!-- Height of volume group item. -->
+ <dimen name="mr_controller_volume_group_list_item_height">61dp</dimen>
+ <!-- Size of an item's icon. -->
+ <dimen name="mr_controller_volume_group_list_item_icon_size">18dp</dimen>
+</resources>
diff --git a/packages/MediaComponents/res/values-lo/strings.xml b/packages/MediaComponents/res/values-lo/strings.xml
new file mode 100644
index 0000000..91737db
--- /dev/null
+++ b/packages/MediaComponents/res/values-lo/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"ລະບົບ"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"ອຸປະກອນ"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"ປຸ່ມຄາສທ໌"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"ປຸ່ມສົ່ງສັນຍານ. ຕັດການເຊື່ອມຕໍ່ແລ້ວ"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"ປຸ່ມສົ່ງສັນຍານ. ກຳລັງເຊື່ອມຕໍ່"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"ປຸ່ມສົ່ງສັນຍານ. ເຊື່ອມຕໍ່ແລ້ວ"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"ສົ່ງສັນຍານຫາ"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"ກຳລັງຊອກຫາອຸປະກອນ"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"ຕັດການເຊື່ອມຕໍ່"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"ຢຸດການສົ່ງສັນຍານ"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"ປິດ"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"ຫຼິ້ນ"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"ຢຸດຊົ່ວຄາວ"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"ຢຸດ"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"ຂະຫຍາຍ"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"ຫຍໍ້ລົງ"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"ໜ້າປົກອະລະບໍ້າ"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"ຕົວປັບລະດັບສຽງ"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"ບໍ່ໄດ້ເລືອກມີເດຍໃດໄວ້"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"ບໍ່ມີຂໍ້ມູນ"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"ການສົ່ງພາບໜ້າຈໍ"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-lt/strings.xml b/packages/MediaComponents/res/values-lt/strings.xml
new file mode 100644
index 0000000..ff036d1
--- /dev/null
+++ b/packages/MediaComponents/res/values-lt/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistema"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Įrenginiai"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Perdavimo mygtukas"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Perdavimo mygtukas. Atsijungta"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Perdavimo mygtukas. Prisijungiama"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Perdavimo mygtukas. Prisijungta"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Perduoti į"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Randami įrenginiai"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Atjungti"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Sustabdyti perdavimą"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Uždaryti"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Leisti"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Pristabdyti"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Sustabdyti"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Išskleisti"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Sutraukti"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Albumo viršelis"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Garsumo šliaužiklis"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Nepasirinkta jokia medija"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Informacija nepasiekiama"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Perduodamas ekranas"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-lv/strings.xml b/packages/MediaComponents/res/values-lv/strings.xml
new file mode 100644
index 0000000..454063e
--- /dev/null
+++ b/packages/MediaComponents/res/values-lv/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistēma"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Ierīces"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Apraides poga"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Apraides poga. Savienojums pārtraukts"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Apraides poga. Notiek savienojuma izveide"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Apraides poga. Savienojums izveidots"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Apraidīšana uz ierīci"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Notiek ierīču meklēšana"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Atvienot"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Apturēt apraidi"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Aizvērt"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Atskaņot"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Apturēt"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Apturēt"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Izvērst"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Sakļaut"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Albuma vāciņš"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Skaļuma slīdnis"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Nav atlasīti multivides faili"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Nav pieejama informācija"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Notiek ekrāna apraide"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-mk/strings.xml b/packages/MediaComponents/res/values-mk/strings.xml
new file mode 100644
index 0000000..12dee36
--- /dev/null
+++ b/packages/MediaComponents/res/values-mk/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Систем"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Уреди"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Копчето за Cast"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Копче за Cast. Исклучено"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Копче за Cast. Се поврзува"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Копче за Cast. Поврзано"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Емитувај на"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Се бараат уреди"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Исклучи"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Сопри го емитувањето"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Затвори"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Репродуцирај"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Паузирај"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Сопри"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Прошири"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Собери"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Корица на албум"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Лизгач за јачина на звук"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Не се избрани медиуми"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Нема достапни информации"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Екранот се емитува"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-ml/strings.xml b/packages/MediaComponents/res/values-ml/strings.xml
new file mode 100644
index 0000000..2d914b9
--- /dev/null
+++ b/packages/MediaComponents/res/values-ml/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"സിസ്റ്റം"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"ഉപകരണങ്ങൾ"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"ടാപ്പുചെയ്യുക"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"കാസ്റ്റ് ബട്ടൺ. വിച്ഛേദിച്ചു"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"കാസ്റ്റ് ബട്ടൺ. കണക്റ്റുചെയ്യുന്നു"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"കാസ്റ്റ് ബട്ടൺ. കണക്റ്റുചെയ്തു"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"ഇതിലേക്ക് കാസ്റ്റുചെയ്യുക"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"ഉപകരണങ്ങൾ കണ്ടെത്തുന്നു"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"വിച്ഛേദിക്കുക"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"കാസ്റ്റുചെയ്യൽ നിർത്തുക"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"അവസാനിപ്പിക്കുക"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"പ്ലേ ചെയ്യുക"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"തൽക്കാലം നിർത്തൂ"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"നിര്ത്തുക"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"വികസിപ്പിക്കുക"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"ചുരുക്കുക"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"ആൽബം ആർട്ട്"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"വോളിയം സ്ലൈഡർ"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"മീഡിയയൊന്നും തിരഞ്ഞെടുത്തിട്ടില്ല"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"വിവരങ്ങളൊന്നും ലഭ്യമല്ല"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"സ്ക്രീൻ കാസ്റ്റുചെയ്യുന്നു"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-mn/strings.xml b/packages/MediaComponents/res/values-mn/strings.xml
new file mode 100644
index 0000000..ef87c92
--- /dev/null
+++ b/packages/MediaComponents/res/values-mn/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Систем"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Төхөөрөмжүүд"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Дамжуулах товчлуур"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Дамжуулах товчлуур. Салсан"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Дамжуулах товчлуур. Холбож байна"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Дамжуулах товчлуур. Холбогдсон"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Дамжуулах"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Төхөөрөмж хайж байна"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Салгах"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Дамжуулахыг зогсоох"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Хаах"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Тоглуулах"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Түр зогсоох"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Зогсоох"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Дэлгэх"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Хураах"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Цомгийн зураг"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Дууны түвшин тааруулагч"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Ямар ч медиа сонгоогүй"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Мэдээлэл байхгүй байна"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Дэлгэцийг дамжуулж байна"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-mr/strings.xml b/packages/MediaComponents/res/values-mr/strings.xml
new file mode 100644
index 0000000..2ffbebb
--- /dev/null
+++ b/packages/MediaComponents/res/values-mr/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"सिस्टम"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"डिव्हाइसेस"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"कास्ट बटण"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"कास्ट बटण. डिस्कनेक्ट केले"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"कास्ट बटण. कनेक्ट करत आहे"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"कास्ट बटण. कनेक्ट केले"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"यावर कास्ट करा"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"डिव्हाइसेस शोधत आहे"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"डिस्कनेक्ट करा"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"कास्ट करणे थांबवा"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"बंद करा"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"प्ले करा"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"विराम"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"थांबा"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"विस्तृत करा"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"संकुचित करा"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"अल्बम कला"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"व्हॉल्यूम स्लायडर"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"मीडिया निवडला नाही"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"कोणतीही माहिती उपलब्ध नाही"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"स्क्रीन कास्ट करत आहे"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-ms/strings.xml b/packages/MediaComponents/res/values-ms/strings.xml
new file mode 100644
index 0000000..085e480
--- /dev/null
+++ b/packages/MediaComponents/res/values-ms/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistem"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Peranti"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Butang Hantar"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Butang hantar. Sambungan diputuskan"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Butang hantar. Menyambung"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Butang hantar. Disambungkan"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Hantar ke"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Mencari peranti"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Putuskan sambungan"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Berhenti menghantar"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Tutup"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Main"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Jeda"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Berhenti"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Kembangkan"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Runtuhkan"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Seni album"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Peluncur kelantangan"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Tiada media dipilih"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Maklumat tidak tersedia"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Menghantar skrin"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-my/strings.xml b/packages/MediaComponents/res/values-my/strings.xml
new file mode 100644
index 0000000..083d805
--- /dev/null
+++ b/packages/MediaComponents/res/values-my/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"စနစ်"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"စက်ပစ္စည်းများ"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"ကာစ်တ်လုပ်ရန် ခလုတ်"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"ကာစ်ခလုတ်။ ချိတ်ဆက်မထားပါ"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"ကာစ်ခလုတ်။ ချိတ်ဆက်နေသည်"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"ကာစ်ခလုတ်။ ချိတ်ဆက်ထားသည်"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"ကာစ်လုပ်ရန် စက်"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"စက်ပစ္စည်းများ ရှာဖွေခြင်း"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"ဆက်သွယ်မှု ဖြတ်ရန်"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"ကာစ်လုပ်ခြင်း ရပ်ရန်"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"ပိတ်ရန်"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"ဖွင့်ရန်"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"ခဏရပ်ရန်"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"ရပ်ရန်"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"ဖြန့်ချရန်၃"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"ခေါက်သိမ်းရန်..."</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"အယ်လ်ဘမ်ပုံ"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"အသံအတိုးအကျယ်ချိန်သည့် ဆလိုက်ဒါ"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"မည်သည့်မီဒီမှ မရွေးချယ်ထားပါ"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"အချက်အလက် မရရှိနိုင်ပါ"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"တည်းဖြတ်ရေး မျက်နှာပြင်"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-nb/strings.xml b/packages/MediaComponents/res/values-nb/strings.xml
new file mode 100644
index 0000000..4f764c9
--- /dev/null
+++ b/packages/MediaComponents/res/values-nb/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"System"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Enheter"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Cast-ikonet"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Cast-knappen. Frakoblet"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Cast-knappen. Kobler til"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Cast-knappen. Tilkoblet"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Cast til"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Søker etter enheter"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Koble fra"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Stopp castingen"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Lukk"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Spill av"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Sett på pause"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Stopp"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Utvid"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Skjul"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Albumgrafikk"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Glidebryter for volum"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Du har ikke valgt noen medier"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Ingen informasjon er tilgjengelig"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Caster skjermen"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-ne/strings.xml b/packages/MediaComponents/res/values-ne/strings.xml
new file mode 100644
index 0000000..d6c2e1a
--- /dev/null
+++ b/packages/MediaComponents/res/values-ne/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"प्रणाली"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"उपकरणहरू"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Cast बटन"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Cast बटन। जडान विच्छेद भयो"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Cast बटन। जडान हुँदै"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Cast बटन। जडान भयो"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"यसमा Cast गर्नुहोस्"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"यन्त्रहरू पत्ता लगाउँदै"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"विच्छेद गर्नुहोस्"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"casting रोक्नुहोस्"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"बन्द गर्नुहोस्"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"बजाउनुहोस्"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"रोक्नुहोस्"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"रोक्नुहोस्"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"विस्तार गर्नुहोस्"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"संक्षिप्त पार्नुहोस्"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"एल्बम आर्ट"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"भोल्युमको स्लाइडर"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"कुनै मिडिया चयन भएको छैन"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"जानकारी उपलब्ध छैन"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"स्क्रिन cast गर्दै"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-nl/strings.xml b/packages/MediaComponents/res/values-nl/strings.xml
new file mode 100644
index 0000000..05df62d
--- /dev/null
+++ b/packages/MediaComponents/res/values-nl/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Systeem"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Apparaten"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Cast-icoon"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Cast-icoon. Verbinding verbroken"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Cast-icoon. Verbinding maken"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Cast-icoon. Verbonden"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Casten naar"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Apparaten zoeken"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Loskoppelen"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Casten stoppen"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Sluiten"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Afspelen"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Onderbreken"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Stoppen"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Uitvouwen"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Samenvouwen"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Albumhoes"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Volumeschuifregelaar"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Geen media geselecteerd"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Geen informatie beschikbaar"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Scherm casten"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-pa/strings.xml b/packages/MediaComponents/res/values-pa/strings.xml
new file mode 100644
index 0000000..1b5df71
--- /dev/null
+++ b/packages/MediaComponents/res/values-pa/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"ਸਿਸਟਮ"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"ਡਿਵਾਈਸਾਂ"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"ਕਾਸਟ ਬਟਨ"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"ਕਾਸਟ ਬਟਨ। ਡਿਸਕਨੈਕਟ ਕੀਤਾ ਗਿਆ"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"ਕਾਸਟ ਬਟਨ। ਕਨੈਕਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"ਕਾਸਟ ਬਟਨ। ਕਨੈਕਟ ਕੀਤਾ ਗਿਆ"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"ਏਥੇ ਕਾਸਟ ਕਰੋ"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"ਡੀਵਾਈਸਾਂ ਨੂੰ ਲੱਭਿਆ ਜਾ ਰਿਹਾ ਹੈ"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"ਡਿਸਕਨੈਕਟ ਕਰੋ"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"ਕਾਸਟ ਕਰਨਾ ਬੰਦ ਕਰੋ"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"ਬੰਦ ਕਰੋ"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"ਪਲੇ ਕਰੋ"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"ਰੋਕੋ"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"ਬੰਦ ਕਰੋ"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"ਵਿਸਤਾਰ ਕਰੋ"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"ਬੰਦ ਕਰੋ"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"ਐਲਬਮ ਆਰਟ"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"ਵੌਲਯੂਮ ਸਲਾਈਡਰ"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"ਕੋਈ ਵੀ ਮੀਡੀਆ ਨਹੀਂ ਚੁਣਿਆ ਗਿਆ"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"ਕੋਈ ਜਾਣਕਾਰੀ ਉਪਲਬਧ ਨਹੀਂ"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"ਸਕ੍ਰੀਨ ਜੋੜ ਰਿਹਾ ਹੈ"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-pl/strings.xml b/packages/MediaComponents/res/values-pl/strings.xml
new file mode 100644
index 0000000..c792a6d
--- /dev/null
+++ b/packages/MediaComponents/res/values-pl/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"System"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Urządzenia"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Przycisk Cast"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Przycisk Prześlij ekran. Rozłączono"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Przycisk Prześlij ekran. Łączę"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Przycisk Prześlij ekran. Połączono"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Przesyłaj na"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Znajdowanie urządzeń"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Odłącz"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Zatrzymaj przesyłanie"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Zamknij"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Odtwórz"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Wstrzymaj"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Zatrzymaj"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Rozwiń"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Zwiń"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Okładka albumu"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Suwak głośności"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Nie wybrano multimediów"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Brak informacji"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Przesyłam ekran"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-pt-rBR/strings.xml b/packages/MediaComponents/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000..43c619d
--- /dev/null
+++ b/packages/MediaComponents/res/values-pt-rBR/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistema"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Dispositivos"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Botão Transmitir"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Botão \"Transmitir\". Desconectado"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Botão \"Transmitir\". Conectando"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Botão \"Transmitir\". Conectado"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Transmitir para"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Localizando dispositivos"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Desconectar"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Interromper transmissão"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Fechar"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Reproduzir"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Pausar"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Parar"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Expandir"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Recolher"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Arte do álbum"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Controle deslizante de volume"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Nenhuma mídia selecionada"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Nenhuma informação disponível"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Transmitindo a tela"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-pt-rPT/strings.xml b/packages/MediaComponents/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..3f0a61d
--- /dev/null
+++ b/packages/MediaComponents/res/values-pt-rPT/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistema"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Dispositivos"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Botão Transmitir"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Botão Transmitir. Desligado"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Botão Transmitir. A ligar..."</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Botão Transmitir. Ligado"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Transmitir para"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"A localizar dispositivos"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Desassociar"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Parar a transmissão"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Fechar"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Reproduzir"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Interromper"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Parar"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Expandir"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Reduzir"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Imagem do álbum"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Controlo de deslize do volume"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Nenhum suporte multimédia selecionado"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Nenhuma informação disponível"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"A transmitir o ecrã"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-pt/strings.xml b/packages/MediaComponents/res/values-pt/strings.xml
new file mode 100644
index 0000000..43c619d
--- /dev/null
+++ b/packages/MediaComponents/res/values-pt/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistema"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Dispositivos"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Botão Transmitir"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Botão \"Transmitir\". Desconectado"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Botão \"Transmitir\". Conectando"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Botão \"Transmitir\". Conectado"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Transmitir para"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Localizando dispositivos"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Desconectar"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Interromper transmissão"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Fechar"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Reproduzir"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Pausar"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Parar"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Expandir"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Recolher"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Arte do álbum"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Controle deslizante de volume"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Nenhuma mídia selecionada"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Nenhuma informação disponível"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Transmitindo a tela"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-ro/strings.xml b/packages/MediaComponents/res/values-ro/strings.xml
new file mode 100644
index 0000000..6ebb2f6
--- /dev/null
+++ b/packages/MediaComponents/res/values-ro/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistem"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Dispozitive"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Butonul de proiecție"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Butonul de proiecție. Deconectat"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Butonul de proiecție. Se conectează"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Butonul de proiecție. Conectat"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Proiectați pe"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Se caută dispozitive"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Deconectați-vă"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Nu mai proiectați"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Închideți"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Redați"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Întrerupeți"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Opriți"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Extindeți"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Restrângeți"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Grafica albumului"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Glisor pentru volum"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Niciun fișier media selectat"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Nu sunt disponibile informații"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Se proiectează ecranul"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-ru/strings.xml b/packages/MediaComponents/res/values-ru/strings.xml
new file mode 100644
index 0000000..7c462d2
--- /dev/null
+++ b/packages/MediaComponents/res/values-ru/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Система"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Устройства"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Кнопка трансляции"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Кнопка трансляции. Устройство отключено."</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Кнопка трансляции. Устройство подключается."</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Кнопка трансляции. Устройство подключено."</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Выберите устройство"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Поиск устройств…"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Отключить"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Прекратить трансляцию"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Закрыть"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Воспроизвести"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Приостановить"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Остановить"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Развернуть"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Свернуть"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Обложка"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Регулятор громкости"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Медиафайл не выбран"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Данных нет"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Подключение к удаленному монитору"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-si/strings.xml b/packages/MediaComponents/res/values-si/strings.xml
new file mode 100644
index 0000000..a55ce50
--- /dev/null
+++ b/packages/MediaComponents/res/values-si/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"පද්ධතිය"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"උපාංග"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"විකාශ බොත්තම"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"විකාශ බොත්තම. විසන්ධි කරන ලදී"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"විකාශ බොත්තම සම්බන්ධ කරමින්"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"විකාශ බොත්තම සම්බන්ධ කරන ලදී"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"විකාශය"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"උපාංග සෙවීම"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"විසන්ධි කරන්න"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"විකාශ කිරීම නතර කරන්න"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"වසන්න"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"ධාවනය කරන්න"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"විරාම ගන්වන්න"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"නතර කරන්න"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"දිග හරින්න"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"හකුළන්න"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"ඇල්බම කලාව"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"හඬ පරිමා ස්ලයිඩරය"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"මාධ්යය තෝරා නැත"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"ලබා ගත හැකි තොරතුරු නොමැත"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"විකාශ තිරය"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-sk/strings.xml b/packages/MediaComponents/res/values-sk/strings.xml
new file mode 100644
index 0000000..a58aa11
--- /dev/null
+++ b/packages/MediaComponents/res/values-sk/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Systém"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Zariadenia"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Tlačidlo prenosu"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Tlačidlo prenosu. Odpojené"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Tlačidlo prenosu. Pripája sa"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Tlačidlo prenosu. Pripojené"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Prenos do"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Hľadajú sa zariadenia"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Odpojiť"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Zastaviť prenášanie"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Zavrieť"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Prehrať"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Pozastaviť"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Zastaviť"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Rozbaliť"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Zbaliť"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Obrázok albumu"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Posúvač hlasitosti"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Nie sú vybrané žiadne médiá"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Nie sú k dispozícii žiadne informácie"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Prenáša sa obrazovka"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-sl/strings.xml b/packages/MediaComponents/res/values-sl/strings.xml
new file mode 100644
index 0000000..8ca4ce4
--- /dev/null
+++ b/packages/MediaComponents/res/values-sl/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistem"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Naprave"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Gumb za predvajanje"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Gumb za predvajanje. Povezava je prekinjena."</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Gumb za predvajanje. Vzpostavljanje povezave."</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Gumb za predvajanje. Povezava je vzpostavljena."</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Predvajanje prek:"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Iskanje naprav"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Prekini povezavo"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Ustavi predvajanje"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Zapri"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Predvajanje"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Zaustavi"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Ustavi"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Razširi"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Strni"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Naslovnica albuma"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Drsnik za glasnost"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Ni izbrane predstavnosti"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Podatki niso na voljo"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Predvajanje zaslona"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-sq/strings.xml b/packages/MediaComponents/res/values-sq/strings.xml
new file mode 100644
index 0000000..816e110
--- /dev/null
+++ b/packages/MediaComponents/res/values-sq/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistemi"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Pajisjet"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Butoni i transmetimit"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Butoni i transmetimit. Je i shkëputur"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Butoni i transmetimit. Po lidhet"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Butoni i transmetimit. Je i lidhur"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Transmeto te"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Po kërkon pajisje"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Shkëpute"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Ndalo transmetimin"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Mbyll"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Luaj"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Pauzë"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Ndalo"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Zgjeroje"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Palose"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Kopertina e albumit"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Rrëshqitësi i volumit"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Nuk u zgjodh asnjë media"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Nuk jepet asnjë informacion"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Po transmeton ekranin"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-sr/strings.xml b/packages/MediaComponents/res/values-sr/strings.xml
new file mode 100644
index 0000000..caabad5
--- /dev/null
+++ b/packages/MediaComponents/res/values-sr/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Систем"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Уређаји"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Дугме Пребаци"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Дугме Пребаци. Веза је прекинута"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Дугме Пребаци. Повезује се"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Дугме Пребаци. Повезан је"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Пребацуј на"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Проналажење уређаја"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Прекини везу"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Заустави пребацивање"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Затвори"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Пусти"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Паузирај"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Заустави"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Прошири"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Скупи"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Омот албума"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Клизач за јачину звука"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Нема изабраних медија"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Нису доступне никакве информације"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Пребацује се екран"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-sv/strings.xml b/packages/MediaComponents/res/values-sv/strings.xml
new file mode 100644
index 0000000..ca7d3e0
--- /dev/null
+++ b/packages/MediaComponents/res/values-sv/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"System"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Enheter"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Cast-knappen"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Cast-knappen. Frånkopplad"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Cast-knappen. Ansluter"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Cast-knappen. Ansluten"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Casta till"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Letar efter enheter"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Koppla från"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Sluta casta"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Stäng"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Spela upp"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Pausa"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Avbryt"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Utöka"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Komprimera"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Skivomslag"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Volymreglage"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Inga media har valts"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Det finns ingen information"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Skärmen castas"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-sw/strings.xml b/packages/MediaComponents/res/values-sw/strings.xml
new file mode 100644
index 0000000..9562cb1
--- /dev/null
+++ b/packages/MediaComponents/res/values-sw/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Mfumo"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Vifaa"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Kitufe cha kutuma"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Kitufe cha kutuma. Kimeondolewa"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Kitufe cha kutuma. Kinaunganisha"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Kitufe cha kutuma. Kimeunganishwa"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Tuma kwenye"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Inatafuta vifaa"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Ondoa"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Acha kutuma"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Funga"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Cheza"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Sitisha"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Simamisha"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Panua"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Kunja"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Sanaa ya albamu"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Kitelezi cha sauti"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Hakuna maudhui yaliyochaguliwa"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Hakuna maelezo yaliyopatikana"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Inatuma skrini"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-sw600dp/dimens.xml b/packages/MediaComponents/res/values-sw600dp/dimens.xml
new file mode 100644
index 0000000..4042348
--- /dev/null
+++ b/packages/MediaComponents/res/values-sw600dp/dimens.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<resources>
+ <!-- The platform's desired fixed width for a dialog along the major axis
+ (the screen is in landscape). This may be either a fraction or a dimension.-->
+ <item type="dimen" name="mr_dialog_fixed_width_major">60%</item>
+ <!-- The platform's desired fixed width for a dialog along the minor axis
+ (the screen is in portrait). This may be either a fraction or a dimension.-->
+ <item type="dimen" name="mr_dialog_fixed_width_minor">90%</item>
+</resources>
diff --git a/packages/MediaComponents/res/values-sw720dp/dimens.xml b/packages/MediaComponents/res/values-sw720dp/dimens.xml
new file mode 100644
index 0000000..634ab8d
--- /dev/null
+++ b/packages/MediaComponents/res/values-sw720dp/dimens.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<resources>
+ <!-- The platform's desired fixed width for a dialog along the major axis
+ (the screen is in landscape). This may be either a fraction or a dimension.-->
+ <item type="dimen" name="mr_dialog_fixed_width_major">50%</item>
+ <!-- The platform's desired fixed width for a dialog along the minor axis
+ (the screen is in portrait). This may be either a fraction or a dimension.-->
+ <item type="dimen" name="mr_dialog_fixed_width_minor">70%</item>
+</resources>
diff --git a/packages/MediaComponents/res/values-ta/strings.xml b/packages/MediaComponents/res/values-ta/strings.xml
new file mode 100644
index 0000000..e1978f3
--- /dev/null
+++ b/packages/MediaComponents/res/values-ta/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"சிஸ்டம்"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"சாதனங்கள்"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"திரையிடு பட்டன்"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"அனுப்புதல் பொத்தான். துண்டிக்கப்பட்டது"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"அனுப்புதல் பொத்தான். இணைக்கிறது"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"அனுப்புதல் பொத்தான். இணைக்கப்பட்டது"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"இதற்கு அனுப்பு"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"சாதனங்களைத் தேடுகிறது"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"தொடர்பைத் துண்டி"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"அனுப்புவதை நிறுத்து"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"மூடும்"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"இயக்கும்"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"இடைநிறுத்தும்"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"நிறுத்துவதற்கான பொத்தான்"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"விரிவாக்கு"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"சுருக்கு"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"ஆல்பம் ஆர்ட்"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"ஒலியளவு ஸ்லைடர்"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"மீடியா எதுவும் தேர்ந்தெடுக்கப்படவில்லை"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"தகவல் எதுவுமில்லை"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"திரையை அனுப்புகிறீர்கள்"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-te/strings.xml b/packages/MediaComponents/res/values-te/strings.xml
new file mode 100644
index 0000000..7d312e3
--- /dev/null
+++ b/packages/MediaComponents/res/values-te/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"సిస్టమ్"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"పరికరాలు"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"ప్రసారం చేయి బటన్"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"ప్రసార బటన్. డిస్కనెక్ట్ చేయబడింది"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"ప్రసార బటన్. కనెక్ట్ చేస్తోంది"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"ప్రసార బటన్. కనెక్ట్ చేయబడింది"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"దీనికి ప్రసారం చేయండి"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"పరికరాలను కనుగొంటోంది"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"డిస్కనెక్ట్ చేయి"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"ప్రసారాన్ని ఆపివేయి"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"మూసివేస్తుంది"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"ప్లే చేస్తుంది"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"పాజ్ చేస్తుంది"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"ఆపివేయి"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"విస్తరింపజేస్తుంది"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"కుదిస్తుంది"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"ఆల్బమ్ ఆర్ట్"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"వాల్యూమ్ స్లయిడర్"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"మీడియా ఏదీ ఎంచుకోబడలేదు"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"సమాచారం అందుబాటులో లేదు"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"స్క్రీన్ను ప్రసారం చేస్తోంది"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-th/strings.xml b/packages/MediaComponents/res/values-th/strings.xml
new file mode 100644
index 0000000..cfa8ae5
--- /dev/null
+++ b/packages/MediaComponents/res/values-th/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"ระบบ"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"อุปกรณ์"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"ปุ่ม \"แคสต์\""</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"ปุ่ม \"แคสต์\" ยกเลิกการเชื่อมต่อ"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"ปุ่ม \"แคสต์\" กำลังเชื่อมต่อ"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"ปุ่ม \"แคสต์\" เชื่อมต่อแล้ว"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"แคสต์ไปยัง"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"กำลังค้นหาอุปกรณ์"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"ยกเลิกการเชื่อมต่อ"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"หยุดแคสต์"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"ปิด"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"เล่น"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"หยุดชั่วคราว"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"หยุด"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"ขยาย"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"ยุบ"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"ปกอัลบั้ม"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"แถบเลื่อนปรับระดับเสียง"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"ไม่ได้เลือกสื่อไว้"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"ไม่มีข้อมูล"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"กำลังแคสต์หน้าจอ"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-tl/strings.xml b/packages/MediaComponents/res/values-tl/strings.xml
new file mode 100644
index 0000000..a8be3d0
--- /dev/null
+++ b/packages/MediaComponents/res/values-tl/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"System"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Mga Device"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Button na I-cast"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Button na I-cast. Nadiskonekta"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Button na I-cast. Kumokonekta"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Button na I-cast. Nakakonekta"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"I-cast sa"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Naghahanap ng mga device"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Idiskonekta"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Ihinto ang pag-cast"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Isara"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"I-play"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"I-pause"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Ihinto"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Palawakin"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"I-collapse"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Album art"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Slider ng volume"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Walang piniling media"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Walang available na impormasyon"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Kina-cast ang screen"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-tr/strings.xml b/packages/MediaComponents/res/values-tr/strings.xml
new file mode 100644
index 0000000..05f6392
--- /dev/null
+++ b/packages/MediaComponents/res/values-tr/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistem"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Cihazlar"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Yayınla düğmesi"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Yayınla düğmesi. Bağlantı kesildi"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Yayınla düğmesi. Bağlanıyor"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Yayınla düğmesi. Bağlandı"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Şuraya yayınla:"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Cihazlar bulunuyor"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Bağlantıyı kes"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Yayını durdur"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Kapat"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Oynat"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Duraklat"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Durdur"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Genişlet"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Daralt"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Albüm kapağı"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Ses düzeyi kaydırma çubuğu"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Medya seçilmedi"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Bilgi yok"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Ekran yayınlanıyor"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-uk/strings.xml b/packages/MediaComponents/res/values-uk/strings.xml
new file mode 100644
index 0000000..33d365e
--- /dev/null
+++ b/packages/MediaComponents/res/values-uk/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Система"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Пристрої"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Кнопка трансляції"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Кнопка трансляції. Від’єднано"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Кнопка трансляції. Під’єднання"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Кнопка трансляції. Під’єднано"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Транслювати на"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Пошук пристроїв"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Відключити"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Припинити трансляцію"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Закрити"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Відтворити"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Призупинити"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Припинити"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Розгорнути"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Згорнути"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Обкладинка альбому"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Повзунок гучності"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Медіа-файл не вибрано"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Немає даних"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Трансляція екрана"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-ur/strings.xml b/packages/MediaComponents/res/values-ur/strings.xml
new file mode 100644
index 0000000..632c598
--- /dev/null
+++ b/packages/MediaComponents/res/values-ur/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"سسٹم"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"آلات"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"کاسٹ کرنے کا بٹن"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"کاسٹ کرنے کا بٹن۔ غیر منسلک ہے"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"کاسٹ کرنے کا بٹن۔ منسلک ہو رہا ہے"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"کاسٹ کرنے کا بٹن۔ منسلک ہے"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"اس میں کاسٹ کریں"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"آلات تلاش ہو رہے ہیں"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"غیر منسلک کریں"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"کاسٹ کرنا بند کریں"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"بند کریں"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"چلائیں"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"موقوف کریں"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"روکیں"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"پھیلائیں"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"سکیڑیں"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"البم آرٹ"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"والیوم سلائیڈر"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"کوئی میڈیا منتخب نہیں ہے"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"کوئی معلومات دستیاب نہیں"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"سکرین کاسٹ ہو رہی ہے"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-uz/strings.xml b/packages/MediaComponents/res/values-uz/strings.xml
new file mode 100644
index 0000000..10a0817
--- /dev/null
+++ b/packages/MediaComponents/res/values-uz/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Tizim"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Qurilmalar"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Translatsiya tugmasi"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Translatsiya tugmasi. Uzildi"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Translatsiya tugmasi. Ulanmoqda"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Translatsiya tugmasi. Ulandi"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Quyidagiga translatsiya qilish:"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Qurilmalarni topish"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Ulanishni uzish"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Translatsiyani to‘xtatish"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Yopish"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Boshlash"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"To‘xtatib turish"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"To‘xtatish"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Yoyish"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Yig‘ish"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Albom muqovasi"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Tovush balandligi slayderi"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Multimedia tanlamagan"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Hech qanday ma’lumot yo‘q"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Ekranni translatsiya qilish"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-vi/strings.xml b/packages/MediaComponents/res/values-vi/strings.xml
new file mode 100644
index 0000000..7098cca
--- /dev/null
+++ b/packages/MediaComponents/res/values-vi/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Hệ thống"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Thiết bị"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Nút truyền"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Nút truyền. Đã ngắt kết nối"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Nút truyền. Đang kết nối"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Nút truyền. Đã kết nối"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Truyền tới"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Đang tìm thiết bị"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Ngắt kết nối"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Dừng truyền"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Đóng"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Phát"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Tạm dừng"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Dừng"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Mở rộng"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Thu gọn"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Ảnh bìa album"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Thanh trượt âm lượng"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Không có phương tiện nào được chọn"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Không có thông tin nào"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Đang truyền màn hình"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-zh-rCN/strings.xml b/packages/MediaComponents/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..1e22d01
--- /dev/null
+++ b/packages/MediaComponents/res/values-zh-rCN/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"系统"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"设备"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"投射按钮"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"投射按钮。已断开连接"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"投射按钮。正在连接"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"投射按钮。已连接"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"投射到"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"正在查找设备"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"断开连接"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"停止投射"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"关闭"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"播放"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"暂停"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"停止"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"展开"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"收起"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"专辑封面"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"音量滑块"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"未选择任何媒体"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"没有任何相关信息"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"正在投射屏幕"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-zh-rHK/strings.xml b/packages/MediaComponents/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..156e5c2
--- /dev/null
+++ b/packages/MediaComponents/res/values-zh-rHK/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"系統"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"裝置"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"投放按鈕"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"投放按鈕。已解除連接"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"投放按鈕。正在連接"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"投放按鈕。已連接"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"投放至"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"正在尋找裝置"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"中斷連線"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"停止投放"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"關閉"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"播放"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"暫停"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"停止"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"展開"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"收合"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"專輯封面"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"音量滑桿"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"尚未選擇媒體"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"沒有詳細資料"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"正在投放螢幕"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-zh-rTW/strings.xml b/packages/MediaComponents/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..6cafde1
--- /dev/null
+++ b/packages/MediaComponents/res/values-zh-rTW/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"系統"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"裝置"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"投放按鈕"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"投放按鈕;已中斷連線"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"投放按鈕;連線中"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"投放按鈕;已連線"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"投放到"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"正在尋找裝置"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"中斷連線"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"停止投放"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"關閉"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"播放"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"暫停"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"停止"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"展開"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"收合"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"專輯封面"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"音量滑桿"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"未選取任何媒體"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"沒有可用的資訊"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"正在投放螢幕"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values-zu/strings.xml b/packages/MediaComponents/res/values-zu/strings.xml
new file mode 100644
index 0000000..e107c43
--- /dev/null
+++ b/packages/MediaComponents/res/values-zu/strings.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2018 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Isistimu"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Amadivayisi"</string>
+ <string name="mr_button_content_description" msgid="3698378085901466129">"Inkinobho ye-Cast"</string>
+ <string name="mr_cast_button_disconnected" msgid="816305490427819240">"Inkinobho yokusakaza. Kunqanyuliwe"</string>
+ <string name="mr_cast_button_connecting" msgid="2187642765091873834">"Inkinobho yokusakaza. Kuyaxhunywa"</string>
+ <string name="mr_cast_button_connected" msgid="5088427771788648085">"Inkinobho yokusakaza. Kuxhunyiwe"</string>
+ <string name="mr_chooser_title" msgid="414301941546135990">"Sakaza ku-"</string>
+ <string name="mr_chooser_searching" msgid="6349900579507521956">"Ithola amadivayisi"</string>
+ <string name="mr_controller_disconnect" msgid="1227264889412989580">"Nqamula"</string>
+ <string name="mr_controller_stop_casting" msgid="8857886794086583226">"Misa ukusakaza"</string>
+ <string name="mr_controller_close_description" msgid="7333862312480583260">"Vala"</string>
+ <string name="mr_controller_play" msgid="683634565969987458">"Dlala"</string>
+ <string name="mr_controller_pause" msgid="5451884435510905406">"Misa isikhashana"</string>
+ <string name="mr_controller_stop" msgid="735874641921425123">"Misa"</string>
+ <string name="mr_controller_expand_group" msgid="8062427022744266907">"Nweba"</string>
+ <string name="mr_controller_collapse_group" msgid="7924809056904240926">"Goqa"</string>
+ <string name="mr_controller_album_art" msgid="6422801843540543585">"Ubuciko be-albhamu"</string>
+ <string name="mr_controller_volume_slider" msgid="2361785992211841709">"Isilayida sevolumu"</string>
+ <string name="mr_controller_no_media_selected" msgid="6547130360349182381">"Ayikho imidiya ekhethiwe"</string>
+ <string name="mr_controller_no_info_available" msgid="5585418471741142924">"Alukho ulwazi olutholakalayo"</string>
+ <string name="mr_controller_casting_screen" msgid="4868457957151124867">"Isikrini sokusakaza"</string>
+</resources>
diff --git a/packages/MediaComponents/res/values/attrs.xml b/packages/MediaComponents/res/values/attrs.xml
new file mode 100644
index 0000000..6175b11
--- /dev/null
+++ b/packages/MediaComponents/res/values/attrs.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<resources>
+ <declare-styleable name="MediaRouteButton">
+ <!-- This drawable is a state list where the "checked" state
+ indicates active media routing. Checkable indicates connecting
+ and non-checked / non-checkable indicates
+ that media is playing to the local device only. -->
+ <attr name="externalRouteEnabledDrawable" format="reference" />
+ <!-- Tint to apply to the media route button -->
+ <attr name="mediaRouteButtonTint" format="color" />
+
+ <attr name="android:minWidth" />
+ <attr name="android:minHeight" />
+ </declare-styleable>
+
+ <attr name="mediaRouteButtonStyle" format="reference" />
+ <attr name="mediaRouteCloseDrawable" format="reference" />
+ <attr name="mediaRoutePlayDrawable" format="reference" />
+ <attr name="mediaRoutePauseDrawable" format="reference" />
+ <attr name="mediaRouteStopDrawable" format="reference" />
+ <attr name="mediaRouteAudioTrackDrawable" format="reference" />
+ <attr name="mediaRouteDefaultIconDrawable" format="reference" />
+ <attr name="mediaRouteTvIconDrawable" format="reference" />
+ <attr name="mediaRouteSpeakerIconDrawable" format="reference" />
+ <attr name="mediaRouteSpeakerGroupIconDrawable" format="reference" />
+ <attr name="mediaRouteControlPanelThemeOverlay" format="reference" />
+
+ <attr name="mediaRouteTheme" format="reference" />
+ <attr name="enableControlView" format="boolean" />
+</resources>
diff --git a/packages/MediaComponents/res/values/dimens.xml b/packages/MediaComponents/res/values/dimens.xml
new file mode 100644
index 0000000..91241cd
--- /dev/null
+++ b/packages/MediaComponents/res/values/dimens.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<resources>
+ <!-- Dialog size -->
+ <eat-comment />
+ <!-- The platform's desired fixed width for a dialog along the major axis
+ (the screen is in landscape). This may be either a fraction or a dimension.-->
+ <dimen name="mr_dialog_fixed_width_major">320dp</dimen>
+ <!-- The platform's desired fixed width for a dialog along the minor axis
+ (the screen is in portrait). This may be either a fraction or a dimension.-->
+ <dimen name="mr_dialog_fixed_width_minor">320dp</dimen>
+
+ <!-- MediaRouteController's volume group list -->
+ <eat-comment />
+ <!-- Maximum height of volume group list. -->
+ <dimen name="mr_controller_volume_group_list_max_height">288dp</dimen>
+ <!-- Height of volume group item. -->
+ <dimen name="mr_controller_volume_group_list_item_height">68dp</dimen>
+ <!-- Size of an item's icon. -->
+ <dimen name="mr_controller_volume_group_list_item_icon_size">24dp</dimen>
+
+ <dimen name="mr_controller_volume_group_list_padding_top">16dp</dimen>
+ <!-- Group list expand/collapse animation duration. -->
+ <integer name="mr_controller_volume_group_list_animation_duration_ms">400</integer>
+ <!-- Group list fade in animation duration. -->
+ <integer name="mr_controller_volume_group_list_fade_in_duration_ms">400</integer>
+ <!-- Group list fade out animation duration. -->
+ <integer name="mr_controller_volume_group_list_fade_out_duration_ms">200</integer>
+</resources>
diff --git a/packages/MediaComponents/res/values/strings.xml b/packages/MediaComponents/res/values/strings.xml
new file mode 100644
index 0000000..35db0e5
--- /dev/null
+++ b/packages/MediaComponents/res/values/strings.xml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<resources>
+ <!-- Name for the default system route prior to Jellybean. [CHAR LIMIT=30] -->
+ <string name="mr_system_route_name">System</string>
+
+ <!-- Name for the user route category created when publishing routes to the system in Jellybean and above. [CHAR LIMIT=30] -->
+ <string name="mr_user_route_category_name">Devices</string>
+
+ <!-- String to be shown as a tooltip of MediaRouteButton
+ Cast is the standard android verb for sending content to a remote device. [CHAR LIMIT=50] -->
+ <string name="mr_button_content_description">Cast button</string>
+
+ <!-- Content description of a MediaRouteButton for accessibility support when no remote device is connected.
+ Cast is the standard android verb for sending content to a remote device. [CHAR LIMIT=NONE] -->
+ <string name="mr_cast_button_disconnected">Cast button. Disconnected</string>
+
+ <!-- Content description of a MediaRouteButton for accessibility support while connecting to a remote device.
+ Cast is the standard android verb for sending content to a remote device. [CHAR LIMIT=NONE] -->
+ <string name="mr_cast_button_connecting">Cast button. Connecting</string>
+
+ <!-- Content description of a MediaRouteButton for accessibility support when a remote device is connected.
+ Cast is the standard android verb for sending content to a remote device. [CHAR LIMIT=NONE] -->
+ <string name="mr_cast_button_connected">Cast button. Connected</string>
+
+ <!-- Title of the media route chooser dialog. [CHAR LIMIT=30] -->
+ <string name="mr_chooser_title">Cast to</string>
+
+ <!-- Placeholder text to show when no devices have been found. [CHAR LIMIT=50] -->
+ <string name="mr_chooser_searching">Finding devices</string>
+
+ <!-- Button to disconnect from a media route. [CHAR LIMIT=30] -->
+ <string name="mr_controller_disconnect">Disconnect</string>
+
+ <!-- Button to stop playback and disconnect from a media route. [CHAR LIMIT=30] -->
+ <string name="mr_controller_stop_casting">Stop casting</string>
+
+ <!-- Content description for accessibility (not shown on the screen): dialog close button. [CHAR LIMIT=NONE] -->
+ <string name="mr_controller_close_description">Close</string>
+
+ <!-- Content description for accessibility (not shown on the screen): media play button. [CHAR LIMIT=NONE] -->
+ <string name="mr_controller_play">Play</string>
+
+ <!-- Content description for accessibility (not shown on the screen): media pause button. [CHAR LIMIT=NONE] -->
+ <string name="mr_controller_pause">Pause</string>
+
+ <!-- Content description for accessibility (not shown on the screen): media stop button. [CHAR LIMIT=NONE] -->
+ <string name="mr_controller_stop">Stop</string>
+
+ <!-- Content description for accessibility (not shown on the screen): group expand button. Pressing button shows group members of a selected route group. [CHAR LIMIT=NONE] -->
+ <string name="mr_controller_expand_group">Expand</string>
+
+ <!-- Content description for accessibility (not shown on the screen): group collapse button. Pressing button hides group members of a selected route group. [CHAR LIMIT=NONE] -->
+ <string name="mr_controller_collapse_group">Collapse</string>
+
+ <!-- Content description for accessibility (not shown on the screen): album art button. Clicking on the album art takes user to a predefined activity per media app. [CHAR LIMIT=50] -->
+ <string name="mr_controller_album_art">Album art</string>
+
+ <!-- Content description for accessibility (not shown on the screen): volume slider. [CHAR LIMIT=NONE] -->
+ <string name="mr_controller_volume_slider">Volume slider</string>
+
+ <!-- Placeholder text to show when no media have been selected for playback. [CHAR LIMIT=50] -->
+ <string name="mr_controller_no_media_selected">No media selected</string>
+
+ <!-- Placeholder text to show when no title/description have been found for a given song/video. [CHAR LIMIT=50] -->
+ <string name="mr_controller_no_info_available">No info available</string>
+
+ <!-- Placeholder text indicating that the user is currently casting screen. [CHAR LIMIT=50] -->
+ <string name="mr_controller_casting_screen">Casting screen</string>
+
+ <string name="lockscreen_pause_button_content_description">Pause</string>
+ <string name="lockscreen_play_button_content_description">Play</string>
+ <string name="lockscreen_replay_button_content_description">Replay</string>
+
+ <!-- Text for error alert when a video container is not valid for progressive download/playback. -->
+ <string name="VideoView2_error_text_invalid_progressive_playback">This video isn\'t valid for streaming to this device.</string>
+ <!-- Text for error alert when a video cannot be played. It can be used by any app. -->
+ <string name="VideoView2_error_text_unknown">Can\'t play this video.</string>
+ <!-- Button to close error alert when a video cannot be played. -->
+ <string name="VideoView2_error_button">OK</string>
+</resources>
diff --git a/packages/MediaComponents/res/values/style.xml b/packages/MediaComponents/res/values/style.xml
new file mode 100644
index 0000000..d31b41d
--- /dev/null
+++ b/packages/MediaComponents/res/values/style.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="TransportControlsButton">
+ <item name="android:background">@null</item>
+ <item name="android:layout_width">70dp</item>
+ <item name="android:layout_height">40dp</item>
+ </style>
+
+ <style name="TransportControlsButton.Previous">
+ <item name="android:src">@drawable/ic_skip_previous</item>
+ </style>
+
+ <style name="TransportControlsButton.Next">
+ <item name="android:src">@drawable/ic_skip_next</item>
+ </style>
+
+ <style name="TransportControlsButton.Pause">
+ <item name="android:src">@drawable/ic_pause_circle_filled</item>
+ </style>
+
+ <style name="TransportControlsButton.Ffwd">
+ <item name="android:src">@drawable/ic_forward_30</item>
+ </style>
+
+ <style name="TransportControlsButton.Rew">
+ <item name="android:src">@drawable/ic_rewind_10</item>
+ </style>
+
+
+ <style name="TitleBarButton">
+ <item name="android:background">@null</item>
+ <item name="android:layout_width">36dp</item>
+ <item name="android:layout_height">36dp</item>
+ <item name="android:layout_margin">10dp</item>
+ </style>
+
+ <style name="TitleBarButton.MediaRouteButton">
+ <item name="android:src">@drawable/ic_cast</item>
+ </style>
+
+
+ <style name="BottomBarButton">
+ <item name="android:background">@null</item>
+ <item name="android:layout_width">24dp</item>
+ <item name="android:layout_height">24dp</item>
+ <item name="android:layout_margin">10dp</item>
+ </style>
+
+ <style name="BottomBarButton.CC">
+ <item name="android:src">@drawable/ic_media_cc_disabled</item>
+ </style>
+
+ <style name="BottomBarButton.FullScreen">
+ <item name="android:src">@drawable/ic_fullscreen</item>
+ </style>
+
+ <style name="BottomBarButton.Overflow">
+ <item name="android:src">@drawable/ic_chevron_right</item>
+ </style>
+
+</resources>
+
diff --git a/packages/MediaComponents/res/values/styles.xml b/packages/MediaComponents/res/values/styles.xml
new file mode 100644
index 0000000..bde6900
--- /dev/null
+++ b/packages/MediaComponents/res/values/styles.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<resources>
+ <style name="Widget.MediaRouter.MediaRouteButton"
+ parent="Widget.AppCompat.ActionButton">
+ <item name="externalRouteEnabledDrawable">@drawable/mr_button_dark</item>
+ </style>
+
+ <style name="Widget.MediaRouter.Light.MediaRouteButton"
+ parent="Widget.AppCompat.Light.ActionButton">
+ <item name="externalRouteEnabledDrawable">@drawable/mr_button_light</item>
+ </style>
+
+ <style name="TextAppearance.MediaRouter.Title" parent="TextAppearance.AppCompat.Title" />
+
+ <style name="TextAppearance.MediaRouter.PrimaryText" parent="TextAppearance.AppCompat.Subhead" />
+
+ <style name="TextAppearance.MediaRouter.SecondaryText" parent="TextAppearance.AppCompat.Body1" />
+</resources>
diff --git a/packages/MediaComponents/res/values/symbols.xml b/packages/MediaComponents/res/values/symbols.xml
new file mode 100644
index 0000000..ee0e8c6
--- /dev/null
+++ b/packages/MediaComponents/res/values/symbols.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/* Copyright 2017, 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.
+*/
+-->
+<resources>
+ <!--java-symbol type="id" name="cc" />
+ <java-symbol type="id" name="ffwd" />
+ <java-symbol type="id" name="mediacontroller_progress" />
+ <java-symbol type="id" name="next" />
+ <java-symbol type="id" name="pause" />
+ <java-symbol type="id" name="prev" />
+ <java-symbol type="id" name="rew" />
+ <java-symbol type="id" name="time" />
+ <java-symbol type="id" name="time_current" /-->
+</resources>
\ No newline at end of file
diff --git a/packages/MediaComponents/res/values/themes.xml b/packages/MediaComponents/res/values/themes.xml
new file mode 100644
index 0000000..51098e9
--- /dev/null
+++ b/packages/MediaComponents/res/values/themes.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<resources>
+
+ <style name="Theme.MediaRouter" parent="ThemeOverlay.AppCompat.Dark">
+ <item name="windowNoTitle">true</item>
+ <item name="mediaRouteButtonStyle">@style/Widget.MediaRouter.MediaRouteButton</item>
+
+ <item name="mediaRouteCloseDrawable">@drawable/mr_dialog_close_dark</item>
+ <item name="mediaRoutePlayDrawable">@drawable/mr_media_play_dark</item>
+ <item name="mediaRoutePauseDrawable">@drawable/mr_media_pause_dark</item>
+ <item name="mediaRouteStopDrawable">@drawable/mr_media_stop_dark</item>
+ <item name="mediaRouteAudioTrackDrawable">@drawable/mr_vol_type_audiotrack_dark</item>
+ <item name="mediaRouteDefaultIconDrawable">@drawable/ic_mr_button_disconnected_dark</item>
+ <item name="mediaRouteTvIconDrawable">@drawable/ic_vol_type_tv_dark</item>
+ <item name="mediaRouteSpeakerIconDrawable">@drawable/ic_vol_type_speaker_dark</item>
+ <item name="mediaRouteSpeakerGroupIconDrawable">@drawable/ic_vol_type_speaker_group_dark</item>
+
+ <item name="mediaRouteControlPanelThemeOverlay">@null</item>
+ </style>
+
+ <style name="Theme.MediaRouter.LightControlPanel">
+ <item name="mediaRouteControlPanelThemeOverlay">@style/ThemeOverlay.MediaRouter.Light</item>
+ </style>
+
+ <style name="Theme.MediaRouter.Light" parent="ThemeOverlay.AppCompat.Light">
+ <item name="windowNoTitle">true</item>
+ <item name="mediaRouteButtonStyle">@style/Widget.MediaRouter.Light.MediaRouteButton</item>
+
+ <item name="mediaRouteCloseDrawable">@drawable/mr_dialog_close_light</item>
+ <item name="mediaRoutePlayDrawable">@drawable/mr_media_play_light</item>
+ <item name="mediaRoutePauseDrawable">@drawable/mr_media_pause_light</item>
+ <item name="mediaRouteStopDrawable">@drawable/mr_media_stop_light</item>
+ <item name="mediaRouteAudioTrackDrawable">@drawable/mr_vol_type_audiotrack_light</item>
+ <item name="mediaRouteDefaultIconDrawable">@drawable/ic_mr_button_grey</item>
+ <item name="mediaRouteTvIconDrawable">@drawable/ic_vol_type_tv_light</item>
+ <item name="mediaRouteSpeakerIconDrawable">@drawable/ic_vol_type_speaker_light</item>
+ <item name="mediaRouteSpeakerGroupIconDrawable">@drawable/ic_vol_type_speaker_group_light</item>
+
+ <item name="mediaRouteControlPanelThemeOverlay">@null</item>
+ </style>
+
+ <style name="Theme.MediaRouter.Light.DarkControlPanel">
+ <item name="mediaRouteControlPanelThemeOverlay">@style/ThemeOverlay.MediaRouter.Dark</item>
+ </style>
+
+ <style name="ThemeOverlay.MediaRouter.Dark" parent="ThemeOverlay.AppCompat.Dark">
+ <item name="mediaRoutePlayDrawable">@drawable/mr_media_play_dark</item>
+ <item name="mediaRoutePauseDrawable">@drawable/mr_media_pause_dark</item>
+ <item name="mediaRouteStopDrawable">@drawable/mr_media_stop_dark</item>
+ <item name="mediaRouteAudioTrackDrawable">@drawable/mr_vol_type_audiotrack_dark</item>
+
+ </style>
+ <style name="ThemeOverlay.MediaRouter.Light" parent="ThemeOverlay.AppCompat.Light">
+ <item name="mediaRoutePlayDrawable">@drawable/mr_media_play_light</item>
+ <item name="mediaRoutePauseDrawable">@drawable/mr_media_pause_light</item>
+ <item name="mediaRouteStopDrawable">@drawable/mr_media_stop_light</item>
+ <item name="mediaRouteAudioTrackDrawable">@drawable/mr_vol_type_audiotrack_light</item>
+ </style>
+
+</resources>
diff --git a/packages/MediaComponents/src/com/android/media/update/ApiFactory.java b/packages/MediaComponents/src/com/android/media/update/ApiFactory.java
index abff13e..633a342 100644
--- a/packages/MediaComponents/src/com/android/media/update/ApiFactory.java
+++ b/packages/MediaComponents/src/com/android/media/update/ApiFactory.java
@@ -16,33 +16,37 @@
package com.android.media.update;
-import android.content.Context;
-import android.media.update.MediaController2Provider;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.media.update.MediaControlView2Provider;
import android.media.update.VideoView2Provider;
import android.media.update.StaticProvider;
import android.media.update.ViewProvider;
-import android.widget.MediaController2;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.widget.MediaControlView2;
import android.widget.VideoView2;
-import com.android.widget.MediaController2Impl;
+import com.android.widget.MediaControlView2Impl;
import com.android.widget.VideoView2Impl;
public class ApiFactory implements StaticProvider {
-
- public static Object initialize(Context appContext, Context libContext)
+ public static Object initialize(Resources libResources, Theme libTheme)
throws ReflectiveOperationException {
- ApiHelper.initialize(appContext, libContext);
+ ApiHelper.initialize(libResources, libTheme);
return new ApiFactory();
}
@Override
- public MediaController2Provider createMediaController2(
- MediaController2 instance, ViewProvider superProvider) {
- return new MediaController2Impl(instance, superProvider);
+ public MediaControlView2Provider createMediaControlView2(
+ MediaControlView2 instance, ViewProvider superProvider) {
+ return new MediaControlView2Impl(instance, superProvider);
}
@Override
- public VideoView2Provider createVideoView2(VideoView2 instance, ViewProvider superProvider) {
- return new VideoView2Impl(instance, superProvider);
+ public VideoView2Provider createVideoView2(
+ VideoView2 instance, ViewProvider superProvider,
+ @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ return new VideoView2Impl(instance, superProvider, attrs, defStyleAttr, defStyleRes);
}
}
diff --git a/packages/MediaComponents/src/com/android/media/update/ApiHelper.java b/packages/MediaComponents/src/com/android/media/update/ApiHelper.java
index 550da86..26f858c 100644
--- a/packages/MediaComponents/src/com/android/media/update/ApiHelper.java
+++ b/packages/MediaComponents/src/com/android/media/update/ApiHelper.java
@@ -16,36 +16,34 @@
package com.android.media.update;
-import android.content.Context;
import android.content.res.Resources;
+import android.content.res.Resources.Theme;
public class ApiHelper {
private static ApiHelper sInstance;
- private final Context mAppContext;
private final Resources mLibResources;
- private final Resources.Theme mLibTheme;
+ private final Theme mLibTheme;
public static ApiHelper getInstance() {
return sInstance;
}
- static void initialize(Context appContext, Context libContext) {
+ static void initialize(Resources libResources, Theme libTheme) {
if (sInstance == null) {
- sInstance = new ApiHelper(appContext, libContext);
+ sInstance = new ApiHelper(libResources, libTheme);
}
}
- private ApiHelper(Context appContext, Context libContext) {
- mAppContext = appContext;
- mLibResources = libContext.getResources();
- mLibTheme = libContext.getTheme();
+ private ApiHelper(Resources libResources, Theme libTheme) {
+ mLibResources = libResources;
+ mLibTheme = libTheme;
}
- public Resources getLibResources() {
- return mLibResources;
+ public static Resources getLibResources() {
+ return sInstance.mLibResources;
}
- public Resources.Theme getLibTheme() {
- return mLibTheme;
+ public static Resources.Theme getLibTheme() {
+ return sInstance.mLibTheme;
}
}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/api24/media/MediaRouterApi24.java b/packages/MediaComponents/src/com/android/support/mediarouter/api24/media/MediaRouterApi24.java
new file mode 100644
index 0000000..1146af6
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/api24/media/MediaRouterApi24.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.media;
+
+// @@RequiresApi(24)
+final class MediaRouterApi24 {
+ public static final class RouteInfo {
+ public static int getDeviceType(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getDeviceType();
+ }
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteActionProvider.java b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteActionProvider.java
new file mode 100644
index 0000000..d3e8d47
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteActionProvider.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.app;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.support.v4.view.ActionProvider;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.support.mediarouter.media.MediaRouteSelector;
+import com.android.support.mediarouter.media.MediaRouter;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * The media route action provider displays a {@link MediaRouteButton media route button}
+ * in the application's {@link ActionBar} to allow the user to select routes and
+ * to control the currently selected route.
+ * <p>
+ * The application must specify the kinds of routes that the user should be allowed
+ * to select by specifying a {@link MediaRouteSelector selector} with the
+ * {@link #setRouteSelector} method.
+ * </p><p>
+ * Refer to {@link MediaRouteButton} for a description of the button that will
+ * appear in the action bar menu. Note that instead of disabling the button
+ * when no routes are available, the action provider will instead make the
+ * menu item invisible. In this way, the button will only be visible when it
+ * is possible for the user to discover and select a matching route.
+ * </p>
+ *
+ * <h3>Prerequisites</h3>
+ * <p>
+ * To use the media route action provider, the activity must be a subclass of
+ * {@link AppCompatActivity} from the <code>android.support.v7.appcompat</code>
+ * support library. Refer to support library documentation for details.
+ * </p>
+ *
+ * <h3>Example</h3>
+ * <p>
+ * </p><p>
+ * The application should define a menu resource to include the provider in the
+ * action bar options menu. Note that the support library action bar uses attributes
+ * that are defined in the application's resource namespace rather than the framework's
+ * resource namespace to configure each item.
+ * </p><pre>
+ * <menu xmlns:android="http://schemas.android.com/apk/res/android"
+ * xmlns:app="http://schemas.android.com/apk/res-auto">
+ * <item android:id="@+id/media_route_menu_item"
+ * android:title="@string/media_route_menu_title"
+ * app:showAsAction="always"
+ * app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"/>
+ * </menu>
+ * </pre><p>
+ * Then configure the menu and set the route selector for the chooser.
+ * </p><pre>
+ * public class MyActivity extends AppCompatActivity {
+ * private MediaRouter mRouter;
+ * private MediaRouter.Callback mCallback;
+ * private MediaRouteSelector mSelector;
+ *
+ * protected void onCreate(Bundle savedInstanceState) {
+ * super.onCreate(savedInstanceState);
+ *
+ * mRouter = Mediarouter.getInstance(this);
+ * mSelector = new MediaRouteSelector.Builder()
+ * .addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
+ * .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
+ * .build();
+ * mCallback = new MyCallback();
+ * }
+ *
+ * // Add the callback on start to tell the media router what kinds of routes
+ * // the application is interested in so that it can try to discover suitable ones.
+ * public void onStart() {
+ * super.onStart();
+ *
+ * mediaRouter.addCallback(mSelector, mCallback,
+ * MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
+ *
+ * MediaRouter.RouteInfo route = mediaRouter.updateSelectedRoute(mSelector);
+ * // do something with the route...
+ * }
+ *
+ * // Remove the selector on stop to tell the media router that it no longer
+ * // needs to invest effort trying to discover routes of these kinds for now.
+ * public void onStop() {
+ * super.onStop();
+ *
+ * mediaRouter.removeCallback(mCallback);
+ * }
+ *
+ * public boolean onCreateOptionsMenu(Menu menu) {
+ * super.onCreateOptionsMenu(menu);
+ *
+ * getMenuInflater().inflate(R.menu.sample_media_router_menu, menu);
+ *
+ * MenuItem mediaRouteMenuItem = menu.findItem(R.id.media_route_menu_item);
+ * MediaRouteActionProvider mediaRouteActionProvider =
+ * (MediaRouteActionProvider)MenuItemCompat.getActionProvider(mediaRouteMenuItem);
+ * mediaRouteActionProvider.setRouteSelector(mSelector);
+ * return true;
+ * }
+ *
+ * private final class MyCallback extends MediaRouter.Callback {
+ * // Implement callback methods as needed.
+ * }
+ * }
+ * </pre>
+ *
+ * @see #setRouteSelector
+ */
+public class MediaRouteActionProvider extends ActionProvider {
+ private static final String TAG = "MediaRouteActionProvider";
+
+ private final MediaRouter mRouter;
+ private final MediaRouterCallback mCallback;
+
+ private MediaRouteSelector mSelector = MediaRouteSelector.EMPTY;
+ private MediaRouteDialogFactory mDialogFactory = MediaRouteDialogFactory.getDefault();
+ private MediaRouteButton mButton;
+
+ /**
+ * Creates the action provider.
+ *
+ * @param context The context.
+ */
+ public MediaRouteActionProvider(Context context) {
+ super(context);
+
+ mRouter = MediaRouter.getInstance(context);
+ mCallback = new MediaRouterCallback(this);
+ }
+
+ /**
+ * Gets the media route selector for filtering the routes that the user can
+ * select using the media route chooser dialog.
+ *
+ * @return The selector, never null.
+ */
+ @NonNull
+ public MediaRouteSelector getRouteSelector() {
+ return mSelector;
+ }
+
+ /**
+ * Sets the media route selector for filtering the routes that the user can
+ * select using the media route chooser dialog.
+ *
+ * @param selector The selector, must not be null.
+ */
+ public void setRouteSelector(@NonNull MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ if (!mSelector.equals(selector)) {
+ // FIXME: We currently have no way of knowing whether the action provider
+ // is still needed by the UI. Unfortunately this means the action provider
+ // may leak callbacks until garbage collection occurs. This may result in
+ // media route providers doing more work than necessary in the short term
+ // while trying to discover routes that are no longer of interest to the
+ // application. To solve this problem, the action provider will need some
+ // indication from the framework that it is being destroyed.
+ if (!mSelector.isEmpty()) {
+ mRouter.removeCallback(mCallback);
+ }
+ if (!selector.isEmpty()) {
+ mRouter.addCallback(selector, mCallback);
+ }
+ mSelector = selector;
+ refreshRoute();
+
+ if (mButton != null) {
+ mButton.setRouteSelector(selector);
+ }
+ }
+ }
+
+ /**
+ * Gets the media route dialog factory to use when showing the route chooser
+ * or controller dialog.
+ *
+ * @return The dialog factory, never null.
+ */
+ @NonNull
+ public MediaRouteDialogFactory getDialogFactory() {
+ return mDialogFactory;
+ }
+
+ /**
+ * Sets the media route dialog factory to use when showing the route chooser
+ * or controller dialog.
+ *
+ * @param factory The dialog factory, must not be null.
+ */
+ public void setDialogFactory(@NonNull MediaRouteDialogFactory factory) {
+ if (factory == null) {
+ throw new IllegalArgumentException("factory must not be null");
+ }
+
+ if (mDialogFactory != factory) {
+ mDialogFactory = factory;
+
+ if (mButton != null) {
+ mButton.setDialogFactory(factory);
+ }
+ }
+ }
+
+ /**
+ * Gets the associated media route button, or null if it has not yet been created.
+ */
+ @Nullable
+ public MediaRouteButton getMediaRouteButton() {
+ return mButton;
+ }
+
+ /**
+ * Called when the media route button is being created.
+ * <p>
+ * Subclasses may override this method to customize the button.
+ * </p>
+ */
+ public MediaRouteButton onCreateMediaRouteButton() {
+ return new MediaRouteButton(getContext());
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public View onCreateActionView() {
+ if (mButton != null) {
+ Log.e(TAG, "onCreateActionView: this ActionProvider is already associated " +
+ "with a menu item. Don't reuse MediaRouteActionProvider instances! " +
+ "Abandoning the old menu item...");
+ }
+
+ mButton = onCreateMediaRouteButton();
+ mButton.setCheatSheetEnabled(true);
+ mButton.setRouteSelector(mSelector);
+ mButton.setDialogFactory(mDialogFactory);
+ mButton.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ return mButton;
+ }
+
+ @Override
+ public boolean onPerformDefaultAction() {
+ if (mButton != null) {
+ return mButton.showDialog();
+ }
+ return false;
+ }
+
+ @Override
+ public boolean overridesItemVisibility() {
+ return true;
+ }
+
+ @Override
+ public boolean isVisible() {
+ return mRouter.isRouteAvailable(mSelector,
+ MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE);
+ }
+
+ void refreshRoute() {
+ refreshVisibility();
+ }
+
+ private static final class MediaRouterCallback extends MediaRouter.Callback {
+ private final WeakReference<MediaRouteActionProvider> mProviderWeak;
+
+ public MediaRouterCallback(MediaRouteActionProvider provider) {
+ mProviderWeak = new WeakReference<MediaRouteActionProvider>(provider);
+ }
+
+ @Override
+ public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoute(router);
+ }
+
+ @Override
+ public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoute(router);
+ }
+
+ @Override
+ public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoute(router);
+ }
+
+ @Override
+ public void onProviderAdded(MediaRouter router, MediaRouter.ProviderInfo provider) {
+ refreshRoute(router);
+ }
+
+ @Override
+ public void onProviderRemoved(MediaRouter router, MediaRouter.ProviderInfo provider) {
+ refreshRoute(router);
+ }
+
+ @Override
+ public void onProviderChanged(MediaRouter router, MediaRouter.ProviderInfo provider) {
+ refreshRoute(router);
+ }
+
+ private void refreshRoute(MediaRouter router) {
+ MediaRouteActionProvider provider = mProviderWeak.get();
+ if (provider != null) {
+ provider.refreshRoute();
+ } else {
+ router.removeCallback(this);
+ }
+ }
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteButton.java b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteButton.java
new file mode 100644
index 0000000..68e8d3a
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteButton.java
@@ -0,0 +1,614 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.app;
+
+import android.annotation.NonNull;
+import android.app.Activity;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.drawable.AnimationDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.graphics.drawable.DrawableCompat;
+import android.support.v7.widget.TooltipCompat;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.SoundEffectConstants;
+import android.view.View;
+
+import com.android.media.update.R;
+import com.android.support.mediarouter.media.MediaRouteSelector;
+import com.android.support.mediarouter.media.MediaRouter;
+
+/**
+ * The media route button allows the user to select routes and to control the
+ * currently selected route.
+ * <p>
+ * The application must specify the kinds of routes that the user should be allowed
+ * to select by specifying a {@link MediaRouteSelector selector} with the
+ * {@link #setRouteSelector} method.
+ * </p><p>
+ * When the default route is selected or when the currently selected route does not
+ * match the {@link #getRouteSelector() selector}, the button will appear in
+ * an inactive state indicating that the application is not connected to a
+ * route of the kind that it wants to use. Clicking on the button opens
+ * a {@link MediaRouteChooserDialog} to allow the user to select a route.
+ * If no non-default routes match the selector and it is not possible for an active
+ * scan to discover any matching routes, then the button is disabled and cannot
+ * be clicked.
+ * </p><p>
+ * When a non-default route is selected that matches the selector, the button will
+ * appear in an active state indicating that the application is connected
+ * to a route of the kind that it wants to use. The button may also appear
+ * in an intermediary connecting state if the route is in the process of connecting
+ * to the destination but has not yet completed doing so. In either case, clicking
+ * on the button opens a {@link MediaRouteControllerDialog} to allow the user
+ * to control or disconnect from the current route.
+ * </p>
+ *
+ * <h3>Prerequisites</h3>
+ * <p>
+ * To use the media route button, the activity must be a subclass of
+ * {@link FragmentActivity} from the <code>android.support.v4</code>
+ * support library. Refer to support library documentation for details.
+ * </p>
+ *
+ * @see MediaRouteActionProvider
+ * @see #setRouteSelector
+ */
+public class MediaRouteButton extends View {
+ private static final String TAG = "MediaRouteButton";
+
+ private static final String CHOOSER_FRAGMENT_TAG =
+ "android.support.v7.mediarouter:MediaRouteChooserDialogFragment";
+ private static final String CONTROLLER_FRAGMENT_TAG =
+ "android.support.v7.mediarouter:MediaRouteControllerDialogFragment";
+
+ private final MediaRouter mRouter;
+ private final MediaRouterCallback mCallback;
+
+ private MediaRouteSelector mSelector = MediaRouteSelector.EMPTY;
+ private MediaRouteDialogFactory mDialogFactory = MediaRouteDialogFactory.getDefault();
+
+ private boolean mAttachedToWindow;
+
+ private static final SparseArray<Drawable.ConstantState> sRemoteIndicatorCache =
+ new SparseArray<>(2);
+ private RemoteIndicatorLoader mRemoteIndicatorLoader;
+ private Drawable mRemoteIndicator;
+ private boolean mRemoteActive;
+ private boolean mIsConnecting;
+
+ private ColorStateList mButtonTint;
+ private int mMinWidth;
+ private int mMinHeight;
+
+ // The checked state is used when connected to a remote route.
+ private static final int[] CHECKED_STATE_SET = {
+ android.R.attr.state_checked
+ };
+
+ // The checkable state is used while connecting to a remote route.
+ private static final int[] CHECKABLE_STATE_SET = {
+ android.R.attr.state_checkable
+ };
+
+ public MediaRouteButton(Context context) {
+ this(context, null);
+ }
+
+ public MediaRouteButton(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.mediaRouteButtonStyle);
+ }
+
+ public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(MediaRouterThemeHelper.createThemedButtonContext(context), attrs, defStyleAttr);
+ context = getContext();
+
+ mRouter = MediaRouter.getInstance(context);
+ mCallback = new MediaRouterCallback();
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.MediaRouteButton, defStyleAttr, 0);
+ mButtonTint = a.getColorStateList(R.styleable.MediaRouteButton_mediaRouteButtonTint);
+ mMinWidth = a.getDimensionPixelSize(
+ R.styleable.MediaRouteButton_android_minWidth, 0);
+ mMinHeight = a.getDimensionPixelSize(
+ R.styleable.MediaRouteButton_android_minHeight, 0);
+ int remoteIndicatorResId = a.getResourceId(
+ R.styleable.MediaRouteButton_externalRouteEnabledDrawable, 0);
+ a.recycle();
+
+ if (remoteIndicatorResId != 0) {
+ Drawable.ConstantState remoteIndicatorState =
+ sRemoteIndicatorCache.get(remoteIndicatorResId);
+ if (remoteIndicatorState != null) {
+ setRemoteIndicatorDrawable(remoteIndicatorState.newDrawable());
+ } else {
+ mRemoteIndicatorLoader = new RemoteIndicatorLoader(remoteIndicatorResId);
+ mRemoteIndicatorLoader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ }
+
+ updateContentDescription();
+ setClickable(true);
+ }
+
+ /**
+ * Gets the media route selector for filtering the routes that the user can
+ * select using the media route chooser dialog.
+ *
+ * @return The selector, never null.
+ */
+ @NonNull
+ public MediaRouteSelector getRouteSelector() {
+ return mSelector;
+ }
+
+ /**
+ * Sets the media route selector for filtering the routes that the user can
+ * select using the media route chooser dialog.
+ *
+ * @param selector The selector, must not be null.
+ */
+ public void setRouteSelector(MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ if (!mSelector.equals(selector)) {
+ if (mAttachedToWindow) {
+ if (!mSelector.isEmpty()) {
+ mRouter.removeCallback(mCallback);
+ }
+ if (!selector.isEmpty()) {
+ mRouter.addCallback(selector, mCallback);
+ }
+ }
+ mSelector = selector;
+ refreshRoute();
+ }
+ }
+
+ /**
+ * Gets the media route dialog factory to use when showing the route chooser
+ * or controller dialog.
+ *
+ * @return The dialog factory, never null.
+ */
+ @NonNull
+ public MediaRouteDialogFactory getDialogFactory() {
+ return mDialogFactory;
+ }
+
+ /**
+ * Sets the media route dialog factory to use when showing the route chooser
+ * or controller dialog.
+ *
+ * @param factory The dialog factory, must not be null.
+ */
+ public void setDialogFactory(@NonNull MediaRouteDialogFactory factory) {
+ if (factory == null) {
+ throw new IllegalArgumentException("factory must not be null");
+ }
+
+ mDialogFactory = factory;
+ }
+
+ /**
+ * Show the route chooser or controller dialog.
+ * <p>
+ * If the default route is selected or if the currently selected route does
+ * not match the {@link #getRouteSelector selector}, then shows the route chooser dialog.
+ * Otherwise, shows the route controller dialog to offer the user
+ * a choice to disconnect from the route or perform other control actions
+ * such as setting the route's volume.
+ * </p><p>
+ * The application can customize the dialogs by calling {@link #setDialogFactory}
+ * to provide a customized dialog factory.
+ * </p>
+ *
+ * @return True if the dialog was actually shown.
+ *
+ * @throws IllegalStateException if the activity is not a subclass of
+ * {@link FragmentActivity}.
+ */
+ public boolean showDialog() {
+ if (!mAttachedToWindow) {
+ return false;
+ }
+
+ final FragmentManager fm = getFragmentManager();
+ if (fm == null) {
+ throw new IllegalStateException("The activity must be a subclass of FragmentActivity");
+ }
+
+ MediaRouter.RouteInfo route = mRouter.getSelectedRoute();
+ if (route.isDefaultOrBluetooth() || !route.matchesSelector(mSelector)) {
+ if (fm.findFragmentByTag(CHOOSER_FRAGMENT_TAG) != null) {
+ Log.w(TAG, "showDialog(): Route chooser dialog already showing!");
+ return false;
+ }
+ MediaRouteChooserDialogFragment f =
+ mDialogFactory.onCreateChooserDialogFragment();
+ f.setRouteSelector(mSelector);
+ f.show(fm, CHOOSER_FRAGMENT_TAG);
+ } else {
+ if (fm.findFragmentByTag(CONTROLLER_FRAGMENT_TAG) != null) {
+ Log.w(TAG, "showDialog(): Route controller dialog already showing!");
+ return false;
+ }
+ MediaRouteControllerDialogFragment f =
+ mDialogFactory.onCreateControllerDialogFragment();
+ f.show(fm, CONTROLLER_FRAGMENT_TAG);
+ }
+ return true;
+ }
+
+ private FragmentManager getFragmentManager() {
+ Activity activity = getActivity();
+ if (activity instanceof FragmentActivity) {
+ return ((FragmentActivity)activity).getSupportFragmentManager();
+ }
+ return null;
+ }
+
+ private Activity getActivity() {
+ // Gross way of unwrapping the Activity so we can get the FragmentManager
+ Context context = getContext();
+ while (context instanceof ContextWrapper) {
+ if (context instanceof Activity) {
+ return (Activity)context;
+ }
+ context = ((ContextWrapper)context).getBaseContext();
+ }
+ return null;
+ }
+
+ /**
+ * Sets whether to enable showing a toast with the content descriptor of the
+ * button when the button is long pressed.
+ */
+ void setCheatSheetEnabled(boolean enable) {
+ TooltipCompat.setTooltipText(this,
+ enable ? getContext().getString(R.string.mr_button_content_description) : null);
+ }
+
+ @Override
+ public boolean performClick() {
+ // Send the appropriate accessibility events and call listeners
+ boolean handled = super.performClick();
+ if (!handled) {
+ playSoundEffect(SoundEffectConstants.CLICK);
+ }
+ return showDialog() || handled;
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+
+ // Technically we should be handling this more completely, but these
+ // are implementation details here. Checkable is used to express the connecting
+ // drawable state and it's mutually exclusive with check for the purposes
+ // of state selection here.
+ if (mIsConnecting) {
+ mergeDrawableStates(drawableState, CHECKABLE_STATE_SET);
+ } else if (mRemoteActive) {
+ mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+ }
+ return drawableState;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+
+ if (mRemoteIndicator != null) {
+ int[] myDrawableState = getDrawableState();
+ mRemoteIndicator.setState(myDrawableState);
+ invalidate();
+ }
+ }
+
+ /**
+ * Sets a drawable to use as the remote route indicator.
+ */
+ public void setRemoteIndicatorDrawable(Drawable d) {
+ if (mRemoteIndicatorLoader != null) {
+ mRemoteIndicatorLoader.cancel(false);
+ }
+
+ if (mRemoteIndicator != null) {
+ mRemoteIndicator.setCallback(null);
+ unscheduleDrawable(mRemoteIndicator);
+ }
+ if (d != null) {
+ if (mButtonTint != null) {
+ d = DrawableCompat.wrap(d.mutate());
+ DrawableCompat.setTintList(d, mButtonTint);
+ }
+ d.setCallback(this);
+ d.setState(getDrawableState());
+ d.setVisible(getVisibility() == VISIBLE, false);
+ }
+ mRemoteIndicator = d;
+
+ refreshDrawableState();
+ if (mAttachedToWindow && mRemoteIndicator != null
+ && mRemoteIndicator.getCurrent() instanceof AnimationDrawable) {
+ AnimationDrawable curDrawable = (AnimationDrawable) mRemoteIndicator.getCurrent();
+ if (mIsConnecting) {
+ if (!curDrawable.isRunning()) {
+ curDrawable.start();
+ }
+ } else if (mRemoteActive) {
+ if (curDrawable.isRunning()) {
+ curDrawable.stop();
+ }
+ curDrawable.selectDrawable(curDrawable.getNumberOfFrames() - 1);
+ }
+ }
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return super.verifyDrawable(who) || who == mRemoteIndicator;
+ }
+
+ @Override
+ public void jumpDrawablesToCurrentState() {
+ // We can't call super to handle the background so we do it ourselves.
+ //super.jumpDrawablesToCurrentState();
+ if (getBackground() != null) {
+ DrawableCompat.jumpToCurrentState(getBackground());
+ }
+
+ // Handle our own remote indicator.
+ if (mRemoteIndicator != null) {
+ DrawableCompat.jumpToCurrentState(mRemoteIndicator);
+ }
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+
+ if (mRemoteIndicator != null) {
+ mRemoteIndicator.setVisible(getVisibility() == VISIBLE, false);
+ }
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ mAttachedToWindow = true;
+ if (!mSelector.isEmpty()) {
+ mRouter.addCallback(mSelector, mCallback);
+ }
+ refreshRoute();
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ mAttachedToWindow = false;
+ if (!mSelector.isEmpty()) {
+ mRouter.removeCallback(mCallback);
+ }
+
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+
+ final int width = Math.max(mMinWidth, mRemoteIndicator != null ?
+ mRemoteIndicator.getIntrinsicWidth() + getPaddingLeft() + getPaddingRight() : 0);
+ final int height = Math.max(mMinHeight, mRemoteIndicator != null ?
+ mRemoteIndicator.getIntrinsicHeight() + getPaddingTop() + getPaddingBottom() : 0);
+
+ int measuredWidth;
+ switch (widthMode) {
+ case MeasureSpec.EXACTLY:
+ measuredWidth = widthSize;
+ break;
+ case MeasureSpec.AT_MOST:
+ measuredWidth = Math.min(widthSize, width);
+ break;
+ default:
+ case MeasureSpec.UNSPECIFIED:
+ measuredWidth = width;
+ break;
+ }
+
+ int measuredHeight;
+ switch (heightMode) {
+ case MeasureSpec.EXACTLY:
+ measuredHeight = heightSize;
+ break;
+ case MeasureSpec.AT_MOST:
+ measuredHeight = Math.min(heightSize, height);
+ break;
+ default:
+ case MeasureSpec.UNSPECIFIED:
+ measuredHeight = height;
+ break;
+ }
+
+ setMeasuredDimension(measuredWidth, measuredHeight);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (mRemoteIndicator != null) {
+ final int left = getPaddingLeft();
+ final int right = getWidth() - getPaddingRight();
+ final int top = getPaddingTop();
+ final int bottom = getHeight() - getPaddingBottom();
+
+ final int drawWidth = mRemoteIndicator.getIntrinsicWidth();
+ final int drawHeight = mRemoteIndicator.getIntrinsicHeight();
+ final int drawLeft = left + (right - left - drawWidth) / 2;
+ final int drawTop = top + (bottom - top - drawHeight) / 2;
+
+ mRemoteIndicator.setBounds(drawLeft, drawTop,
+ drawLeft + drawWidth, drawTop + drawHeight);
+ mRemoteIndicator.draw(canvas);
+ }
+ }
+
+ void refreshRoute() {
+ final MediaRouter.RouteInfo route = mRouter.getSelectedRoute();
+ final boolean isRemote = !route.isDefaultOrBluetooth() && route.matchesSelector(mSelector);
+ final boolean isConnecting = isRemote && route.isConnecting();
+ boolean needsRefresh = false;
+ if (mRemoteActive != isRemote) {
+ mRemoteActive = isRemote;
+ needsRefresh = true;
+ }
+ if (mIsConnecting != isConnecting) {
+ mIsConnecting = isConnecting;
+ needsRefresh = true;
+ }
+
+ if (needsRefresh) {
+ updateContentDescription();
+ refreshDrawableState();
+ }
+ if (mAttachedToWindow) {
+ setEnabled(mRouter.isRouteAvailable(mSelector,
+ MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE));
+ }
+ if (mRemoteIndicator != null
+ && mRemoteIndicator.getCurrent() instanceof AnimationDrawable) {
+ AnimationDrawable curDrawable = (AnimationDrawable) mRemoteIndicator.getCurrent();
+ if (mAttachedToWindow) {
+ if ((needsRefresh || isConnecting) && !curDrawable.isRunning()) {
+ curDrawable.start();
+ }
+ } else if (isRemote && !isConnecting) {
+ // When the route is already connected before the view is attached, show the last
+ // frame of the connected animation immediately.
+ if (curDrawable.isRunning()) {
+ curDrawable.stop();
+ }
+ curDrawable.selectDrawable(curDrawable.getNumberOfFrames() - 1);
+ }
+ }
+ }
+
+ private void updateContentDescription() {
+ int resId;
+ if (mIsConnecting) {
+ resId = R.string.mr_cast_button_connecting;
+ } else if (mRemoteActive) {
+ resId = R.string.mr_cast_button_connected;
+ } else {
+ resId = R.string.mr_cast_button_disconnected;
+ }
+ setContentDescription(getContext().getString(resId));
+ }
+
+ private final class MediaRouterCallback extends MediaRouter.Callback {
+ MediaRouterCallback() {
+ }
+
+ @Override
+ public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoute();
+ }
+
+ @Override
+ public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoute();
+ }
+
+ @Override
+ public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoute();
+ }
+
+ @Override
+ public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoute();
+ }
+
+ @Override
+ public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoute();
+ }
+
+ @Override
+ public void onProviderAdded(MediaRouter router, MediaRouter.ProviderInfo provider) {
+ refreshRoute();
+ }
+
+ @Override
+ public void onProviderRemoved(MediaRouter router, MediaRouter.ProviderInfo provider) {
+ refreshRoute();
+ }
+
+ @Override
+ public void onProviderChanged(MediaRouter router, MediaRouter.ProviderInfo provider) {
+ refreshRoute();
+ }
+ }
+
+ private final class RemoteIndicatorLoader extends AsyncTask<Void, Void, Drawable> {
+ private final int mResId;
+
+ RemoteIndicatorLoader(int resId) {
+ mResId = resId;
+ }
+
+ @Override
+ protected Drawable doInBackground(Void... params) {
+ return getContext().getResources().getDrawable(mResId);
+ }
+
+ @Override
+ protected void onPostExecute(Drawable remoteIndicator) {
+ cacheAndReset(remoteIndicator);
+ setRemoteIndicatorDrawable(remoteIndicator);
+ }
+
+ @Override
+ protected void onCancelled(Drawable remoteIndicator) {
+ cacheAndReset(remoteIndicator);
+ }
+
+ private void cacheAndReset(Drawable remoteIndicator) {
+ if (remoteIndicator != null) {
+ sRemoteIndicatorCache.put(mResId, remoteIndicator.getConstantState());
+ }
+ mRemoteIndicatorLoader = null;
+ }
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteChooserDialog.java b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteChooserDialog.java
new file mode 100644
index 0000000..cc7c3d5
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteChooserDialog.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.app;
+
+import static com.android.support.mediarouter.media.MediaRouter.RouteInfo.CONNECTION_STATE_CONNECTED;
+import static com.android.support.mediarouter.media.MediaRouter.RouteInfo.CONNECTION_STATE_CONNECTING;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.support.v7.app.AppCompatDialog;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Gravity;
+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 com.android.media.update.R;
+import com.android.support.mediarouter.media.MediaRouteSelector;
+import com.android.support.mediarouter.media.MediaRouter;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * This class implements the route chooser dialog for {@link MediaRouter}.
+ * <p>
+ * This dialog allows the user to choose a route that matches a given selector.
+ * </p>
+ *
+ * @see MediaRouteButton
+ * @see MediaRouteActionProvider
+ */
+public class MediaRouteChooserDialog extends AppCompatDialog {
+ static final String TAG = "MediaRouteChooserDialog";
+
+ // Do not update the route list immediately to avoid unnatural dialog change.
+ private static final long UPDATE_ROUTES_DELAY_MS = 300L;
+ static final int MSG_UPDATE_ROUTES = 1;
+
+ private final MediaRouter mRouter;
+ private final MediaRouterCallback mCallback;
+
+ private TextView mTitleView;
+ private MediaRouteSelector mSelector = MediaRouteSelector.EMPTY;
+ private ArrayList<MediaRouter.RouteInfo> mRoutes;
+ private RouteAdapter mAdapter;
+ private ListView mListView;
+ private boolean mAttachedToWindow;
+ private long mLastUpdateTime;
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_UPDATE_ROUTES:
+ updateRoutes((List<MediaRouter.RouteInfo>) message.obj);
+ break;
+ }
+ }
+ };
+
+ public MediaRouteChooserDialog(Context context) {
+ this(context, 0);
+ }
+
+ public MediaRouteChooserDialog(Context context, int theme) {
+ super(context = MediaRouterThemeHelper.createThemedDialogContext(context, theme, false),
+ MediaRouterThemeHelper.createThemedDialogStyle(context));
+ context = getContext();
+
+ mRouter = MediaRouter.getInstance(context);
+ mCallback = new MediaRouterCallback();
+ }
+
+ /**
+ * Gets the media route selector for filtering the routes that the user can select.
+ *
+ * @return The selector, never null.
+ */
+ @NonNull
+ public MediaRouteSelector getRouteSelector() {
+ return mSelector;
+ }
+
+ /**
+ * Sets the media route selector for filtering the routes that the user can select.
+ *
+ * @param selector The selector, must not be null.
+ */
+ public void setRouteSelector(@NonNull MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ if (!mSelector.equals(selector)) {
+ mSelector = selector;
+
+ if (mAttachedToWindow) {
+ mRouter.removeCallback(mCallback);
+ mRouter.addCallback(selector, mCallback,
+ MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
+ }
+
+ refreshRoutes();
+ }
+ }
+
+ /**
+ * Called to filter the set of routes that should be included in the list.
+ * <p>
+ * The default implementation iterates over all routes in the provided list and
+ * removes those for which {@link #onFilterRoute} returns false.
+ * </p>
+ *
+ * @param routes The list of routes to filter in-place, never null.
+ */
+ public void onFilterRoutes(@NonNull List<MediaRouter.RouteInfo> routes) {
+ for (int i = routes.size(); i-- > 0; ) {
+ if (!onFilterRoute(routes.get(i))) {
+ routes.remove(i);
+ }
+ }
+ }
+
+ /**
+ * Returns true if the route should be included in the list.
+ * <p>
+ * The default implementation returns true for enabled non-default routes that
+ * match the selector. Subclasses can override this method to filter routes
+ * differently.
+ * </p>
+ *
+ * @param route The route to consider, never null.
+ * @return True if the route should be included in the chooser dialog.
+ */
+ public boolean onFilterRoute(@NonNull MediaRouter.RouteInfo route) {
+ return !route.isDefaultOrBluetooth() && route.isEnabled()
+ && route.matchesSelector(mSelector);
+ }
+
+ @Override
+ public void setTitle(CharSequence title) {
+ mTitleView.setText(title);
+ }
+
+ @Override
+ public void setTitle(int titleId) {
+ mTitleView.setText(titleId);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.mr_chooser_dialog);
+
+ mRoutes = new ArrayList<>();
+ mAdapter = new RouteAdapter(getContext(), mRoutes);
+ mListView = (ListView)findViewById(R.id.mr_chooser_list);
+ mListView.setAdapter(mAdapter);
+ mListView.setOnItemClickListener(mAdapter);
+ mListView.setEmptyView(findViewById(android.R.id.empty));
+ mTitleView = findViewById(R.id.mr_chooser_title);
+
+ updateLayout();
+ }
+
+ /**
+ * Sets the width of the dialog. Also called when configuration changes.
+ */
+ void updateLayout() {
+ getWindow().setLayout(MediaRouteDialogHelper.getDialogWidth(getContext()),
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ mAttachedToWindow = true;
+ mRouter.addCallback(mSelector, mCallback, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
+ refreshRoutes();
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ mAttachedToWindow = false;
+ mRouter.removeCallback(mCallback);
+ mHandler.removeMessages(MSG_UPDATE_ROUTES);
+
+ super.onDetachedFromWindow();
+ }
+
+ /**
+ * Refreshes the list of routes that are shown in the chooser dialog.
+ */
+ public void refreshRoutes() {
+ if (mAttachedToWindow) {
+ ArrayList<MediaRouter.RouteInfo> routes = new ArrayList<>(mRouter.getRoutes());
+ onFilterRoutes(routes);
+ Collections.sort(routes, RouteComparator.sInstance);
+ if (SystemClock.uptimeMillis() - mLastUpdateTime >= UPDATE_ROUTES_DELAY_MS) {
+ updateRoutes(routes);
+ } else {
+ mHandler.removeMessages(MSG_UPDATE_ROUTES);
+ mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_UPDATE_ROUTES, routes),
+ mLastUpdateTime + UPDATE_ROUTES_DELAY_MS);
+ }
+ }
+ }
+
+ void updateRoutes(List<MediaRouter.RouteInfo> routes) {
+ mLastUpdateTime = SystemClock.uptimeMillis();
+ mRoutes.clear();
+ mRoutes.addAll(routes);
+ mAdapter.notifyDataSetChanged();
+ }
+
+ private final class RouteAdapter extends ArrayAdapter<MediaRouter.RouteInfo>
+ implements ListView.OnItemClickListener {
+ private final LayoutInflater mInflater;
+ private final Drawable mDefaultIcon;
+ private final Drawable mTvIcon;
+ private final Drawable mSpeakerIcon;
+ private final Drawable mSpeakerGroupIcon;
+
+ public RouteAdapter(Context context, List<MediaRouter.RouteInfo> routes) {
+ super(context, 0, routes);
+ mInflater = LayoutInflater.from(context);
+ TypedArray styledAttributes = getContext().obtainStyledAttributes(new int[] {
+ R.attr.mediaRouteDefaultIconDrawable,
+ R.attr.mediaRouteTvIconDrawable,
+ R.attr.mediaRouteSpeakerIconDrawable,
+ R.attr.mediaRouteSpeakerGroupIconDrawable});
+ mDefaultIcon = styledAttributes.getDrawable(0);
+ mTvIcon = styledAttributes.getDrawable(1);
+ mSpeakerIcon = styledAttributes.getDrawable(2);
+ mSpeakerGroupIcon = styledAttributes.getDrawable(3);
+ styledAttributes.recycle();
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return getItem(position).isEnabled();
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view = convertView;
+ if (view == null) {
+ view = mInflater.inflate(R.layout.mr_chooser_list_item, parent, false);
+ }
+
+ MediaRouter.RouteInfo route = getItem(position);
+ TextView text1 = (TextView) view.findViewById(R.id.mr_chooser_route_name);
+ TextView text2 = (TextView) view.findViewById(R.id.mr_chooser_route_desc);
+ text1.setText(route.getName());
+ String description = route.getDescription();
+ boolean isConnectedOrConnecting =
+ route.getConnectionState() == CONNECTION_STATE_CONNECTED
+ || route.getConnectionState() == CONNECTION_STATE_CONNECTING;
+ if (isConnectedOrConnecting && !TextUtils.isEmpty(description)) {
+ text1.setGravity(Gravity.BOTTOM);
+ text2.setVisibility(View.VISIBLE);
+ text2.setText(description);
+ } else {
+ text1.setGravity(Gravity.CENTER_VERTICAL);
+ text2.setVisibility(View.GONE);
+ text2.setText("");
+ }
+ view.setEnabled(route.isEnabled());
+
+ ImageView iconView = (ImageView) view.findViewById(R.id.mr_chooser_route_icon);
+ if (iconView != null) {
+ iconView.setImageDrawable(getIconDrawable(route));
+ }
+ return view;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ MediaRouter.RouteInfo route = getItem(position);
+ if (route.isEnabled()) {
+ route.select();
+ dismiss();
+ }
+ }
+
+ private Drawable getIconDrawable(MediaRouter.RouteInfo route) {
+ Uri iconUri = route.getIconUri();
+ if (iconUri != null) {
+ try {
+ InputStream is = getContext().getContentResolver().openInputStream(iconUri);
+ Drawable drawable = Drawable.createFromStream(is, null);
+ if (drawable != null) {
+ return drawable;
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to load " + iconUri, e);
+ // Falls back.
+ }
+ }
+ return getDefaultIconDrawable(route);
+ }
+
+ private Drawable getDefaultIconDrawable(MediaRouter.RouteInfo route) {
+ // If the type of the receiver device is specified, use it.
+ switch (route.getDeviceType()) {
+ case MediaRouter.RouteInfo.DEVICE_TYPE_TV:
+ return mTvIcon;
+ case MediaRouter.RouteInfo.DEVICE_TYPE_SPEAKER:
+ return mSpeakerIcon;
+ }
+
+ // Otherwise, make the best guess based on other route information.
+ if (route instanceof MediaRouter.RouteGroup) {
+ // Only speakers can be grouped for now.
+ return mSpeakerGroupIcon;
+ }
+ return mDefaultIcon;
+ }
+ }
+
+ private final class MediaRouterCallback extends MediaRouter.Callback {
+ MediaRouterCallback() {
+ }
+
+ @Override
+ public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoutes();
+ }
+
+ @Override
+ public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoutes();
+ }
+
+ @Override
+ public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoutes();
+ }
+
+ @Override
+ public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
+ dismiss();
+ }
+ }
+
+ static final class RouteComparator implements Comparator<MediaRouter.RouteInfo> {
+ public static final RouteComparator sInstance = new RouteComparator();
+
+ @Override
+ public int compare(MediaRouter.RouteInfo lhs, MediaRouter.RouteInfo rhs) {
+ return lhs.getName().compareToIgnoreCase(rhs.getName());
+ }
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteChooserDialogFragment.java b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteChooserDialogFragment.java
new file mode 100644
index 0000000..2f85fb3
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteChooserDialogFragment.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.app;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+
+import com.android.support.mediarouter.media.MediaRouteSelector;
+
+/**
+ * Media route chooser dialog fragment.
+ * <p>
+ * Creates a {@link MediaRouteChooserDialog}. The application may subclass
+ * this dialog fragment to customize the media route chooser dialog.
+ * </p>
+ */
+public class MediaRouteChooserDialogFragment extends DialogFragment {
+ private final String ARGUMENT_SELECTOR = "selector";
+
+ private MediaRouteChooserDialog mDialog;
+ private MediaRouteSelector mSelector;
+
+ /**
+ * Creates a media route chooser dialog fragment.
+ * <p>
+ * All subclasses of this class must also possess a default constructor.
+ * </p>
+ */
+ public MediaRouteChooserDialogFragment() {
+ setCancelable(true);
+ }
+
+ /**
+ * Gets the media route selector for filtering the routes that the user can select.
+ *
+ * @return The selector, never null.
+ */
+ public MediaRouteSelector getRouteSelector() {
+ ensureRouteSelector();
+ return mSelector;
+ }
+
+ private void ensureRouteSelector() {
+ if (mSelector == null) {
+ Bundle args = getArguments();
+ if (args != null) {
+ mSelector = MediaRouteSelector.fromBundle(args.getBundle(ARGUMENT_SELECTOR));
+ }
+ if (mSelector == null) {
+ mSelector = MediaRouteSelector.EMPTY;
+ }
+ }
+ }
+
+ /**
+ * Sets the media route selector for filtering the routes that the user can select.
+ * This method must be called before the fragment is added.
+ *
+ * @param selector The selector to set.
+ */
+ public void setRouteSelector(MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ ensureRouteSelector();
+ if (!mSelector.equals(selector)) {
+ mSelector = selector;
+
+ Bundle args = getArguments();
+ if (args == null) {
+ args = new Bundle();
+ }
+ args.putBundle(ARGUMENT_SELECTOR, selector.asBundle());
+ setArguments(args);
+
+ MediaRouteChooserDialog dialog = (MediaRouteChooserDialog)getDialog();
+ if (dialog != null) {
+ dialog.setRouteSelector(selector);
+ }
+ }
+ }
+
+ /**
+ * Called when the chooser dialog is being created.
+ * <p>
+ * Subclasses may override this method to customize the dialog.
+ * </p>
+ */
+ public MediaRouteChooserDialog onCreateChooserDialog(
+ Context context, Bundle savedInstanceState) {
+ return new MediaRouteChooserDialog(context);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ mDialog = onCreateChooserDialog(getContext(), savedInstanceState);
+ mDialog.setRouteSelector(getRouteSelector());
+ return mDialog;
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ if (mDialog != null) {
+ mDialog.updateLayout();
+ }
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteControllerDialog.java b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteControllerDialog.java
new file mode 100644
index 0000000..942797b
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteControllerDialog.java
@@ -0,0 +1,1481 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.app;
+
+import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PAUSE;
+import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY;
+import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_PAUSE;
+import static android.support.v4.media.session.PlaybackStateCompat.ACTION_STOP;
+
+import android.app.PendingIntent;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.support.v4.util.ObjectsCompat;
+import android.support.v4.view.accessibility.AccessibilityEventCompat;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.graphics.Palette;
+
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.view.animation.Transformation;
+import android.view.animation.TranslateAnimation;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import com.android.media.update.R;
+import com.android.support.mediarouter.media.MediaRouteSelector;
+import com.android.support.mediarouter.media.MediaRouter;
+import com.android.support.mediarouter.app.OverlayListView.OverlayObject;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This class implements the route controller dialog for {@link MediaRouter}.
+ * <p>
+ * This dialog allows the user to control or disconnect from the currently selected route.
+ * </p>
+ *
+ * @see MediaRouteButton
+ * @see MediaRouteActionProvider
+ */
+public class MediaRouteControllerDialog extends AlertDialog {
+ // Tags should be less than 24 characters long (see docs for android.util.Log.isLoggable())
+ static final String TAG = "MediaRouteCtrlDialog";
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ // Time to wait before updating the volume when the user lets go of the seek bar
+ // to allow the route provider time to propagate the change and publish a new
+ // route descriptor.
+ static final int VOLUME_UPDATE_DELAY_MILLIS = 500;
+ static final int CONNECTION_TIMEOUT_MILLIS = (int) TimeUnit.SECONDS.toMillis(30L);
+
+ private static final int BUTTON_NEUTRAL_RES_ID = android.R.id.button3;
+ static final int BUTTON_DISCONNECT_RES_ID = android.R.id.button2;
+ static final int BUTTON_STOP_RES_ID = android.R.id.button1;
+
+ final MediaRouter mRouter;
+ private final MediaRouterCallback mCallback;
+ final MediaRouter.RouteInfo mRoute;
+
+ Context mContext;
+ private boolean mCreated;
+ private boolean mAttachedToWindow;
+
+ private int mDialogContentWidth;
+
+ private View mCustomControlView;
+
+ private Button mDisconnectButton;
+ private Button mStopCastingButton;
+ private ImageButton mPlaybackControlButton;
+ private ImageButton mCloseButton;
+ private MediaRouteExpandCollapseButton mGroupExpandCollapseButton;
+
+ private FrameLayout mExpandableAreaLayout;
+ private LinearLayout mDialogAreaLayout;
+ FrameLayout mDefaultControlLayout;
+ private FrameLayout mCustomControlLayout;
+ private ImageView mArtView;
+ private TextView mTitleView;
+ private TextView mSubtitleView;
+ private TextView mRouteNameTextView;
+
+ private boolean mVolumeControlEnabled = true;
+ // Layout for media controllers including play/pause button and the main volume slider.
+ private LinearLayout mMediaMainControlLayout;
+ private RelativeLayout mPlaybackControlLayout;
+ private LinearLayout mVolumeControlLayout;
+ private View mDividerView;
+
+ OverlayListView mVolumeGroupList;
+ VolumeGroupAdapter mVolumeGroupAdapter;
+ private List<MediaRouter.RouteInfo> mGroupMemberRoutes;
+ Set<MediaRouter.RouteInfo> mGroupMemberRoutesAdded;
+ private Set<MediaRouter.RouteInfo> mGroupMemberRoutesRemoved;
+ Set<MediaRouter.RouteInfo> mGroupMemberRoutesAnimatingWithBitmap;
+ SeekBar mVolumeSlider;
+ VolumeChangeListener mVolumeChangeListener;
+ MediaRouter.RouteInfo mRouteInVolumeSliderTouched;
+ private int mVolumeGroupListItemIconSize;
+ private int mVolumeGroupListItemHeight;
+ private int mVolumeGroupListMaxHeight;
+ private final int mVolumeGroupListPaddingTop;
+ Map<MediaRouter.RouteInfo, SeekBar> mVolumeSliderMap;
+
+ MediaControllerCompat mMediaController;
+ MediaControllerCallback mControllerCallback;
+ PlaybackStateCompat mState;
+ MediaDescriptionCompat mDescription;
+
+ FetchArtTask mFetchArtTask;
+ Bitmap mArtIconBitmap;
+ Uri mArtIconUri;
+ boolean mArtIconIsLoaded;
+ Bitmap mArtIconLoadedBitmap;
+ int mArtIconBackgroundColor;
+
+ boolean mHasPendingUpdate;
+ boolean mPendingUpdateAnimationNeeded;
+
+ boolean mIsGroupExpanded;
+ boolean mIsGroupListAnimating;
+ boolean mIsGroupListAnimationPending;
+ int mGroupListAnimationDurationMs;
+ private int mGroupListFadeInDurationMs;
+ private int mGroupListFadeOutDurationMs;
+
+ private Interpolator mInterpolator;
+ private Interpolator mLinearOutSlowInInterpolator;
+ private Interpolator mFastOutSlowInInterpolator;
+ private Interpolator mAccelerateDecelerateInterpolator;
+
+ final AccessibilityManager mAccessibilityManager;
+
+ Runnable mGroupListFadeInAnimation = new Runnable() {
+ @Override
+ public void run() {
+ startGroupListFadeInAnimation();
+ }
+ };
+
+ public MediaRouteControllerDialog(Context context) {
+ this(context, 0);
+ }
+
+ public MediaRouteControllerDialog(Context context, int theme) {
+ super(context = MediaRouterThemeHelper.createThemedDialogContext(context, theme, true),
+ MediaRouterThemeHelper.createThemedDialogStyle(context));
+ mContext = getContext();
+
+ mControllerCallback = new MediaControllerCallback();
+ mRouter = MediaRouter.getInstance(mContext);
+ mCallback = new MediaRouterCallback();
+ mRoute = mRouter.getSelectedRoute();
+ setMediaSession(mRouter.getMediaSessionToken());
+ mVolumeGroupListPaddingTop = mContext.getResources().getDimensionPixelSize(
+ R.dimen.mr_controller_volume_group_list_padding_top);
+ mAccessibilityManager =
+ (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
+ R.interpolator.mr_linear_out_slow_in);
+ mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
+ R.interpolator.mr_fast_out_slow_in);
+ }
+ mAccelerateDecelerateInterpolator = new AccelerateDecelerateInterpolator();
+ }
+
+ /**
+ * Gets the route that this dialog is controlling.
+ */
+ public MediaRouter.RouteInfo getRoute() {
+ return mRoute;
+ }
+
+ private MediaRouter.RouteGroup getGroup() {
+ if (mRoute instanceof MediaRouter.RouteGroup) {
+ return (MediaRouter.RouteGroup) mRoute;
+ }
+ return null;
+ }
+
+ /**
+ * Provides the subclass an opportunity to create a view that will replace the default media
+ * controls for the currently playing content.
+ *
+ * @param savedInstanceState The dialog's saved instance state.
+ * @return The media control view, or null if none.
+ */
+ public View onCreateMediaControlView(Bundle savedInstanceState) {
+ return null;
+ }
+
+ /**
+ * Gets the media control view that was created by {@link #onCreateMediaControlView(Bundle)}.
+ *
+ * @return The media control view, or null if none.
+ */
+ public View getMediaControlView() {
+ return mCustomControlView;
+ }
+
+ /**
+ * Sets whether to enable the volume slider and volume control using the volume keys
+ * when the route supports it.
+ * <p>
+ * The default value is true.
+ * </p>
+ */
+ public void setVolumeControlEnabled(boolean enable) {
+ if (mVolumeControlEnabled != enable) {
+ mVolumeControlEnabled = enable;
+ if (mCreated) {
+ update(false);
+ }
+ }
+ }
+
+ /**
+ * Returns whether to enable the volume slider and volume control using the volume keys
+ * when the route supports it.
+ */
+ public boolean isVolumeControlEnabled() {
+ return mVolumeControlEnabled;
+ }
+
+ /**
+ * Set the session to use for metadata and transport controls. The dialog
+ * will listen to changes on this session and update the UI automatically in
+ * response to changes.
+ *
+ * @param sessionToken The token for the session to use.
+ */
+ private void setMediaSession(MediaSessionCompat.Token sessionToken) {
+ if (mMediaController != null) {
+ mMediaController.unregisterCallback(mControllerCallback);
+ mMediaController = null;
+ }
+ if (sessionToken == null) {
+ return;
+ }
+ if (!mAttachedToWindow) {
+ return;
+ }
+ try {
+ mMediaController = new MediaControllerCompat(mContext, sessionToken);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error creating media controller in setMediaSession.", e);
+ }
+ if (mMediaController != null) {
+ mMediaController.registerCallback(mControllerCallback);
+ }
+ MediaMetadataCompat metadata = mMediaController == null ? null
+ : mMediaController.getMetadata();
+ mDescription = metadata == null ? null : metadata.getDescription();
+ mState = mMediaController == null ? null : mMediaController.getPlaybackState();
+ updateArtIconIfNeeded();
+ update(false);
+ }
+
+ /**
+ * Gets the session to use for metadata and transport controls.
+ *
+ * @return The token for the session to use or null if none.
+ */
+ public MediaSessionCompat.Token getMediaSession() {
+ return mMediaController == null ? null : mMediaController.getSessionToken();
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getWindow().setBackgroundDrawableResource(android.R.color.transparent);
+ setContentView(R.layout.mr_controller_material_dialog_b);
+
+ // Remove the neutral button.
+ findViewById(BUTTON_NEUTRAL_RES_ID).setVisibility(View.GONE);
+
+ ClickListener listener = new ClickListener();
+
+ mExpandableAreaLayout = findViewById(R.id.mr_expandable_area);
+ mExpandableAreaLayout.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dismiss();
+ }
+ });
+ mDialogAreaLayout = findViewById(R.id.mr_dialog_area);
+ mDialogAreaLayout.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // Eat unhandled touch events.
+ }
+ });
+ int color = MediaRouterThemeHelper.getButtonTextColor(mContext);
+ mDisconnectButton = findViewById(BUTTON_DISCONNECT_RES_ID);
+ mDisconnectButton.setText(R.string.mr_controller_disconnect);
+ mDisconnectButton.setTextColor(color);
+ mDisconnectButton.setOnClickListener(listener);
+
+ mStopCastingButton = findViewById(BUTTON_STOP_RES_ID);
+ mStopCastingButton.setText(R.string.mr_controller_stop_casting);
+ mStopCastingButton.setTextColor(color);
+ mStopCastingButton.setOnClickListener(listener);
+
+ mRouteNameTextView = findViewById(R.id.mr_name);
+ mCloseButton = findViewById(R.id.mr_close);
+ mCloseButton.setOnClickListener(listener);
+ mCustomControlLayout = findViewById(R.id.mr_custom_control);
+ mDefaultControlLayout = findViewById(R.id.mr_default_control);
+
+ // Start the session activity when a content item (album art, title or subtitle) is clicked.
+ View.OnClickListener onClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mMediaController != null) {
+ PendingIntent pi = mMediaController.getSessionActivity();
+ if (pi != null) {
+ try {
+ pi.send();
+ dismiss();
+ } catch (PendingIntent.CanceledException e) {
+ Log.e(TAG, pi + " was not sent, it had been canceled.");
+ }
+ }
+ }
+ }
+ };
+ mArtView = findViewById(R.id.mr_art);
+ mArtView.setOnClickListener(onClickListener);
+ findViewById(R.id.mr_control_title_container).setOnClickListener(onClickListener);
+
+ mMediaMainControlLayout = findViewById(R.id.mr_media_main_control);
+ mDividerView = findViewById(R.id.mr_control_divider);
+
+ mPlaybackControlLayout = findViewById(R.id.mr_playback_control);
+ mTitleView = findViewById(R.id.mr_control_title);
+ mSubtitleView = findViewById(R.id.mr_control_subtitle);
+ mPlaybackControlButton = findViewById(R.id.mr_control_playback_ctrl);
+ mPlaybackControlButton.setOnClickListener(listener);
+
+ mVolumeControlLayout = findViewById(R.id.mr_volume_control);
+ mVolumeControlLayout.setVisibility(View.GONE);
+ mVolumeSlider = findViewById(R.id.mr_volume_slider);
+ mVolumeSlider.setTag(mRoute);
+ mVolumeChangeListener = new VolumeChangeListener();
+ mVolumeSlider.setOnSeekBarChangeListener(mVolumeChangeListener);
+
+ mVolumeGroupList = findViewById(R.id.mr_volume_group_list);
+ mGroupMemberRoutes = new ArrayList<MediaRouter.RouteInfo>();
+ mVolumeGroupAdapter = new VolumeGroupAdapter(mVolumeGroupList.getContext(),
+ mGroupMemberRoutes);
+ mVolumeGroupList.setAdapter(mVolumeGroupAdapter);
+ mGroupMemberRoutesAnimatingWithBitmap = new HashSet<>();
+
+ MediaRouterThemeHelper.setMediaControlsBackgroundColor(mContext,
+ mMediaMainControlLayout, mVolumeGroupList, getGroup() != null);
+ MediaRouterThemeHelper.setVolumeSliderColor(mContext,
+ (MediaRouteVolumeSlider) mVolumeSlider, mMediaMainControlLayout);
+ mVolumeSliderMap = new HashMap<>();
+ mVolumeSliderMap.put(mRoute, mVolumeSlider);
+
+ mGroupExpandCollapseButton =
+ findViewById(R.id.mr_group_expand_collapse);
+ mGroupExpandCollapseButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mIsGroupExpanded = !mIsGroupExpanded;
+ if (mIsGroupExpanded) {
+ mVolumeGroupList.setVisibility(View.VISIBLE);
+ }
+ loadInterpolator();
+ updateLayoutHeight(true);
+ }
+ });
+ loadInterpolator();
+ mGroupListAnimationDurationMs = mContext.getResources().getInteger(
+ R.integer.mr_controller_volume_group_list_animation_duration_ms);
+ mGroupListFadeInDurationMs = mContext.getResources().getInteger(
+ R.integer.mr_controller_volume_group_list_fade_in_duration_ms);
+ mGroupListFadeOutDurationMs = mContext.getResources().getInteger(
+ R.integer.mr_controller_volume_group_list_fade_out_duration_ms);
+
+ mCustomControlView = onCreateMediaControlView(savedInstanceState);
+ if (mCustomControlView != null) {
+ mCustomControlLayout.addView(mCustomControlView);
+ mCustomControlLayout.setVisibility(View.VISIBLE);
+ }
+ mCreated = true;
+ updateLayout();
+ }
+
+ /**
+ * Sets the width of the dialog. Also called when configuration changes.
+ */
+ void updateLayout() {
+ int width = MediaRouteDialogHelper.getDialogWidth(mContext);
+ getWindow().setLayout(width, ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ View decorView = getWindow().getDecorView();
+ mDialogContentWidth = width - decorView.getPaddingLeft() - decorView.getPaddingRight();
+
+ Resources res = mContext.getResources();
+ mVolumeGroupListItemIconSize = res.getDimensionPixelSize(
+ R.dimen.mr_controller_volume_group_list_item_icon_size);
+ mVolumeGroupListItemHeight = res.getDimensionPixelSize(
+ R.dimen.mr_controller_volume_group_list_item_height);
+ mVolumeGroupListMaxHeight = res.getDimensionPixelSize(
+ R.dimen.mr_controller_volume_group_list_max_height);
+
+ // Fetch art icons again for layout changes to resize it accordingly
+ mArtIconBitmap = null;
+ mArtIconUri = null;
+ updateArtIconIfNeeded();
+ update(false);
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mAttachedToWindow = true;
+
+ mRouter.addCallback(MediaRouteSelector.EMPTY, mCallback,
+ MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS);
+ setMediaSession(mRouter.getMediaSessionToken());
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ mRouter.removeCallback(mCallback);
+ setMediaSession(null);
+ mAttachedToWindow = false;
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
+ || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+ mRoute.requestUpdateVolume(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ? -1 : 1);
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
+ || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ void update(boolean animate) {
+ // Defer dialog updates if a user is adjusting a volume in the list
+ if (mRouteInVolumeSliderTouched != null) {
+ mHasPendingUpdate = true;
+ mPendingUpdateAnimationNeeded |= animate;
+ return;
+ }
+ mHasPendingUpdate = false;
+ mPendingUpdateAnimationNeeded = false;
+ if (!mRoute.isSelected() || mRoute.isDefaultOrBluetooth()) {
+ dismiss();
+ return;
+ }
+ if (!mCreated) {
+ return;
+ }
+
+ mRouteNameTextView.setText(mRoute.getName());
+ mDisconnectButton.setVisibility(mRoute.canDisconnect() ? View.VISIBLE : View.GONE);
+ if (mCustomControlView == null && mArtIconIsLoaded) {
+ if (isBitmapRecycled(mArtIconLoadedBitmap)) {
+ Log.w(TAG, "Can't set artwork image with recycled bitmap: " + mArtIconLoadedBitmap);
+ } else {
+ mArtView.setImageBitmap(mArtIconLoadedBitmap);
+ mArtView.setBackgroundColor(mArtIconBackgroundColor);
+ }
+ clearLoadedBitmap();
+ }
+ updateVolumeControlLayout();
+ updatePlaybackControlLayout();
+ updateLayoutHeight(animate);
+ }
+
+ private boolean isBitmapRecycled(Bitmap bitmap) {
+ return bitmap != null && bitmap.isRecycled();
+ }
+
+ private boolean canShowPlaybackControlLayout() {
+ return mCustomControlView == null && (mDescription != null || mState != null);
+ }
+
+ /**
+ * Returns the height of main media controller which includes playback control and master
+ * volume control.
+ */
+ private int getMainControllerHeight(boolean showPlaybackControl) {
+ int height = 0;
+ if (showPlaybackControl || mVolumeControlLayout.getVisibility() == View.VISIBLE) {
+ height += mMediaMainControlLayout.getPaddingTop()
+ + mMediaMainControlLayout.getPaddingBottom();
+ if (showPlaybackControl) {
+ height += mPlaybackControlLayout.getMeasuredHeight();
+ }
+ if (mVolumeControlLayout.getVisibility() == View.VISIBLE) {
+ height += mVolumeControlLayout.getMeasuredHeight();
+ }
+ if (showPlaybackControl && mVolumeControlLayout.getVisibility() == View.VISIBLE) {
+ height += mDividerView.getMeasuredHeight();
+ }
+ }
+ return height;
+ }
+
+ private void updateMediaControlVisibility(boolean canShowPlaybackControlLayout) {
+ // TODO: Update the top and bottom padding of the control layout according to the display
+ // height.
+ mDividerView.setVisibility((mVolumeControlLayout.getVisibility() == View.VISIBLE
+ && canShowPlaybackControlLayout) ? View.VISIBLE : View.GONE);
+ mMediaMainControlLayout.setVisibility((mVolumeControlLayout.getVisibility() == View.GONE
+ && !canShowPlaybackControlLayout) ? View.GONE : View.VISIBLE);
+ }
+
+ void updateLayoutHeight(final boolean animate) {
+ // We need to defer the update until the first layout has occurred, as we don't yet know the
+ // overall visible display size in which the window this view is attached to has been
+ // positioned in.
+ mDefaultControlLayout.requestLayout();
+ ViewTreeObserver observer = mDefaultControlLayout.getViewTreeObserver();
+ observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mDefaultControlLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ if (mIsGroupListAnimating) {
+ mIsGroupListAnimationPending = true;
+ } else {
+ updateLayoutHeightInternal(animate);
+ }
+ }
+ });
+ }
+
+ /**
+ * Updates the height of views and hide artwork or metadata if space is limited.
+ */
+ void updateLayoutHeightInternal(boolean animate) {
+ // Measure the size of widgets and get the height of main components.
+ int oldHeight = getLayoutHeight(mMediaMainControlLayout);
+ setLayoutHeight(mMediaMainControlLayout, ViewGroup.LayoutParams.MATCH_PARENT);
+ updateMediaControlVisibility(canShowPlaybackControlLayout());
+ View decorView = getWindow().getDecorView();
+ decorView.measure(
+ MeasureSpec.makeMeasureSpec(getWindow().getAttributes().width, MeasureSpec.EXACTLY),
+ MeasureSpec.UNSPECIFIED);
+ setLayoutHeight(mMediaMainControlLayout, oldHeight);
+ int artViewHeight = 0;
+ if (mCustomControlView == null && mArtView.getDrawable() instanceof BitmapDrawable) {
+ Bitmap art = ((BitmapDrawable) mArtView.getDrawable()).getBitmap();
+ if (art != null) {
+ artViewHeight = getDesiredArtHeight(art.getWidth(), art.getHeight());
+ mArtView.setScaleType(art.getWidth() >= art.getHeight()
+ ? ImageView.ScaleType.FIT_XY : ImageView.ScaleType.FIT_CENTER);
+ }
+ }
+ int mainControllerHeight = getMainControllerHeight(canShowPlaybackControlLayout());
+ int volumeGroupListCount = mGroupMemberRoutes.size();
+ // Scale down volume group list items in landscape mode.
+ int expandedGroupListHeight = getGroup() == null ? 0 :
+ mVolumeGroupListItemHeight * getGroup().getRoutes().size();
+ if (volumeGroupListCount > 0) {
+ expandedGroupListHeight += mVolumeGroupListPaddingTop;
+ }
+ expandedGroupListHeight = Math.min(expandedGroupListHeight, mVolumeGroupListMaxHeight);
+ int visibleGroupListHeight = mIsGroupExpanded ? expandedGroupListHeight : 0;
+
+ int desiredControlLayoutHeight =
+ Math.max(artViewHeight, visibleGroupListHeight) + mainControllerHeight;
+ Rect visibleRect = new Rect();
+ decorView.getWindowVisibleDisplayFrame(visibleRect);
+ // Height of non-control views in decor view.
+ // This includes title bar, button bar, and dialog's vertical padding which should be
+ // always shown.
+ int nonControlViewHeight = mDialogAreaLayout.getMeasuredHeight()
+ - mDefaultControlLayout.getMeasuredHeight();
+ // Maximum allowed height for controls to fit screen.
+ int maximumControlViewHeight = visibleRect.height() - nonControlViewHeight;
+
+ // Show artwork if it fits the screen.
+ if (mCustomControlView == null && artViewHeight > 0
+ && desiredControlLayoutHeight <= maximumControlViewHeight) {
+ mArtView.setVisibility(View.VISIBLE);
+ setLayoutHeight(mArtView, artViewHeight);
+ } else {
+ if (getLayoutHeight(mVolumeGroupList) + mMediaMainControlLayout.getMeasuredHeight()
+ >= mDefaultControlLayout.getMeasuredHeight()) {
+ mArtView.setVisibility(View.GONE);
+ }
+ artViewHeight = 0;
+ desiredControlLayoutHeight = visibleGroupListHeight + mainControllerHeight;
+ }
+ // Show the playback control if it fits the screen.
+ if (canShowPlaybackControlLayout()
+ && desiredControlLayoutHeight <= maximumControlViewHeight) {
+ mPlaybackControlLayout.setVisibility(View.VISIBLE);
+ } else {
+ mPlaybackControlLayout.setVisibility(View.GONE);
+ }
+ updateMediaControlVisibility(mPlaybackControlLayout.getVisibility() == View.VISIBLE);
+ mainControllerHeight = getMainControllerHeight(
+ mPlaybackControlLayout.getVisibility() == View.VISIBLE);
+ desiredControlLayoutHeight =
+ Math.max(artViewHeight, visibleGroupListHeight) + mainControllerHeight;
+
+ // Limit the volume group list height to fit the screen.
+ if (desiredControlLayoutHeight > maximumControlViewHeight) {
+ visibleGroupListHeight -= (desiredControlLayoutHeight - maximumControlViewHeight);
+ desiredControlLayoutHeight = maximumControlViewHeight;
+ }
+ // Update the layouts with the computed heights.
+ mMediaMainControlLayout.clearAnimation();
+ mVolumeGroupList.clearAnimation();
+ mDefaultControlLayout.clearAnimation();
+ if (animate) {
+ animateLayoutHeight(mMediaMainControlLayout, mainControllerHeight);
+ animateLayoutHeight(mVolumeGroupList, visibleGroupListHeight);
+ animateLayoutHeight(mDefaultControlLayout, desiredControlLayoutHeight);
+ } else {
+ setLayoutHeight(mMediaMainControlLayout, mainControllerHeight);
+ setLayoutHeight(mVolumeGroupList, visibleGroupListHeight);
+ setLayoutHeight(mDefaultControlLayout, desiredControlLayoutHeight);
+ }
+ // Maximize the window size with a transparent layout in advance for smooth animation.
+ setLayoutHeight(mExpandableAreaLayout, visibleRect.height());
+ rebuildVolumeGroupList(animate);
+ }
+
+ void updateVolumeGroupItemHeight(View item) {
+ LinearLayout container = (LinearLayout) item.findViewById(R.id.volume_item_container);
+ setLayoutHeight(container, mVolumeGroupListItemHeight);
+ View icon = item.findViewById(R.id.mr_volume_item_icon);
+ ViewGroup.LayoutParams lp = icon.getLayoutParams();
+ lp.width = mVolumeGroupListItemIconSize;
+ lp.height = mVolumeGroupListItemIconSize;
+ icon.setLayoutParams(lp);
+ }
+
+ private void animateLayoutHeight(final View view, int targetHeight) {
+ final int startValue = getLayoutHeight(view);
+ final int endValue = targetHeight;
+ Animation anim = new Animation() {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ int height = startValue - (int) ((startValue - endValue) * interpolatedTime);
+ setLayoutHeight(view, height);
+ }
+ };
+ anim.setDuration(mGroupListAnimationDurationMs);
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ anim.setInterpolator(mInterpolator);
+ }
+ view.startAnimation(anim);
+ }
+
+ void loadInterpolator() {
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ mInterpolator = mIsGroupExpanded ? mLinearOutSlowInInterpolator
+ : mFastOutSlowInInterpolator;
+ } else {
+ mInterpolator = mAccelerateDecelerateInterpolator;
+ }
+ }
+
+ private void updateVolumeControlLayout() {
+ if (isVolumeControlAvailable(mRoute)) {
+ if (mVolumeControlLayout.getVisibility() == View.GONE) {
+ mVolumeControlLayout.setVisibility(View.VISIBLE);
+ mVolumeSlider.setMax(mRoute.getVolumeMax());
+ mVolumeSlider.setProgress(mRoute.getVolume());
+ mGroupExpandCollapseButton.setVisibility(getGroup() == null ? View.GONE
+ : View.VISIBLE);
+ }
+ } else {
+ mVolumeControlLayout.setVisibility(View.GONE);
+ }
+ }
+
+ private void rebuildVolumeGroupList(boolean animate) {
+ List<MediaRouter.RouteInfo> routes = getGroup() == null ? null : getGroup().getRoutes();
+ if (routes == null) {
+ mGroupMemberRoutes.clear();
+ mVolumeGroupAdapter.notifyDataSetChanged();
+ } else if (MediaRouteDialogHelper.listUnorderedEquals(mGroupMemberRoutes, routes)) {
+ mVolumeGroupAdapter.notifyDataSetChanged();
+ } else {
+ HashMap<MediaRouter.RouteInfo, Rect> previousRouteBoundMap = animate
+ ? MediaRouteDialogHelper.getItemBoundMap(mVolumeGroupList, mVolumeGroupAdapter)
+ : null;
+ HashMap<MediaRouter.RouteInfo, BitmapDrawable> previousRouteBitmapMap = animate
+ ? MediaRouteDialogHelper.getItemBitmapMap(mContext, mVolumeGroupList,
+ mVolumeGroupAdapter) : null;
+ mGroupMemberRoutesAdded =
+ MediaRouteDialogHelper.getItemsAdded(mGroupMemberRoutes, routes);
+ mGroupMemberRoutesRemoved = MediaRouteDialogHelper.getItemsRemoved(mGroupMemberRoutes,
+ routes);
+ mGroupMemberRoutes.addAll(0, mGroupMemberRoutesAdded);
+ mGroupMemberRoutes.removeAll(mGroupMemberRoutesRemoved);
+ mVolumeGroupAdapter.notifyDataSetChanged();
+ if (animate && mIsGroupExpanded
+ && mGroupMemberRoutesAdded.size() + mGroupMemberRoutesRemoved.size() > 0) {
+ animateGroupListItems(previousRouteBoundMap, previousRouteBitmapMap);
+ } else {
+ mGroupMemberRoutesAdded = null;
+ mGroupMemberRoutesRemoved = null;
+ }
+ }
+ }
+
+ private void animateGroupListItems(final Map<MediaRouter.RouteInfo, Rect> previousRouteBoundMap,
+ final Map<MediaRouter.RouteInfo, BitmapDrawable> previousRouteBitmapMap) {
+ mVolumeGroupList.setEnabled(false);
+ mVolumeGroupList.requestLayout();
+ mIsGroupListAnimating = true;
+ ViewTreeObserver observer = mVolumeGroupList.getViewTreeObserver();
+ observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mVolumeGroupList.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ animateGroupListItemsInternal(previousRouteBoundMap, previousRouteBitmapMap);
+ }
+ });
+ }
+
+ void animateGroupListItemsInternal(
+ Map<MediaRouter.RouteInfo, Rect> previousRouteBoundMap,
+ Map<MediaRouter.RouteInfo, BitmapDrawable> previousRouteBitmapMap) {
+ if (mGroupMemberRoutesAdded == null || mGroupMemberRoutesRemoved == null) {
+ return;
+ }
+ int groupSizeDelta = mGroupMemberRoutesAdded.size() - mGroupMemberRoutesRemoved.size();
+ boolean listenerRegistered = false;
+ Animation.AnimationListener listener = new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ mVolumeGroupList.startAnimationAll();
+ mVolumeGroupList.postDelayed(mGroupListFadeInAnimation,
+ mGroupListAnimationDurationMs);
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) { }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) { }
+ };
+
+ // Animate visible items from previous positions to current positions except routes added
+ // just before. Added routes will remain hidden until translate animation finishes.
+ int first = mVolumeGroupList.getFirstVisiblePosition();
+ for (int i = 0; i < mVolumeGroupList.getChildCount(); ++i) {
+ View view = mVolumeGroupList.getChildAt(i);
+ int position = first + i;
+ MediaRouter.RouteInfo route = mVolumeGroupAdapter.getItem(position);
+ Rect previousBounds = previousRouteBoundMap.get(route);
+ int currentTop = view.getTop();
+ int previousTop = previousBounds != null ? previousBounds.top
+ : (currentTop + mVolumeGroupListItemHeight * groupSizeDelta);
+ AnimationSet animSet = new AnimationSet(true);
+ if (mGroupMemberRoutesAdded != null && mGroupMemberRoutesAdded.contains(route)) {
+ previousTop = currentTop;
+ Animation alphaAnim = new AlphaAnimation(0.0f, 0.0f);
+ alphaAnim.setDuration(mGroupListFadeInDurationMs);
+ animSet.addAnimation(alphaAnim);
+ }
+ Animation translationAnim = new TranslateAnimation(0, 0, previousTop - currentTop, 0);
+ translationAnim.setDuration(mGroupListAnimationDurationMs);
+ animSet.addAnimation(translationAnim);
+ animSet.setFillAfter(true);
+ animSet.setFillEnabled(true);
+ animSet.setInterpolator(mInterpolator);
+ if (!listenerRegistered) {
+ listenerRegistered = true;
+ animSet.setAnimationListener(listener);
+ }
+ view.clearAnimation();
+ view.startAnimation(animSet);
+ previousRouteBoundMap.remove(route);
+ previousRouteBitmapMap.remove(route);
+ }
+
+ // If a member route doesn't exist any longer, it can be either removed or moved out of the
+ // ListView layout boundary. In this case, use the previously captured bitmaps for
+ // animation.
+ for (Map.Entry<MediaRouter.RouteInfo, BitmapDrawable> item
+ : previousRouteBitmapMap.entrySet()) {
+ final MediaRouter.RouteInfo route = item.getKey();
+ final BitmapDrawable bitmap = item.getValue();
+ final Rect bounds = previousRouteBoundMap.get(route);
+ OverlayObject object = null;
+ if (mGroupMemberRoutesRemoved.contains(route)) {
+ object = new OverlayObject(bitmap, bounds).setAlphaAnimation(1.0f, 0.0f)
+ .setDuration(mGroupListFadeOutDurationMs)
+ .setInterpolator(mInterpolator);
+ } else {
+ int deltaY = groupSizeDelta * mVolumeGroupListItemHeight;
+ object = new OverlayObject(bitmap, bounds).setTranslateYAnimation(deltaY)
+ .setDuration(mGroupListAnimationDurationMs)
+ .setInterpolator(mInterpolator)
+ .setAnimationEndListener(new OverlayObject.OnAnimationEndListener() {
+ @Override
+ public void onAnimationEnd() {
+ mGroupMemberRoutesAnimatingWithBitmap.remove(route);
+ mVolumeGroupAdapter.notifyDataSetChanged();
+ }
+ });
+ mGroupMemberRoutesAnimatingWithBitmap.add(route);
+ }
+ mVolumeGroupList.addOverlayObject(object);
+ }
+ }
+
+ void startGroupListFadeInAnimation() {
+ clearGroupListAnimation(true);
+ mVolumeGroupList.requestLayout();
+ ViewTreeObserver observer = mVolumeGroupList.getViewTreeObserver();
+ observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mVolumeGroupList.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ startGroupListFadeInAnimationInternal();
+ }
+ });
+ }
+
+ void startGroupListFadeInAnimationInternal() {
+ if (mGroupMemberRoutesAdded != null && mGroupMemberRoutesAdded.size() != 0) {
+ fadeInAddedRoutes();
+ } else {
+ finishAnimation(true);
+ }
+ }
+
+ void finishAnimation(boolean animate) {
+ mGroupMemberRoutesAdded = null;
+ mGroupMemberRoutesRemoved = null;
+ mIsGroupListAnimating = false;
+ if (mIsGroupListAnimationPending) {
+ mIsGroupListAnimationPending = false;
+ updateLayoutHeight(animate);
+ }
+ mVolumeGroupList.setEnabled(true);
+ }
+
+ private void fadeInAddedRoutes() {
+ Animation.AnimationListener listener = new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) { }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ finishAnimation(true);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) { }
+ };
+ boolean listenerRegistered = false;
+ int first = mVolumeGroupList.getFirstVisiblePosition();
+ for (int i = 0; i < mVolumeGroupList.getChildCount(); ++i) {
+ View view = mVolumeGroupList.getChildAt(i);
+ int position = first + i;
+ MediaRouter.RouteInfo route = mVolumeGroupAdapter.getItem(position);
+ if (mGroupMemberRoutesAdded.contains(route)) {
+ Animation alphaAnim = new AlphaAnimation(0.0f, 1.0f);
+ alphaAnim.setDuration(mGroupListFadeInDurationMs);
+ alphaAnim.setFillEnabled(true);
+ alphaAnim.setFillAfter(true);
+ if (!listenerRegistered) {
+ listenerRegistered = true;
+ alphaAnim.setAnimationListener(listener);
+ }
+ view.clearAnimation();
+ view.startAnimation(alphaAnim);
+ }
+ }
+ }
+
+ void clearGroupListAnimation(boolean exceptAddedRoutes) {
+ int first = mVolumeGroupList.getFirstVisiblePosition();
+ for (int i = 0; i < mVolumeGroupList.getChildCount(); ++i) {
+ View view = mVolumeGroupList.getChildAt(i);
+ int position = first + i;
+ MediaRouter.RouteInfo route = mVolumeGroupAdapter.getItem(position);
+ if (exceptAddedRoutes && mGroupMemberRoutesAdded != null
+ && mGroupMemberRoutesAdded.contains(route)) {
+ continue;
+ }
+ LinearLayout container = (LinearLayout) view.findViewById(R.id.volume_item_container);
+ container.setVisibility(View.VISIBLE);
+ AnimationSet animSet = new AnimationSet(true);
+ Animation alphaAnim = new AlphaAnimation(1.0f, 1.0f);
+ alphaAnim.setDuration(0);
+ animSet.addAnimation(alphaAnim);
+ Animation translationAnim = new TranslateAnimation(0, 0, 0, 0);
+ translationAnim.setDuration(0);
+ animSet.setFillAfter(true);
+ animSet.setFillEnabled(true);
+ view.clearAnimation();
+ view.startAnimation(animSet);
+ }
+ mVolumeGroupList.stopAnimationAll();
+ if (!exceptAddedRoutes) {
+ finishAnimation(false);
+ }
+ }
+
+ private void updatePlaybackControlLayout() {
+ if (canShowPlaybackControlLayout()) {
+ CharSequence title = mDescription == null ? null : mDescription.getTitle();
+ boolean hasTitle = !TextUtils.isEmpty(title);
+
+ CharSequence subtitle = mDescription == null ? null : mDescription.getSubtitle();
+ boolean hasSubtitle = !TextUtils.isEmpty(subtitle);
+
+ boolean showTitle = false;
+ boolean showSubtitle = false;
+ if (mRoute.getPresentationDisplayId()
+ != MediaRouter.RouteInfo.PRESENTATION_DISPLAY_ID_NONE) {
+ // The user is currently casting screen.
+ mTitleView.setText(R.string.mr_controller_casting_screen);
+ showTitle = true;
+ } else if (mState == null || mState.getState() == PlaybackStateCompat.STATE_NONE) {
+ // Show "No media selected" as we don't yet know the playback state.
+ mTitleView.setText(R.string.mr_controller_no_media_selected);
+ showTitle = true;
+ } else if (!hasTitle && !hasSubtitle) {
+ mTitleView.setText(R.string.mr_controller_no_info_available);
+ showTitle = true;
+ } else {
+ if (hasTitle) {
+ mTitleView.setText(title);
+ showTitle = true;
+ }
+ if (hasSubtitle) {
+ mSubtitleView.setText(subtitle);
+ showSubtitle = true;
+ }
+ }
+ mTitleView.setVisibility(showTitle ? View.VISIBLE : View.GONE);
+ mSubtitleView.setVisibility(showSubtitle ? View.VISIBLE : View.GONE);
+
+ if (mState != null) {
+ boolean isPlaying = mState.getState() == PlaybackStateCompat.STATE_BUFFERING
+ || mState.getState() == PlaybackStateCompat.STATE_PLAYING;
+ Context playbackControlButtonContext = mPlaybackControlButton.getContext();
+ boolean visible = true;
+ int iconDrawableAttr = 0;
+ int iconDescResId = 0;
+ if (isPlaying && isPauseActionSupported()) {
+ iconDrawableAttr = R.attr.mediaRoutePauseDrawable;
+ iconDescResId = R.string.mr_controller_pause;
+ } else if (isPlaying && isStopActionSupported()) {
+ iconDrawableAttr = R.attr.mediaRouteStopDrawable;
+ iconDescResId = R.string.mr_controller_stop;
+ } else if (!isPlaying && isPlayActionSupported()) {
+ iconDrawableAttr = R.attr.mediaRoutePlayDrawable;
+ iconDescResId = R.string.mr_controller_play;
+ } else {
+ visible = false;
+ }
+ mPlaybackControlButton.setVisibility(visible ? View.VISIBLE : View.GONE);
+ if (visible) {
+ mPlaybackControlButton.setImageResource(
+ MediaRouterThemeHelper.getThemeResource(
+ playbackControlButtonContext, iconDrawableAttr));
+ mPlaybackControlButton.setContentDescription(
+ playbackControlButtonContext.getResources()
+ .getText(iconDescResId));
+ }
+ }
+ }
+ }
+
+ private boolean isPlayActionSupported() {
+ return (mState.getActions() & (ACTION_PLAY | ACTION_PLAY_PAUSE)) != 0;
+ }
+
+ private boolean isPauseActionSupported() {
+ return (mState.getActions() & (ACTION_PAUSE | ACTION_PLAY_PAUSE)) != 0;
+ }
+
+ private boolean isStopActionSupported() {
+ return (mState.getActions() & ACTION_STOP) != 0;
+ }
+
+ boolean isVolumeControlAvailable(MediaRouter.RouteInfo route) {
+ return mVolumeControlEnabled && route.getVolumeHandling()
+ == MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE;
+ }
+
+ private static int getLayoutHeight(View view) {
+ return view.getLayoutParams().height;
+ }
+
+ static void setLayoutHeight(View view, int height) {
+ ViewGroup.LayoutParams lp = view.getLayoutParams();
+ lp.height = height;
+ view.setLayoutParams(lp);
+ }
+
+ private static boolean uriEquals(Uri uri1, Uri uri2) {
+ if (uri1 != null && uri1.equals(uri2)) {
+ return true;
+ } else if (uri1 == null && uri2 == null) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns desired art height to fit into controller dialog.
+ */
+ int getDesiredArtHeight(int originalWidth, int originalHeight) {
+ if (originalWidth >= originalHeight) {
+ // For landscape art, fit width to dialog width.
+ return (int) ((float) mDialogContentWidth * originalHeight / originalWidth + 0.5f);
+ }
+ // For portrait art, fit height to 16:9 ratio case's height.
+ return (int) ((float) mDialogContentWidth * 9 / 16 + 0.5f);
+ }
+
+ void updateArtIconIfNeeded() {
+ if (mCustomControlView != null || !isIconChanged()) {
+ return;
+ }
+ if (mFetchArtTask != null) {
+ mFetchArtTask.cancel(true);
+ }
+ mFetchArtTask = new FetchArtTask();
+ mFetchArtTask.execute();
+ }
+
+ /**
+ * Clear the bitmap loaded by FetchArtTask. Will be called after the loaded bitmaps are applied
+ * to artwork, or no longer valid.
+ */
+ void clearLoadedBitmap() {
+ mArtIconIsLoaded = false;
+ mArtIconLoadedBitmap = null;
+ mArtIconBackgroundColor = 0;
+ }
+
+ /**
+ * Returns whether a new art image is different from an original art image. Compares
+ * Bitmap objects first, and then compares URIs only if bitmap is unchanged with
+ * a null value.
+ */
+ private boolean isIconChanged() {
+ Bitmap newBitmap = mDescription == null ? null : mDescription.getIconBitmap();
+ Uri newUri = mDescription == null ? null : mDescription.getIconUri();
+ Bitmap oldBitmap = mFetchArtTask == null ? mArtIconBitmap : mFetchArtTask.getIconBitmap();
+ Uri oldUri = mFetchArtTask == null ? mArtIconUri : mFetchArtTask.getIconUri();
+ if (oldBitmap != newBitmap) {
+ return true;
+ } else if (oldBitmap == null && !uriEquals(oldUri, newUri)) {
+ return true;
+ }
+ return false;
+ }
+
+ private final class MediaRouterCallback extends MediaRouter.Callback {
+ MediaRouterCallback() {
+ }
+
+ @Override
+ public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route) {
+ update(false);
+ }
+
+ @Override
+ public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) {
+ update(true);
+ }
+
+ @Override
+ public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo route) {
+ SeekBar volumeSlider = mVolumeSliderMap.get(route);
+ int volume = route.getVolume();
+ if (DEBUG) {
+ Log.d(TAG, "onRouteVolumeChanged(), route.getVolume:" + volume);
+ }
+ if (volumeSlider != null && mRouteInVolumeSliderTouched != route) {
+ volumeSlider.setProgress(volume);
+ }
+ }
+ }
+
+ private final class MediaControllerCallback extends MediaControllerCompat.Callback {
+ MediaControllerCallback() {
+ }
+
+ @Override
+ public void onSessionDestroyed() {
+ if (mMediaController != null) {
+ mMediaController.unregisterCallback(mControllerCallback);
+ mMediaController = null;
+ }
+ }
+
+ @Override
+ public void onPlaybackStateChanged(PlaybackStateCompat state) {
+ mState = state;
+ update(false);
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadataCompat metadata) {
+ mDescription = metadata == null ? null : metadata.getDescription();
+ updateArtIconIfNeeded();
+ update(false);
+ }
+ }
+
+ private final class ClickListener implements View.OnClickListener {
+ ClickListener() {
+ }
+
+ @Override
+ public void onClick(View v) {
+ int id = v.getId();
+ if (id == BUTTON_STOP_RES_ID || id == BUTTON_DISCONNECT_RES_ID) {
+ if (mRoute.isSelected()) {
+ mRouter.unselect(id == BUTTON_STOP_RES_ID ?
+ MediaRouter.UNSELECT_REASON_STOPPED :
+ MediaRouter.UNSELECT_REASON_DISCONNECTED);
+ }
+ dismiss();
+ } else if (id == R.id.mr_control_playback_ctrl) {
+ if (mMediaController != null && mState != null) {
+ boolean isPlaying = mState.getState() == PlaybackStateCompat.STATE_PLAYING;
+ int actionDescResId = 0;
+ if (isPlaying && isPauseActionSupported()) {
+ mMediaController.getTransportControls().pause();
+ actionDescResId = R.string.mr_controller_pause;
+ } else if (isPlaying && isStopActionSupported()) {
+ mMediaController.getTransportControls().stop();
+ actionDescResId = R.string.mr_controller_stop;
+ } else if (!isPlaying && isPlayActionSupported()){
+ mMediaController.getTransportControls().play();
+ actionDescResId = R.string.mr_controller_play;
+ }
+ // Announce the action for accessibility.
+ if (mAccessibilityManager != null && mAccessibilityManager.isEnabled()
+ && actionDescResId != 0) {
+ AccessibilityEvent event = AccessibilityEvent.obtain(
+ AccessibilityEventCompat.TYPE_ANNOUNCEMENT);
+ event.setPackageName(mContext.getPackageName());
+ event.setClassName(getClass().getName());
+ event.getText().add(mContext.getString(actionDescResId));
+ mAccessibilityManager.sendAccessibilityEvent(event);
+ }
+ }
+ } else if (id == R.id.mr_close) {
+ dismiss();
+ }
+ }
+ }
+
+ private class VolumeChangeListener implements SeekBar.OnSeekBarChangeListener {
+ private final Runnable mStopTrackingTouch = new Runnable() {
+ @Override
+ public void run() {
+ if (mRouteInVolumeSliderTouched != null) {
+ mRouteInVolumeSliderTouched = null;
+ if (mHasPendingUpdate) {
+ update(mPendingUpdateAnimationNeeded);
+ }
+ }
+ }
+ };
+
+ VolumeChangeListener() {
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ if (mRouteInVolumeSliderTouched != null) {
+ mVolumeSlider.removeCallbacks(mStopTrackingTouch);
+ }
+ mRouteInVolumeSliderTouched = (MediaRouter.RouteInfo) seekBar.getTag();
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ // Defer resetting mVolumeSliderTouched to allow the media route provider
+ // a little time to settle into its new state and publish the final
+ // volume update.
+ mVolumeSlider.postDelayed(mStopTrackingTouch, VOLUME_UPDATE_DELAY_MILLIS);
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (fromUser) {
+ MediaRouter.RouteInfo route = (MediaRouter.RouteInfo) seekBar.getTag();
+ if (DEBUG) {
+ Log.d(TAG, "onProgressChanged(): calling "
+ + "MediaRouter.RouteInfo.requestSetVolume(" + progress + ")");
+ }
+ route.requestSetVolume(progress);
+ }
+ }
+ }
+
+ private class VolumeGroupAdapter extends ArrayAdapter<MediaRouter.RouteInfo> {
+ final float mDisabledAlpha;
+
+ public VolumeGroupAdapter(Context context, List<MediaRouter.RouteInfo> objects) {
+ super(context, 0, objects);
+ mDisabledAlpha = MediaRouterThemeHelper.getDisabledAlpha(context);
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return false;
+ }
+
+ @Override
+ public View getView(final int position, View convertView, ViewGroup parent) {
+ View v = convertView;
+ if (v == null) {
+ v = LayoutInflater.from(parent.getContext()).inflate(
+ R.layout.mr_controller_volume_item, parent, false);
+ } else {
+ updateVolumeGroupItemHeight(v);
+ }
+
+ MediaRouter.RouteInfo route = getItem(position);
+ if (route != null) {
+ boolean isEnabled = route.isEnabled();
+
+ TextView routeName = (TextView) v.findViewById(R.id.mr_name);
+ routeName.setEnabled(isEnabled);
+ routeName.setText(route.getName());
+
+ MediaRouteVolumeSlider volumeSlider =
+ (MediaRouteVolumeSlider) v.findViewById(R.id.mr_volume_slider);
+ MediaRouterThemeHelper.setVolumeSliderColor(
+ parent.getContext(), volumeSlider, mVolumeGroupList);
+ volumeSlider.setTag(route);
+ mVolumeSliderMap.put(route, volumeSlider);
+ volumeSlider.setHideThumb(!isEnabled);
+ volumeSlider.setEnabled(isEnabled);
+ if (isEnabled) {
+ if (isVolumeControlAvailable(route)) {
+ volumeSlider.setMax(route.getVolumeMax());
+ volumeSlider.setProgress(route.getVolume());
+ volumeSlider.setOnSeekBarChangeListener(mVolumeChangeListener);
+ } else {
+ volumeSlider.setMax(100);
+ volumeSlider.setProgress(100);
+ volumeSlider.setEnabled(false);
+ }
+ }
+
+ ImageView volumeItemIcon =
+ (ImageView) v.findViewById(R.id.mr_volume_item_icon);
+ volumeItemIcon.setAlpha(isEnabled ? 0xFF : (int) (0xFF * mDisabledAlpha));
+
+ // If overlay bitmap exists, real view should remain hidden until
+ // the animation ends.
+ LinearLayout container = (LinearLayout) v.findViewById(R.id.volume_item_container);
+ container.setVisibility(mGroupMemberRoutesAnimatingWithBitmap.contains(route)
+ ? View.INVISIBLE : View.VISIBLE);
+
+ // Routes which are being added will be invisible until animation ends.
+ if (mGroupMemberRoutesAdded != null && mGroupMemberRoutesAdded.contains(route)) {
+ Animation alphaAnim = new AlphaAnimation(0.0f, 0.0f);
+ alphaAnim.setDuration(0);
+ alphaAnim.setFillEnabled(true);
+ alphaAnim.setFillAfter(true);
+ v.clearAnimation();
+ v.startAnimation(alphaAnim);
+ }
+ }
+ return v;
+ }
+ }
+
+ private class FetchArtTask extends AsyncTask<Void, Void, Bitmap> {
+ // Show animation only when fetching takes a long time.
+ private static final long SHOW_ANIM_TIME_THRESHOLD_MILLIS = 120L;
+
+ private final Bitmap mIconBitmap;
+ private final Uri mIconUri;
+ private int mBackgroundColor;
+ private long mStartTimeMillis;
+
+ FetchArtTask() {
+ Bitmap bitmap = mDescription == null ? null : mDescription.getIconBitmap();
+ if (isBitmapRecycled(bitmap)) {
+ Log.w(TAG, "Can't fetch the given art bitmap because it's already recycled.");
+ bitmap = null;
+ }
+ mIconBitmap = bitmap;
+ mIconUri = mDescription == null ? null : mDescription.getIconUri();
+ }
+
+ public Bitmap getIconBitmap() {
+ return mIconBitmap;
+ }
+
+ public Uri getIconUri() {
+ return mIconUri;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ mStartTimeMillis = SystemClock.uptimeMillis();
+ clearLoadedBitmap();
+ }
+
+ @Override
+ protected Bitmap doInBackground(Void... arg) {
+ Bitmap art = null;
+ if (mIconBitmap != null) {
+ art = mIconBitmap;
+ } else if (mIconUri != null) {
+ InputStream stream = null;
+ try {
+ if ((stream = openInputStreamByScheme(mIconUri)) == null) {
+ Log.w(TAG, "Unable to open: " + mIconUri);
+ return null;
+ }
+ // Query art size.
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(stream, null, options);
+ if (options.outWidth == 0 || options.outHeight == 0) {
+ return null;
+ }
+ // Rewind the stream in order to restart art decoding.
+ try {
+ stream.reset();
+ } catch (IOException e) {
+ // Failed to rewind the stream, try to reopen it.
+ stream.close();
+ if ((stream = openInputStreamByScheme(mIconUri)) == null) {
+ Log.w(TAG, "Unable to open: " + mIconUri);
+ return null;
+ }
+ }
+ // Calculate required size to decode the art and possibly resize it.
+ options.inJustDecodeBounds = false;
+ int reqHeight = getDesiredArtHeight(options.outWidth, options.outHeight);
+ int ratio = options.outHeight / reqHeight;
+ options.inSampleSize = Math.max(1, Integer.highestOneBit(ratio));
+ if (isCancelled()) {
+ return null;
+ }
+ art = BitmapFactory.decodeStream(stream, null, options);
+ } catch (IOException e){
+ Log.w(TAG, "Unable to open: " + mIconUri, e);
+ } finally {
+ if (stream != null) {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ }
+ if (isBitmapRecycled(art)) {
+ Log.w(TAG, "Can't use recycled bitmap: " + art);
+ return null;
+ }
+ if (art != null && art.getWidth() < art.getHeight()) {
+ // Portrait art requires dominant color as background color.
+ Palette palette = new Palette.Builder(art).maximumColorCount(1).generate();
+ mBackgroundColor = palette.getSwatches().isEmpty()
+ ? 0 : palette.getSwatches().get(0).getRgb();
+ }
+ return art;
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap art) {
+ mFetchArtTask = null;
+ if (!ObjectsCompat.equals(mArtIconBitmap, mIconBitmap)
+ || !ObjectsCompat.equals(mArtIconUri, mIconUri)) {
+ mArtIconBitmap = mIconBitmap;
+ mArtIconLoadedBitmap = art;
+ mArtIconUri = mIconUri;
+ mArtIconBackgroundColor = mBackgroundColor;
+ mArtIconIsLoaded = true;
+ long elapsedTimeMillis = SystemClock.uptimeMillis() - mStartTimeMillis;
+ // Loaded bitmap will be applied on the next update
+ update(elapsedTimeMillis > SHOW_ANIM_TIME_THRESHOLD_MILLIS);
+ }
+ }
+
+ private InputStream openInputStreamByScheme(Uri uri) throws IOException {
+ String scheme = uri.getScheme().toLowerCase();
+ InputStream stream = null;
+ if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)
+ || ContentResolver.SCHEME_CONTENT.equals(scheme)
+ || ContentResolver.SCHEME_FILE.equals(scheme)) {
+ stream = mContext.getContentResolver().openInputStream(uri);
+ } else {
+ URL url = new URL(uri.toString());
+ URLConnection conn = url.openConnection();
+ conn.setConnectTimeout(CONNECTION_TIMEOUT_MILLIS);
+ conn.setReadTimeout(CONNECTION_TIMEOUT_MILLIS);
+ stream = conn.getInputStream();
+ }
+ return (stream == null) ? null : new BufferedInputStream(stream);
+ }
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteControllerDialogFragment.java b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteControllerDialogFragment.java
new file mode 100644
index 0000000..9442df7
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteControllerDialogFragment.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.app;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+
+/**
+ * Media route controller dialog fragment.
+ * <p>
+ * Creates a {@link MediaRouteControllerDialog}. The application may subclass
+ * this dialog fragment to customize the media route controller dialog.
+ * </p>
+ */
+public class MediaRouteControllerDialogFragment extends DialogFragment {
+ private MediaRouteControllerDialog mDialog;
+ /**
+ * Creates a media route controller dialog fragment.
+ * <p>
+ * All subclasses of this class must also possess a default constructor.
+ * </p>
+ */
+ public MediaRouteControllerDialogFragment() {
+ setCancelable(true);
+ }
+
+ /**
+ * Called when the controller dialog is being created.
+ * <p>
+ * Subclasses may override this method to customize the dialog.
+ * </p>
+ */
+ public MediaRouteControllerDialog onCreateControllerDialog(
+ Context context, Bundle savedInstanceState) {
+ return new MediaRouteControllerDialog(context);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ mDialog = onCreateControllerDialog(getContext(), savedInstanceState);
+ return mDialog;
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ if (mDialog != null) {
+ mDialog.clearGroupListAnimation(false);
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ if (mDialog != null) {
+ mDialog.updateLayout();
+ }
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteDialogFactory.java b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteDialogFactory.java
new file mode 100644
index 0000000..a9eaf39
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteDialogFactory.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.app;
+
+import android.support.annotation.NonNull;
+
+/**
+ * The media route dialog factory is responsible for creating the media route
+ * chooser and controller dialogs as needed.
+ * <p>
+ * The application can customize the dialogs by providing a subclass of the
+ * dialog factory to the {@link MediaRouteButton} using the
+ * {@link MediaRouteButton#setDialogFactory setDialogFactory} method.
+ * </p>
+ */
+public class MediaRouteDialogFactory {
+ private static final MediaRouteDialogFactory sDefault = new MediaRouteDialogFactory();
+
+ /**
+ * Creates a default media route dialog factory.
+ */
+ public MediaRouteDialogFactory() {
+ }
+
+ /**
+ * Gets the default factory instance.
+ *
+ * @return The default media route dialog factory, never null.
+ */
+ @NonNull
+ public static MediaRouteDialogFactory getDefault() {
+ return sDefault;
+ }
+
+ /**
+ * Called when the chooser dialog is being opened and it is time to create the fragment.
+ * <p>
+ * Subclasses may override this method to create a customized fragment.
+ * </p>
+ *
+ * @return The media route chooser dialog fragment, must not be null.
+ */
+ @NonNull
+ public MediaRouteChooserDialogFragment onCreateChooserDialogFragment() {
+ return new MediaRouteChooserDialogFragment();
+ }
+
+ /**
+ * Called when the controller dialog is being opened and it is time to create the fragment.
+ * <p>
+ * Subclasses may override this method to create a customized fragment.
+ * </p>
+ *
+ * @return The media route controller dialog fragment, must not be null.
+ */
+ @NonNull
+ public MediaRouteControllerDialogFragment onCreateControllerDialogFragment() {
+ return new MediaRouteControllerDialogFragment();
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteDialogHelper.java b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteDialogHelper.java
new file mode 100644
index 0000000..6f75b46
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteDialogHelper.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.app;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+import com.android.media.update.R;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+final class MediaRouteDialogHelper {
+ /**
+ * The framework should set the dialog width properly, but somehow it doesn't work, hence
+ * duplicating a similar logic here to determine the appropriate dialog width.
+ */
+ public static int getDialogWidth(Context context) {
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ boolean isPortrait = metrics.widthPixels < metrics.heightPixels;
+
+ TypedValue value = new TypedValue();
+ context.getResources().getValue(isPortrait ? R.dimen.mr_dialog_fixed_width_minor
+ : R.dimen.mr_dialog_fixed_width_major, value, true);
+ if (value.type == TypedValue.TYPE_DIMENSION) {
+ return (int) value.getDimension(metrics);
+ } else if (value.type == TypedValue.TYPE_FRACTION) {
+ return (int) value.getFraction(metrics.widthPixels, metrics.widthPixels);
+ }
+ return ViewGroup.LayoutParams.WRAP_CONTENT;
+ }
+
+ /**
+ * Compares two lists regardless of order.
+ *
+ * @param list1 A list
+ * @param list2 A list to be compared with {@code list1}
+ * @return True if two lists have exactly same items regardless of order, false otherwise.
+ */
+ public static <E> boolean listUnorderedEquals(List<E> list1, List<E> list2) {
+ HashSet<E> set1 = new HashSet<>(list1);
+ HashSet<E> set2 = new HashSet<>(list2);
+ return set1.equals(set2);
+ }
+
+ /**
+ * Compares two lists and returns a set of items which exist
+ * after-list but before-list, which means newly added items.
+ *
+ * @param before A list
+ * @param after A list to be compared with {@code before}
+ * @return A set of items which contains newly added items while
+ * comparing {@code after} to {@code before}.
+ */
+ public static <E> Set<E> getItemsAdded(List<E> before, List<E> after) {
+ HashSet<E> set = new HashSet<>(after);
+ set.removeAll(before);
+ return set;
+ }
+
+ /**
+ * Compares two lists and returns a set of items which exist
+ * before-list but after-list, which means removed items.
+ *
+ * @param before A list
+ * @param after A list to be compared with {@code before}
+ * @return A set of items which contains removed items while
+ * comparing {@code after} to {@code before}.
+ */
+ public static <E> Set<E> getItemsRemoved(List<E> before, List<E> after) {
+ HashSet<E> set = new HashSet<>(before);
+ set.removeAll(after);
+ return set;
+ }
+
+ /**
+ * Generates an item-Rect map which indicates where member
+ * items are located in the given ListView.
+ *
+ * @param listView A list view
+ * @param adapter An array adapter which contains an array of items.
+ * @return A map of items and bounds of their views located in the given list view.
+ */
+ public static <E> HashMap<E, Rect> getItemBoundMap(ListView listView,
+ ArrayAdapter<E> adapter) {
+ HashMap<E, Rect> itemBoundMap = new HashMap<>();
+ int firstVisiblePosition = listView.getFirstVisiblePosition();
+ for (int i = 0; i < listView.getChildCount(); ++i) {
+ int position = firstVisiblePosition + i;
+ E item = adapter.getItem(position);
+ View view = listView.getChildAt(i);
+ itemBoundMap.put(item,
+ new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()));
+ }
+ return itemBoundMap;
+ }
+
+ /**
+ * Generates an item-BitmapDrawable map which stores snapshots
+ * of member items in the given ListView.
+ *
+ * @param context A context
+ * @param listView A list view
+ * @param adapter An array adapter which contains an array of items.
+ * @return A map of items and snapshots of their views in the given list view.
+ */
+ public static <E> HashMap<E, BitmapDrawable> getItemBitmapMap(Context context,
+ ListView listView, ArrayAdapter<E> adapter) {
+ HashMap<E, BitmapDrawable> itemBitmapMap = new HashMap<>();
+ int firstVisiblePosition = listView.getFirstVisiblePosition();
+ for (int i = 0; i < listView.getChildCount(); ++i) {
+ int position = firstVisiblePosition + i;
+ E item = adapter.getItem(position);
+ View view = listView.getChildAt(i);
+ itemBitmapMap.put(item, getViewBitmap(context, view));
+ }
+ return itemBitmapMap;
+ }
+
+ private static BitmapDrawable getViewBitmap(Context context, View view) {
+ Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(),
+ Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ view.draw(canvas);
+ return new BitmapDrawable(context.getResources(), bitmap);
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteDiscoveryFragment.java b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteDiscoveryFragment.java
new file mode 100644
index 0000000..02ee118
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteDiscoveryFragment.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.app;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+
+import com.android.support.mediarouter.media.MediaRouter;
+import com.android.support.mediarouter.media.MediaRouteSelector;
+
+/**
+ * Media route discovery fragment.
+ * <p>
+ * This fragment takes care of registering a callback for media route discovery
+ * during the {@link Fragment#onStart onStart()} phase
+ * and removing it during the {@link Fragment#onStop onStop()} phase.
+ * </p><p>
+ * The application must supply a route selector to specify the kinds of routes
+ * to discover. The application may also override {@link #onCreateCallback} to
+ * provide the {@link MediaRouter} callback to register.
+ * </p><p>
+ * Note that the discovery callback makes the application be connected with all the
+ * {@link android.support.v7.media.MediaRouteProviderService media route provider services}
+ * while it is registered.
+ * </p>
+ */
+public class MediaRouteDiscoveryFragment extends Fragment {
+ private final String ARGUMENT_SELECTOR = "selector";
+
+ private MediaRouter mRouter;
+ private MediaRouteSelector mSelector;
+ private MediaRouter.Callback mCallback;
+
+ public MediaRouteDiscoveryFragment() {
+ }
+
+ /**
+ * Gets the media router instance.
+ */
+ public MediaRouter getMediaRouter() {
+ ensureRouter();
+ return mRouter;
+ }
+
+ private void ensureRouter() {
+ if (mRouter == null) {
+ mRouter = MediaRouter.getInstance(getContext());
+ }
+ }
+
+ /**
+ * Gets the media route selector for filtering the routes to be discovered.
+ *
+ * @return The selector, never null.
+ */
+ public MediaRouteSelector getRouteSelector() {
+ ensureRouteSelector();
+ return mSelector;
+ }
+
+ /**
+ * Sets the media route selector for filtering the routes to be discovered.
+ * This method must be called before the fragment is added.
+ *
+ * @param selector The selector to set.
+ */
+ public void setRouteSelector(MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ ensureRouteSelector();
+ if (!mSelector.equals(selector)) {
+ mSelector = selector;
+
+ Bundle args = getArguments();
+ if (args == null) {
+ args = new Bundle();
+ }
+ args.putBundle(ARGUMENT_SELECTOR, selector.asBundle());
+ setArguments(args);
+
+ if (mCallback != null) {
+ mRouter.removeCallback(mCallback);
+ mRouter.addCallback(mSelector, mCallback, onPrepareCallbackFlags());
+ }
+ }
+ }
+
+ private void ensureRouteSelector() {
+ if (mSelector == null) {
+ Bundle args = getArguments();
+ if (args != null) {
+ mSelector = MediaRouteSelector.fromBundle(args.getBundle(ARGUMENT_SELECTOR));
+ }
+ if (mSelector == null) {
+ mSelector = MediaRouteSelector.EMPTY;
+ }
+ }
+ }
+
+ /**
+ * Called to create the {@link android.support.v7.media.MediaRouter.Callback callback}
+ * that will be registered.
+ * <p>
+ * The default callback does nothing. The application may override this method to
+ * supply its own callback.
+ * </p>
+ *
+ * @return The new callback, or null if no callback should be registered.
+ */
+ public MediaRouter.Callback onCreateCallback() {
+ return new MediaRouter.Callback() { };
+ }
+
+ /**
+ * Called to prepare the callback flags that will be used when the
+ * {@link android.support.v7.media.MediaRouter.Callback callback} is registered.
+ * <p>
+ * The default implementation returns {@link MediaRouter#CALLBACK_FLAG_REQUEST_DISCOVERY}.
+ * </p>
+ *
+ * @return The desired callback flags.
+ */
+ public int onPrepareCallbackFlags() {
+ return MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ ensureRouteSelector();
+ ensureRouter();
+ mCallback = onCreateCallback();
+ if (mCallback != null) {
+ mRouter.addCallback(mSelector, mCallback, onPrepareCallbackFlags());
+ }
+ }
+
+ @Override
+ public void onStop() {
+ if (mCallback != null) {
+ mRouter.removeCallback(mCallback);
+ mCallback = null;
+ }
+
+ super.onStop();
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteExpandCollapseButton.java b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteExpandCollapseButton.java
new file mode 100644
index 0000000..392b39d
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteExpandCollapseButton.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.app;
+
+import android.content.Context;
+import android.graphics.ColorFilter;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.drawable.AnimationDrawable;
+import android.support.v4.content.ContextCompat;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageButton;
+
+import com.android.media.update.R;
+
+/**
+ * Chevron/Caret button to expand/collapse group volume list with animation.
+ */
+class MediaRouteExpandCollapseButton extends ImageButton {
+ final AnimationDrawable mExpandAnimationDrawable;
+ final AnimationDrawable mCollapseAnimationDrawable;
+ final String mExpandGroupDescription;
+ final String mCollapseGroupDescription;
+ boolean mIsGroupExpanded;
+ OnClickListener mListener;
+
+ public MediaRouteExpandCollapseButton(Context context) {
+ this(context, null);
+ }
+
+ public MediaRouteExpandCollapseButton(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public MediaRouteExpandCollapseButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mExpandAnimationDrawable = (AnimationDrawable) ContextCompat.getDrawable(
+ context, R.drawable.mr_group_expand);
+ mCollapseAnimationDrawable = (AnimationDrawable) ContextCompat.getDrawable(
+ context, R.drawable.mr_group_collapse);
+
+ ColorFilter filter = new PorterDuffColorFilter(
+ MediaRouterThemeHelper.getControllerColor(context, defStyleAttr),
+ PorterDuff.Mode.SRC_IN);
+ mExpandAnimationDrawable.setColorFilter(filter);
+ mCollapseAnimationDrawable.setColorFilter(filter);
+
+ mExpandGroupDescription = context.getString(R.string.mr_controller_expand_group);
+ mCollapseGroupDescription = context.getString(R.string.mr_controller_collapse_group);
+
+ setImageDrawable(mExpandAnimationDrawable.getFrame(0));
+ setContentDescription(mExpandGroupDescription);
+
+ super.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ mIsGroupExpanded = !mIsGroupExpanded;
+ if (mIsGroupExpanded) {
+ setImageDrawable(mExpandAnimationDrawable);
+ mExpandAnimationDrawable.start();
+ setContentDescription(mCollapseGroupDescription);
+ } else {
+ setImageDrawable(mCollapseAnimationDrawable);
+ mCollapseAnimationDrawable.start();
+ setContentDescription(mExpandGroupDescription);
+ }
+ if (mListener != null) {
+ mListener.onClick(view);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void setOnClickListener(OnClickListener listener) {
+ mListener = listener;
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteVolumeSlider.java b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteVolumeSlider.java
new file mode 100644
index 0000000..7a34fb5
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouteVolumeSlider.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.app;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.support.v7.widget.AppCompatSeekBar;
+import android.util.AttributeSet;
+import android.util.Log;
+
+/**
+ * Volume slider with showing, hiding, and applying alpha supports to the thumb.
+ */
+class MediaRouteVolumeSlider extends AppCompatSeekBar {
+ private static final String TAG = "MediaRouteVolumeSlider";
+
+ private final float mDisabledAlpha;
+
+ private boolean mHideThumb;
+ private Drawable mThumb;
+ private int mColor;
+
+ public MediaRouteVolumeSlider(Context context) {
+ this(context, null);
+ }
+
+ public MediaRouteVolumeSlider(Context context, AttributeSet attrs) {
+ this(context, attrs, android.support.v7.appcompat.R.attr.seekBarStyle);
+ }
+
+ public MediaRouteVolumeSlider(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mDisabledAlpha = MediaRouterThemeHelper.getDisabledAlpha(context);
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ int alpha = isEnabled() ? 0xFF : (int) (0xFF * mDisabledAlpha);
+
+ // The thumb drawable is a collection of drawables and its current drawables are changed per
+ // state. Apply the color filter and alpha on every state change.
+ mThumb.setColorFilter(mColor, PorterDuff.Mode.SRC_IN);
+ mThumb.setAlpha(alpha);
+
+ getProgressDrawable().setColorFilter(mColor, PorterDuff.Mode.SRC_IN);
+ getProgressDrawable().setAlpha(alpha);
+ }
+
+ @Override
+ public void setThumb(Drawable thumb) {
+ mThumb = thumb;
+ super.setThumb(mHideThumb ? null : mThumb);
+ }
+
+ /**
+ * Sets whether to show or hide thumb.
+ */
+ public void setHideThumb(boolean hideThumb) {
+ if (mHideThumb == hideThumb) {
+ return;
+ }
+ mHideThumb = hideThumb;
+ super.setThumb(mHideThumb ? null : mThumb);
+ }
+
+ /**
+ * Sets the volume slider color. The change takes effect next time drawable state is changed.
+ * <p>
+ * The color cannot be translucent, otherwise the underlying progress bar will be seen through
+ * the thumb.
+ * </p>
+ */
+ public void setColor(int color) {
+ if (mColor == color) {
+ return;
+ }
+ if (Color.alpha(color) != 0xFF) {
+ Log.e(TAG, "Volume slider color cannot be translucent: #" + Integer.toHexString(color));
+ }
+ mColor = color;
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouterThemeHelper.java b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouterThemeHelper.java
new file mode 100644
index 0000000..b4b49df
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/app/MediaRouterThemeHelper.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.app;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.support.annotation.IntDef;
+import android.support.v4.graphics.ColorUtils;
+import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
+import android.view.View;
+
+import com.android.media.update.R;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+final class MediaRouterThemeHelper {
+ private static final float MIN_CONTRAST = 3.0f;
+
+ @IntDef({COLOR_DARK_ON_LIGHT_BACKGROUND, COLOR_WHITE_ON_DARK_BACKGROUND})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface ControllerColorType {}
+
+ static final int COLOR_DARK_ON_LIGHT_BACKGROUND = 0xDE000000; /* Opacity of 87% */
+ static final int COLOR_WHITE_ON_DARK_BACKGROUND = Color.WHITE;
+
+ private MediaRouterThemeHelper() {
+ }
+
+ static Context createThemedButtonContext(Context context) {
+ // Apply base Media Router theme.
+ context = new ContextThemeWrapper(context, getRouterThemeId(context));
+
+ // Apply custom Media Router theme.
+ int style = getThemeResource(context, R.attr.mediaRouteTheme);
+ if (style != 0) {
+ context = new ContextThemeWrapper(context, style);
+ }
+
+ return context;
+ }
+
+ /*
+ * The following two methods are to be used in conjunction. They should be used to prepare
+ * the context and theme for a super class constructor (the latter method relies on the
+ * former method to properly prepare the context):
+ * super(context = createThemedDialogContext(context, theme),
+ * createThemedDialogStyle(context));
+ *
+ * It will apply theme in the following order (style lookups will be done in reverse):
+ * 1) Current theme
+ * 2) Supplied theme
+ * 3) Base Media Router theme
+ * 4) Custom Media Router theme, if provided
+ */
+ static Context createThemedDialogContext(Context context, int theme, boolean alertDialog) {
+ // 1) Current theme is already applied to the context
+
+ // 2) If no theme is supplied, look it up from the context (dialogTheme/alertDialogTheme)
+ if (theme == 0) {
+ theme = getThemeResource(context, !alertDialog
+ ? android.support.v7.appcompat.R.attr.dialogTheme
+ : android.support.v7.appcompat.R.attr.alertDialogTheme);
+ }
+ // Apply it
+ context = new ContextThemeWrapper(context, theme);
+
+ // 3) If a custom Media Router theme is provided then apply the base theme
+ if (getThemeResource(context, R.attr.mediaRouteTheme) != 0) {
+ context = new ContextThemeWrapper(context, getRouterThemeId(context));
+ }
+
+ return context;
+ }
+ // This method should be used in conjunction with the previous method.
+ static int createThemedDialogStyle(Context context) {
+ // 4) Apply the custom Media Router theme
+ int theme = getThemeResource(context, R.attr.mediaRouteTheme);
+ if (theme == 0) {
+ // 3) No custom MediaRouther theme was provided so apply the base theme instead
+ theme = getRouterThemeId(context);
+ }
+
+ return theme;
+ }
+ // END. Previous two methods should be used in conjunction.
+
+ static int getThemeResource(Context context, int attr) {
+ TypedValue value = new TypedValue();
+ return context.getTheme().resolveAttribute(attr, value, true) ? value.resourceId : 0;
+ }
+
+ static float getDisabledAlpha(Context context) {
+ TypedValue value = new TypedValue();
+ return context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, value, true)
+ ? value.getFloat() : 0.5f;
+ }
+
+ static @ControllerColorType int getControllerColor(Context context, int style) {
+ int primaryColor = getThemeColor(context, style,
+ android.support.v7.appcompat.R.attr.colorPrimary);
+ if (primaryColor == 0) {
+ primaryColor = getThemeColor(context, style, android.R.attr.colorPrimary);
+ if (primaryColor == 0) {
+ primaryColor = 0xFF000000;
+ }
+ }
+ if (ColorUtils.calculateContrast(COLOR_WHITE_ON_DARK_BACKGROUND, primaryColor)
+ >= MIN_CONTRAST) {
+ return COLOR_WHITE_ON_DARK_BACKGROUND;
+ }
+ return COLOR_DARK_ON_LIGHT_BACKGROUND;
+ }
+
+ static int getButtonTextColor(Context context) {
+ int primaryColor = getThemeColor(context, 0,
+ android.support.v7.appcompat.R.attr.colorPrimary);
+ int backgroundColor = getThemeColor(context, 0, android.R.attr.colorBackground);
+
+ if (ColorUtils.calculateContrast(primaryColor, backgroundColor) < MIN_CONTRAST) {
+ // Default to colorAccent if the contrast ratio is low.
+ return getThemeColor(context, 0, android.support.v7.appcompat.R.attr.colorAccent);
+ }
+ return primaryColor;
+ }
+
+ static void setMediaControlsBackgroundColor(
+ Context context, View mainControls, View groupControls, boolean hasGroup) {
+ int primaryColor = getThemeColor(context, 0,
+ android.support.v7.appcompat.R.attr.colorPrimary);
+ int primaryDarkColor = getThemeColor(context, 0,
+ android.support.v7.appcompat.R.attr.colorPrimaryDark);
+ if (hasGroup && getControllerColor(context, 0) == COLOR_DARK_ON_LIGHT_BACKGROUND) {
+ // Instead of showing dark controls in a possibly dark (i.e. the primary dark), model
+ // the white dialog and use the primary color for the group controls.
+ primaryDarkColor = primaryColor;
+ primaryColor = Color.WHITE;
+ }
+ mainControls.setBackgroundColor(primaryColor);
+ groupControls.setBackgroundColor(primaryDarkColor);
+ // Also store the background colors to the view tags. They are used in
+ // setVolumeSliderColor() below.
+ mainControls.setTag(primaryColor);
+ groupControls.setTag(primaryDarkColor);
+ }
+
+ static void setVolumeSliderColor(
+ Context context, MediaRouteVolumeSlider volumeSlider, View backgroundView) {
+ int controllerColor = getControllerColor(context, 0);
+ if (Color.alpha(controllerColor) != 0xFF) {
+ // Composite with the background in order not to show the underlying progress bar
+ // through the thumb.
+ int backgroundColor = (int) backgroundView.getTag();
+ controllerColor = ColorUtils.compositeColors(controllerColor, backgroundColor);
+ }
+ volumeSlider.setColor(controllerColor);
+ }
+
+ private static boolean isLightTheme(Context context) {
+ TypedValue value = new TypedValue();
+ return context.getTheme().resolveAttribute(android.support.v7.appcompat.R.attr.isLightTheme,
+ value, true) && value.data != 0;
+ }
+
+ private static int getThemeColor(Context context, int style, int attr) {
+ if (style != 0) {
+ int[] attrs = { attr };
+ TypedArray ta = context.obtainStyledAttributes(style, attrs);
+ int color = ta.getColor(0, 0);
+ ta.recycle();
+ if (color != 0) {
+ return color;
+ }
+ }
+ TypedValue value = new TypedValue();
+ context.getTheme().resolveAttribute(attr, value, true);
+ if (value.resourceId != 0) {
+ return context.getResources().getColor(value.resourceId);
+ }
+ return value.data;
+ }
+
+ private static int getRouterThemeId(Context context) {
+ int themeId;
+ if (isLightTheme(context)) {
+ if (getControllerColor(context, 0) == COLOR_DARK_ON_LIGHT_BACKGROUND) {
+ themeId = R.style.Theme_MediaRouter_Light;
+ } else {
+ themeId = R.style.Theme_MediaRouter_Light_DarkControlPanel;
+ }
+ } else {
+ if (getControllerColor(context, 0) == COLOR_DARK_ON_LIGHT_BACKGROUND) {
+ themeId = R.style.Theme_MediaRouter_LightControlPanel;
+ } else {
+ themeId = R.style.Theme_MediaRouter;
+ }
+ }
+ return themeId;
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/app/OverlayListView.java b/packages/MediaComponents/src/com/android/support/mediarouter/app/OverlayListView.java
new file mode 100644
index 0000000..59019ff
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/app/OverlayListView.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.app;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.AttributeSet;
+import android.view.animation.Interpolator;
+import android.widget.ListView;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * A ListView which has an additional overlay layer. {@link BitmapDrawable}
+ * can be added to the layer and can be animated.
+ */
+final class OverlayListView extends ListView {
+ private final List<OverlayObject> mOverlayObjects = new ArrayList<>();
+
+ public OverlayListView(Context context) {
+ super(context);
+ }
+
+ public OverlayListView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public OverlayListView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ /**
+ * Adds an object to the overlay layer.
+ *
+ * @param object An object to be added.
+ */
+ public void addOverlayObject(OverlayObject object) {
+ mOverlayObjects.add(object);
+ }
+
+ /**
+ * Starts all animations of objects in the overlay layer.
+ */
+ public void startAnimationAll() {
+ for (OverlayObject object : mOverlayObjects) {
+ if (!object.isAnimationStarted()) {
+ object.startAnimation(getDrawingTime());
+ }
+ }
+ }
+
+ /**
+ * Stops all animations of objects in the overlay layer.
+ */
+ public void stopAnimationAll() {
+ for (OverlayObject object : mOverlayObjects) {
+ object.stopAnimation();
+ }
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mOverlayObjects.size() > 0) {
+ Iterator<OverlayObject> it = mOverlayObjects.iterator();
+ while (it.hasNext()) {
+ OverlayObject object = it.next();
+ BitmapDrawable bitmap = object.getBitmapDrawable();
+ if (bitmap != null) {
+ bitmap.draw(canvas);
+ }
+ if (!object.update(getDrawingTime())) {
+ it.remove();
+ }
+ }
+ }
+ }
+
+ /**
+ * A class that represents an object to be shown in the overlay layer.
+ */
+ public static class OverlayObject {
+ private BitmapDrawable mBitmap;
+ private float mCurrentAlpha = 1.0f;
+ private Rect mCurrentBounds;
+ private Interpolator mInterpolator;
+ private long mDuration;
+ private Rect mStartRect;
+ private int mDeltaY;
+ private float mStartAlpha = 1.0f;
+ private float mEndAlpha = 1.0f;
+ private long mStartTime;
+ private boolean mIsAnimationStarted;
+ private boolean mIsAnimationEnded;
+ private OnAnimationEndListener mListener;
+
+ public OverlayObject(BitmapDrawable bitmap, Rect startRect) {
+ mBitmap = bitmap;
+ mStartRect = startRect;
+ mCurrentBounds = new Rect(startRect);
+ if (mBitmap != null && mCurrentBounds != null) {
+ mBitmap.setAlpha((int) (mCurrentAlpha * 255));
+ mBitmap.setBounds(mCurrentBounds);
+ }
+ }
+
+ /**
+ * Returns the bitmap that this object represents.
+ *
+ * @return BitmapDrawable that this object has.
+ */
+ public BitmapDrawable getBitmapDrawable() {
+ return mBitmap;
+ }
+
+ /**
+ * Returns the started status of the animation.
+ *
+ * @return True if the animation has started, false otherwise.
+ */
+ public boolean isAnimationStarted() {
+ return mIsAnimationStarted;
+ }
+
+ /**
+ * Sets animation for varying alpha.
+ *
+ * @param startAlpha Starting alpha value for the animation, where 1.0 means
+ * fully opaque and 0.0 means fully transparent.
+ * @param endAlpha Ending alpha value for the animation.
+ * @return This OverlayObject to allow for chaining of calls.
+ */
+ public OverlayObject setAlphaAnimation(float startAlpha, float endAlpha) {
+ mStartAlpha = startAlpha;
+ mEndAlpha = endAlpha;
+ return this;
+ }
+
+ /**
+ * Sets animation for moving objects vertically.
+ *
+ * @param deltaY Distance to move in pixels.
+ * @return This OverlayObject to allow for chaining of calls.
+ */
+ public OverlayObject setTranslateYAnimation(int deltaY) {
+ mDeltaY = deltaY;
+ return this;
+ }
+
+ /**
+ * Sets how long the animation will last.
+ *
+ * @param duration Duration in milliseconds
+ * @return This OverlayObject to allow for chaining of calls.
+ */
+ public OverlayObject setDuration(long duration) {
+ mDuration = duration;
+ return this;
+ }
+
+ /**
+ * Sets the acceleration curve for this animation.
+ *
+ * @param interpolator The interpolator which defines the acceleration curve
+ * @return This OverlayObject to allow for chaining of calls.
+ */
+ public OverlayObject setInterpolator(Interpolator interpolator) {
+ mInterpolator = interpolator;
+ return this;
+ }
+
+ /**
+ * Binds an animation end listener to the animation.
+ *
+ * @param listener the animation end listener to be notified.
+ * @return This OverlayObject to allow for chaining of calls.
+ */
+ public OverlayObject setAnimationEndListener(OnAnimationEndListener listener) {
+ mListener = listener;
+ return this;
+ }
+
+ /**
+ * Starts the animation and sets the start time.
+ *
+ * @param startTime Start time to be set in Millis
+ */
+ public void startAnimation(long startTime) {
+ mStartTime = startTime;
+ mIsAnimationStarted = true;
+ }
+
+ /**
+ * Stops the animation.
+ */
+ public void stopAnimation() {
+ mIsAnimationStarted = true;
+ mIsAnimationEnded = true;
+ if (mListener != null) {
+ mListener.onAnimationEnd();
+ }
+ }
+
+ /**
+ * Calculates and updates current bounds and alpha value.
+ *
+ * @param currentTime Current time.in millis
+ */
+ public boolean update(long currentTime) {
+ if (mIsAnimationEnded) {
+ return false;
+ }
+ float normalizedTime = (currentTime - mStartTime) / (float) mDuration;
+ normalizedTime = Math.max(0.0f, Math.min(1.0f, normalizedTime));
+ if (!mIsAnimationStarted) {
+ normalizedTime = 0.0f;
+ }
+ float interpolatedTime = (mInterpolator == null) ? normalizedTime
+ : mInterpolator.getInterpolation(normalizedTime);
+ int deltaY = (int) (mDeltaY * interpolatedTime);
+ mCurrentBounds.top = mStartRect.top + deltaY;
+ mCurrentBounds.bottom = mStartRect.bottom + deltaY;
+ mCurrentAlpha = mStartAlpha + (mEndAlpha - mStartAlpha) * interpolatedTime;
+ if (mBitmap != null && mCurrentBounds != null) {
+ mBitmap.setAlpha((int) (mCurrentAlpha * 255));
+ mBitmap.setBounds(mCurrentBounds);
+ }
+ if (mIsAnimationStarted && normalizedTime >= 1.0f) {
+ mIsAnimationEnded = true;
+ if (mListener != null) {
+ mListener.onAnimationEnd();
+ }
+ }
+ return !mIsAnimationEnded;
+ }
+
+ /**
+ * An animation listener that receives notifications when the animation ends.
+ */
+ public interface OnAnimationEndListener {
+ /**
+ * Notifies the end of the animation.
+ */
+ public void onAnimationEnd();
+ }
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/jellybean-mr1/MediaRouterJellybeanMr1.java b/packages/MediaComponents/src/com/android/support/mediarouter/jellybean-mr1/MediaRouterJellybeanMr1.java
new file mode 100644
index 0000000..f8539bd
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/jellybean-mr1/MediaRouterJellybeanMr1.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.media;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.os.Build;
+import android.os.Handler;
+import android.support.annotation.RequiresApi;
+import android.util.Log;
+import android.view.Display;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+// @@RequiresApi(17)
+final class MediaRouterJellybeanMr1 {
+ private static final String TAG = "MediaRouterJellybeanMr1";
+
+ public static Object createCallback(Callback callback) {
+ return new CallbackProxy<Callback>(callback);
+ }
+
+ public static final class RouteInfo {
+ public static boolean isEnabled(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).isEnabled();
+ }
+
+ public static Display getPresentationDisplay(Object routeObj) {
+ // android.media.MediaRouter.RouteInfo.getPresentationDisplay() was
+ // added in API 17. However, some factory releases of JB MR1 missed it.
+ try {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getPresentationDisplay();
+ } catch (NoSuchMethodError ex) {
+ Log.w(TAG, "Cannot get presentation display for the route.", ex);
+ }
+ return null;
+ }
+ }
+
+ public static interface Callback extends MediaRouterJellybean.Callback {
+ public void onRoutePresentationDisplayChanged(Object routeObj);
+ }
+
+ /**
+ * Workaround the fact that the version of MediaRouter.addCallback() that accepts a
+ * flag to perform an active scan does not exist in JB MR1 so we need to force
+ * wifi display scans directly through the DisplayManager.
+ * Do not use on JB MR2 and above.
+ */
+ public static final class ActiveScanWorkaround implements Runnable {
+ // Time between wifi display scans when actively scanning in milliseconds.
+ private static final int WIFI_DISPLAY_SCAN_INTERVAL = 15000;
+
+ private final DisplayManager mDisplayManager;
+ private final Handler mHandler;
+ private Method mScanWifiDisplaysMethod;
+
+ private boolean mActivelyScanningWifiDisplays;
+
+ public ActiveScanWorkaround(Context context, Handler handler) {
+ if (Build.VERSION.SDK_INT != 17) {
+ throw new UnsupportedOperationException();
+ }
+
+ mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
+ mHandler = handler;
+ try {
+ mScanWifiDisplaysMethod = DisplayManager.class.getMethod("scanWifiDisplays");
+ } catch (NoSuchMethodException ex) {
+ }
+ }
+
+ public void setActiveScanRouteTypes(int routeTypes) {
+ // On JB MR1, there is no API to scan wifi display routes.
+ // Instead we must make a direct call into the DisplayManager to scan
+ // wifi displays on this version but only when live video routes are requested.
+ // See also the JellybeanMr2Impl implementation of this method.
+ // This was fixed in JB MR2 by adding a new overload of addCallback() to
+ // enable active scanning on request.
+ if ((routeTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO) != 0) {
+ if (!mActivelyScanningWifiDisplays) {
+ if (mScanWifiDisplaysMethod != null) {
+ mActivelyScanningWifiDisplays = true;
+ mHandler.post(this);
+ } else {
+ Log.w(TAG, "Cannot scan for wifi displays because the "
+ + "DisplayManager.scanWifiDisplays() method is "
+ + "not available on this device.");
+ }
+ }
+ } else {
+ if (mActivelyScanningWifiDisplays) {
+ mActivelyScanningWifiDisplays = false;
+ mHandler.removeCallbacks(this);
+ }
+ }
+ }
+
+ @Override
+ public void run() {
+ if (mActivelyScanningWifiDisplays) {
+ try {
+ mScanWifiDisplaysMethod.invoke(mDisplayManager);
+ } catch (IllegalAccessException ex) {
+ Log.w(TAG, "Cannot scan for wifi displays.", ex);
+ } catch (InvocationTargetException ex) {
+ Log.w(TAG, "Cannot scan for wifi displays.", ex);
+ }
+ mHandler.postDelayed(this, WIFI_DISPLAY_SCAN_INTERVAL);
+ }
+ }
+ }
+
+ /**
+ * Workaround the fact that the isConnecting() method does not exist in JB MR1.
+ * Do not use on JB MR2 and above.
+ */
+ public static final class IsConnectingWorkaround {
+ private Method mGetStatusCodeMethod;
+ private int mStatusConnecting;
+
+ public IsConnectingWorkaround() {
+ if (Build.VERSION.SDK_INT != 17) {
+ throw new UnsupportedOperationException();
+ }
+
+ try {
+ Field statusConnectingField =
+ android.media.MediaRouter.RouteInfo.class.getField("STATUS_CONNECTING");
+ mStatusConnecting = statusConnectingField.getInt(null);
+ mGetStatusCodeMethod =
+ android.media.MediaRouter.RouteInfo.class.getMethod("getStatusCode");
+ } catch (NoSuchFieldException ex) {
+ } catch (NoSuchMethodException ex) {
+ } catch (IllegalAccessException ex) {
+ }
+ }
+
+ public boolean isConnecting(Object routeObj) {
+ android.media.MediaRouter.RouteInfo route =
+ (android.media.MediaRouter.RouteInfo)routeObj;
+
+ if (mGetStatusCodeMethod != null) {
+ try {
+ int statusCode = (Integer)mGetStatusCodeMethod.invoke(route);
+ return statusCode == mStatusConnecting;
+ } catch (IllegalAccessException ex) {
+ } catch (InvocationTargetException ex) {
+ }
+ }
+
+ // Assume not connecting.
+ return false;
+ }
+ }
+
+ static class CallbackProxy<T extends Callback>
+ extends MediaRouterJellybean.CallbackProxy<T> {
+ public CallbackProxy(T callback) {
+ super(callback);
+ }
+
+ @Override
+ public void onRoutePresentationDisplayChanged(android.media.MediaRouter router,
+ android.media.MediaRouter.RouteInfo route) {
+ mCallback.onRoutePresentationDisplayChanged(route);
+ }
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/jellybean-mr2/MediaRouterJellybeanMr2.java b/packages/MediaComponents/src/com/android/support/mediarouter/jellybean-mr2/MediaRouterJellybeanMr2.java
new file mode 100644
index 0000000..1103549
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/jellybean-mr2/MediaRouterJellybeanMr2.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.media;
+
+// @@RequiresApi(18)
+final class MediaRouterJellybeanMr2 {
+ public static Object getDefaultRoute(Object routerObj) {
+ return ((android.media.MediaRouter)routerObj).getDefaultRoute();
+ }
+
+ public static void addCallback(Object routerObj, int types, Object callbackObj, int flags) {
+ ((android.media.MediaRouter)routerObj).addCallback(types,
+ (android.media.MediaRouter.Callback)callbackObj, flags);
+ }
+
+ public static final class RouteInfo {
+ public static CharSequence getDescription(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getDescription();
+ }
+
+ public static boolean isConnecting(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).isConnecting();
+ }
+ }
+
+ public static final class UserRouteInfo {
+ public static void setDescription(Object routeObj, CharSequence description) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setDescription(description);
+ }
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/jellybean/MediaRouterJellybean.java b/packages/MediaComponents/src/com/android/support/mediarouter/jellybean/MediaRouterJellybean.java
new file mode 100644
index 0000000..0bb59b8
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/jellybean/MediaRouterJellybean.java
@@ -0,0 +1,462 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.media;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
+import android.os.Build;
+import android.util.Log;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+// @@RequiresApi(16)
+final class MediaRouterJellybean {
+ private static final String TAG = "MediaRouterJellybean";
+
+ // android.media.AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP = 0x80;
+ // android.media.AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES = 0x100;
+ // android.media.AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER = 0x200;
+ public static final int DEVICE_OUT_BLUETOOTH = 0x80 | 0x100 | 0x200;
+
+ public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1;
+ public static final int ROUTE_TYPE_LIVE_VIDEO = 0x2;
+ public static final int ROUTE_TYPE_USER = 0x00800000;
+
+ public static final int ALL_ROUTE_TYPES =
+ MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO
+ | MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO
+ | MediaRouterJellybean.ROUTE_TYPE_USER;
+
+ public static Object getMediaRouter(Context context) {
+ return context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public static List getRoutes(Object routerObj) {
+ final android.media.MediaRouter router = (android.media.MediaRouter)routerObj;
+ final int count = router.getRouteCount();
+ List out = new ArrayList(count);
+ for (int i = 0; i < count; i++) {
+ out.add(router.getRouteAt(i));
+ }
+ return out;
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public static List getCategories(Object routerObj) {
+ final android.media.MediaRouter router = (android.media.MediaRouter)routerObj;
+ final int count = router.getCategoryCount();
+ List out = new ArrayList(count);
+ for (int i = 0; i < count; i++) {
+ out.add(router.getCategoryAt(i));
+ }
+ return out;
+ }
+
+ public static Object getSelectedRoute(Object routerObj, int type) {
+ return ((android.media.MediaRouter)routerObj).getSelectedRoute(type);
+ }
+
+ public static void selectRoute(Object routerObj, int types, Object routeObj) {
+ ((android.media.MediaRouter)routerObj).selectRoute(types,
+ (android.media.MediaRouter.RouteInfo)routeObj);
+ }
+
+ public static void addCallback(Object routerObj, int types, Object callbackObj) {
+ ((android.media.MediaRouter)routerObj).addCallback(types,
+ (android.media.MediaRouter.Callback)callbackObj);
+ }
+
+ public static void removeCallback(Object routerObj, Object callbackObj) {
+ ((android.media.MediaRouter)routerObj).removeCallback(
+ (android.media.MediaRouter.Callback)callbackObj);
+ }
+
+ public static Object createRouteCategory(Object routerObj,
+ String name, boolean isGroupable) {
+ return ((android.media.MediaRouter)routerObj).createRouteCategory(name, isGroupable);
+ }
+
+ public static Object createUserRoute(Object routerObj, Object categoryObj) {
+ return ((android.media.MediaRouter)routerObj).createUserRoute(
+ (android.media.MediaRouter.RouteCategory)categoryObj);
+ }
+
+ public static void addUserRoute(Object routerObj, Object routeObj) {
+ ((android.media.MediaRouter)routerObj).addUserRoute(
+ (android.media.MediaRouter.UserRouteInfo)routeObj);
+ }
+
+ public static void removeUserRoute(Object routerObj, Object routeObj) {
+ ((android.media.MediaRouter)routerObj).removeUserRoute(
+ (android.media.MediaRouter.UserRouteInfo)routeObj);
+ }
+
+ public static Object createCallback(Callback callback) {
+ return new CallbackProxy<Callback>(callback);
+ }
+
+ public static Object createVolumeCallback(VolumeCallback callback) {
+ return new VolumeCallbackProxy<VolumeCallback>(callback);
+ }
+
+ static boolean checkRoutedToBluetooth(Context context) {
+ try {
+ AudioManager audioManager = (AudioManager) context.getSystemService(
+ Context.AUDIO_SERVICE);
+ Method method = audioManager.getClass().getDeclaredMethod(
+ "getDevicesForStream", int.class);
+ int device = (Integer) method.invoke(audioManager, AudioManager.STREAM_MUSIC);
+ return (device & DEVICE_OUT_BLUETOOTH) != 0;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ public static final class RouteInfo {
+ public static CharSequence getName(Object routeObj, Context context) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getName(context);
+ }
+
+ public static CharSequence getStatus(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getStatus();
+ }
+
+ public static int getSupportedTypes(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getSupportedTypes();
+ }
+
+ public static Object getCategory(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getCategory();
+ }
+
+ public static Drawable getIconDrawable(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getIconDrawable();
+ }
+
+ public static int getPlaybackType(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getPlaybackType();
+ }
+
+ public static int getPlaybackStream(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getPlaybackStream();
+ }
+
+ public static int getVolume(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getVolume();
+ }
+
+ public static int getVolumeMax(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getVolumeMax();
+ }
+
+ public static int getVolumeHandling(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getVolumeHandling();
+ }
+
+ public static Object getTag(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getTag();
+ }
+
+ public static void setTag(Object routeObj, Object tag) {
+ ((android.media.MediaRouter.RouteInfo)routeObj).setTag(tag);
+ }
+
+ public static void requestSetVolume(Object routeObj, int volume) {
+ ((android.media.MediaRouter.RouteInfo)routeObj).requestSetVolume(volume);
+ }
+
+ public static void requestUpdateVolume(Object routeObj, int direction) {
+ ((android.media.MediaRouter.RouteInfo)routeObj).requestUpdateVolume(direction);
+ }
+
+ public static Object getGroup(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getGroup();
+ }
+
+ public static boolean isGroup(Object routeObj) {
+ return routeObj instanceof android.media.MediaRouter.RouteGroup;
+ }
+ }
+
+ public static final class RouteGroup {
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public static List getGroupedRoutes(Object groupObj) {
+ final android.media.MediaRouter.RouteGroup group =
+ (android.media.MediaRouter.RouteGroup)groupObj;
+ final int count = group.getRouteCount();
+ List out = new ArrayList(count);
+ for (int i = 0; i < count; i++) {
+ out.add(group.getRouteAt(i));
+ }
+ return out;
+ }
+ }
+
+ public static final class UserRouteInfo {
+ public static void setName(Object routeObj, CharSequence name) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setName(name);
+ }
+
+ public static void setStatus(Object routeObj, CharSequence status) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setStatus(status);
+ }
+
+ public static void setIconDrawable(Object routeObj, Drawable icon) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setIconDrawable(icon);
+ }
+
+ public static void setPlaybackType(Object routeObj, int type) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setPlaybackType(type);
+ }
+
+ public static void setPlaybackStream(Object routeObj, int stream) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setPlaybackStream(stream);
+ }
+
+ public static void setVolume(Object routeObj, int volume) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolume(volume);
+ }
+
+ public static void setVolumeMax(Object routeObj, int volumeMax) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolumeMax(volumeMax);
+ }
+
+ public static void setVolumeHandling(Object routeObj, int volumeHandling) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolumeHandling(volumeHandling);
+ }
+
+ public static void setVolumeCallback(Object routeObj, Object volumeCallbackObj) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolumeCallback(
+ (android.media.MediaRouter.VolumeCallback)volumeCallbackObj);
+ }
+
+ public static void setRemoteControlClient(Object routeObj, Object rccObj) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setRemoteControlClient(
+ (android.media.RemoteControlClient)rccObj);
+ }
+ }
+
+ public static final class RouteCategory {
+ public static CharSequence getName(Object categoryObj, Context context) {
+ return ((android.media.MediaRouter.RouteCategory)categoryObj).getName(context);
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public static List getRoutes(Object categoryObj) {
+ ArrayList out = new ArrayList();
+ ((android.media.MediaRouter.RouteCategory)categoryObj).getRoutes(out);
+ return out;
+ }
+
+ public static int getSupportedTypes(Object categoryObj) {
+ return ((android.media.MediaRouter.RouteCategory)categoryObj).getSupportedTypes();
+ }
+
+ public static boolean isGroupable(Object categoryObj) {
+ return ((android.media.MediaRouter.RouteCategory)categoryObj).isGroupable();
+ }
+ }
+
+ public static interface Callback {
+ public void onRouteSelected(int type, Object routeObj);
+ public void onRouteUnselected(int type, Object routeObj);
+ public void onRouteAdded(Object routeObj);
+ public void onRouteRemoved(Object routeObj);
+ public void onRouteChanged(Object routeObj);
+ public void onRouteGrouped(Object routeObj, Object groupObj, int index);
+ public void onRouteUngrouped(Object routeObj, Object groupObj);
+ public void onRouteVolumeChanged(Object routeObj);
+ }
+
+ public static interface VolumeCallback {
+ public void onVolumeSetRequest(Object routeObj, int volume);
+ public void onVolumeUpdateRequest(Object routeObj, int direction);
+ }
+
+ /**
+ * Workaround for limitations of selectRoute() on JB and JB MR1.
+ * Do not use on JB MR2 and above.
+ */
+ public static final class SelectRouteWorkaround {
+ private Method mSelectRouteIntMethod;
+
+ public SelectRouteWorkaround() {
+ if (Build.VERSION.SDK_INT < 16 || Build.VERSION.SDK_INT > 17) {
+ throw new UnsupportedOperationException();
+ }
+ try {
+ mSelectRouteIntMethod = android.media.MediaRouter.class.getMethod(
+ "selectRouteInt", int.class, android.media.MediaRouter.RouteInfo.class);
+ } catch (NoSuchMethodException ex) {
+ }
+ }
+
+ public void selectRoute(Object routerObj, int types, Object routeObj) {
+ android.media.MediaRouter router = (android.media.MediaRouter)routerObj;
+ android.media.MediaRouter.RouteInfo route =
+ (android.media.MediaRouter.RouteInfo)routeObj;
+
+ int routeTypes = route.getSupportedTypes();
+ if ((routeTypes & ROUTE_TYPE_USER) == 0) {
+ // Handle non-user routes.
+ // On JB and JB MR1, the selectRoute() API only supports programmatically
+ // selecting user routes. So instead we rely on the hidden selectRouteInt()
+ // method on these versions of the platform.
+ // This limitation was removed in JB MR2.
+ if (mSelectRouteIntMethod != null) {
+ try {
+ mSelectRouteIntMethod.invoke(router, types, route);
+ return; // success!
+ } catch (IllegalAccessException ex) {
+ Log.w(TAG, "Cannot programmatically select non-user route. "
+ + "Media routing may not work.", ex);
+ } catch (InvocationTargetException ex) {
+ Log.w(TAG, "Cannot programmatically select non-user route. "
+ + "Media routing may not work.", ex);
+ }
+ } else {
+ Log.w(TAG, "Cannot programmatically select non-user route "
+ + "because the platform is missing the selectRouteInt() "
+ + "method. Media routing may not work.");
+ }
+ }
+
+ // Default handling.
+ router.selectRoute(types, route);
+ }
+ }
+
+ /**
+ * Workaround the fact that the getDefaultRoute() method does not exist in JB and JB MR1.
+ * Do not use on JB MR2 and above.
+ */
+ public static final class GetDefaultRouteWorkaround {
+ private Method mGetSystemAudioRouteMethod;
+
+ public GetDefaultRouteWorkaround() {
+ if (Build.VERSION.SDK_INT < 16 || Build.VERSION.SDK_INT > 17) {
+ throw new UnsupportedOperationException();
+ }
+ try {
+ mGetSystemAudioRouteMethod =
+ android.media.MediaRouter.class.getMethod("getSystemAudioRoute");
+ } catch (NoSuchMethodException ex) {
+ }
+ }
+
+ public Object getDefaultRoute(Object routerObj) {
+ android.media.MediaRouter router = (android.media.MediaRouter)routerObj;
+
+ if (mGetSystemAudioRouteMethod != null) {
+ try {
+ return mGetSystemAudioRouteMethod.invoke(router);
+ } catch (IllegalAccessException ex) {
+ } catch (InvocationTargetException ex) {
+ }
+ }
+
+ // Could not find the method or it does not work.
+ // Return the first route and hope for the best.
+ return router.getRouteAt(0);
+ }
+ }
+
+ static class CallbackProxy<T extends Callback>
+ extends android.media.MediaRouter.Callback {
+ protected final T mCallback;
+
+ public CallbackProxy(T callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void onRouteSelected(android.media.MediaRouter router,
+ int type, android.media.MediaRouter.RouteInfo route) {
+ mCallback.onRouteSelected(type, route);
+ }
+
+ @Override
+ public void onRouteUnselected(android.media.MediaRouter router,
+ int type, android.media.MediaRouter.RouteInfo route) {
+ mCallback.onRouteUnselected(type, route);
+ }
+
+ @Override
+ public void onRouteAdded(android.media.MediaRouter router,
+ android.media.MediaRouter.RouteInfo route) {
+ mCallback.onRouteAdded(route);
+ }
+
+ @Override
+ public void onRouteRemoved(android.media.MediaRouter router,
+ android.media.MediaRouter.RouteInfo route) {
+ mCallback.onRouteRemoved(route);
+ }
+
+ @Override
+ public void onRouteChanged(android.media.MediaRouter router,
+ android.media.MediaRouter.RouteInfo route) {
+ mCallback.onRouteChanged(route);
+ }
+
+ @Override
+ public void onRouteGrouped(android.media.MediaRouter router,
+ android.media.MediaRouter.RouteInfo route,
+ android.media.MediaRouter.RouteGroup group, int index) {
+ mCallback.onRouteGrouped(route, group, index);
+ }
+
+ @Override
+ public void onRouteUngrouped(android.media.MediaRouter router,
+ android.media.MediaRouter.RouteInfo route,
+ android.media.MediaRouter.RouteGroup group) {
+ mCallback.onRouteUngrouped(route, group);
+ }
+
+ @Override
+ public void onRouteVolumeChanged(android.media.MediaRouter router,
+ android.media.MediaRouter.RouteInfo route) {
+ mCallback.onRouteVolumeChanged(route);
+ }
+ }
+
+ static class VolumeCallbackProxy<T extends VolumeCallback>
+ extends android.media.MediaRouter.VolumeCallback {
+ protected final T mCallback;
+
+ public VolumeCallbackProxy(T callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void onVolumeSetRequest(android.media.MediaRouter.RouteInfo route,
+ int volume) {
+ mCallback.onVolumeSetRequest(route, volume);
+ }
+
+ @Override
+ public void onVolumeUpdateRequest(android.media.MediaRouter.RouteInfo route,
+ int direction) {
+ mCallback.onVolumeUpdateRequest(route, direction);
+ }
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaControlIntent.java b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaControlIntent.java
new file mode 100644
index 0000000..1d9e777
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaControlIntent.java
@@ -0,0 +1,1228 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.media;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.net.Uri;
+
+/**
+ * Constants for media control intents.
+ * <p>
+ * This class declares a set of standard media control intent categories and actions that
+ * applications can use to identify the capabilities of media routes and control them.
+ * </p>
+ *
+ * <h3>Media control intent categories</h3>
+ * <p>
+ * Media control intent categories specify means by which applications can
+ * send media to the destination of a media route. Categories are sometimes referred
+ * to as describing "types" or "kinds" of routes.
+ * </p><p>
+ * For example, if a route supports the {@link #CATEGORY_REMOTE_PLAYBACK remote playback category},
+ * then an application can ask it to play media remotely by sending a
+ * {@link #ACTION_PLAY play} or {@link #ACTION_ENQUEUE enqueue} intent with the Uri of the
+ * media content to play. Such a route may then be referred to as
+ * a "remote playback route" because it supports remote playback requests. It is common
+ * for a route to support multiple categories of requests at the same time, such as
+ * live audio and live video.
+ * </p><p>
+ * The following standard route categories are defined.
+ * </p><ul>
+ * <li>{@link #CATEGORY_LIVE_AUDIO Live audio}: The route supports streaming live audio
+ * from the device to the destination. Live audio routes include local speakers
+ * and Bluetooth headsets.
+ * <li>{@link #CATEGORY_LIVE_VIDEO Live video}: The route supports streaming live video
+ * from the device to the destination. Live video routes include local displays
+ * and wireless displays that support mirroring and
+ * {@link android.app.Presentation presentations}. Live video routes typically also
+ * support live audio capabilities.
+ * <li>{@link #CATEGORY_REMOTE_PLAYBACK Remote playback}: The route supports sending
+ * remote playback requests for media content to the destination. The content to be
+ * played is identified by a Uri and mime-type.
+ * </ul><p>
+ * Media route providers may define custom media control intent categories of their own in
+ * addition to the standard ones. Custom categories can be used to provide a variety
+ * of features to applications that recognize and know how to use them. For example,
+ * a media route provider might define a custom category to indicate that its routes
+ * support a special device-specific control interface in addition to other
+ * standard features.
+ * </p><p>
+ * Applications can determine which categories a route supports by using the
+ * {@link MediaRouter.RouteInfo#supportsControlCategory MediaRouter.RouteInfo.supportsControlCategory}
+ * or {@link MediaRouter.RouteInfo#getControlFilters MediaRouter.RouteInfo.getControlFilters}
+ * methods. Applications can also specify the types of routes that they want to use by
+ * creating {@link MediaRouteSelector media route selectors} that contain the desired
+ * categories and are used to filter routes in several parts of the media router API.
+ * </p>
+ *
+ * <h3>Media control intent actions</h3>
+ * <p>
+ * Media control intent actions specify particular functions that applications
+ * can ask the destination of a media route to perform. Media route control requests
+ * take the form of intents in a similar manner to other intents used to start activities
+ * or send broadcasts. The difference is that media control intents are directed to
+ * routes rather than activity or broadcast receiver components.
+ * </p><p>
+ * Each media route control intent specifies an action, a category and some number of parameters
+ * that are supplied as extras. Applications send media control requests to routes using the
+ * {@link MediaRouter.RouteInfo#sendControlRequest MediaRouter.RouteInfo.sendControlRequest}
+ * method and receive results via a callback.
+ * </p><p>
+ * All media control intent actions are associated with the media control intent categories
+ * that support them. Thus only remote playback routes may perform remote playback actions.
+ * The documentation of each action specifies the category to which the action belongs,
+ * the parameters it requires, and the results it returns.
+ * </p>
+ *
+ * <h3>Live audio and live video routes</h3>
+ * <p>
+ * {@link #CATEGORY_LIVE_AUDIO Live audio} and {@link #CATEGORY_LIVE_VIDEO live video}
+ * routes present media using standard system interfaces such as audio streams,
+ * {@link android.app.Presentation presentations} or display mirroring. These routes are
+ * the easiest to use because applications simply render content locally on the device
+ * and the system streams it to the route destination automatically.
+ * </p><p>
+ * In most cases, applications can stream content to live audio and live video routes in
+ * the same way they would play the content locally without any modification. However,
+ * applications may also be able to take advantage of more sophisticated features such
+ * as second-screen presentation APIs that are particular to these routes.
+ * </p>
+ *
+ * <h3>Remote playback routes</h3>
+ * <p>
+ * {@link #CATEGORY_REMOTE_PLAYBACK Remote playback} routes present media remotely
+ * by playing content from a Uri.
+ * These routes destinations take responsibility for fetching and rendering content
+ * on their own. Applications do not render the content themselves; instead, applications
+ * send control requests to initiate play, pause, resume, or stop media items and receive
+ * status updates as they change state.
+ * </p>
+ *
+ * <h4>Sessions</h4>
+ * <p>
+ * Each remote media playback action is conducted within the scope of a session.
+ * Sessions are used to prevent applications from accidentally interfering with one
+ * another because at most one session can be valid at a time.
+ * </p><p>
+ * A session can be created using the {@link #ACTION_START_SESSION start session action}
+ * and terminated using the {@link #ACTION_END_SESSION end session action} when the
+ * route provides explicit session management features.
+ * </p><p>
+ * Explicit session management was added in a later revision of the protocol so not
+ * all routes support it. If the route does not support explicit session management
+ * then implicit session management may still be used. Implicit session management
+ * relies on the use of the {@link #ACTION_PLAY play} and {@link #ACTION_ENQUEUE enqueue}
+ * actions which have the side-effect of creating a new session if none is provided
+ * as argument.
+ * </p><p>
+ * When a new session is created, the previous session is invalidated and any ongoing
+ * media playback is stopped before the requested action is performed. Any attempt
+ * to use an invalidated session will result in an error. (Protocol implementations
+ * are encouraged to aggressively discard information associated with invalidated sessions
+ * since it is no longer of use.)
+ * </p><p>
+ * Each session is identified by a unique session id that may be used to control
+ * the session using actions such as pause, resume, stop and end session.
+ * </p>
+ *
+ * <h4>Media items</h4>
+ * <p>
+ * Each successful {@link #ACTION_PLAY play} or {@link #ACTION_ENQUEUE enqueue} action
+ * returns a unique media item id that an application can use to monitor and control
+ * playback. The media item id may be passed to other actions such as
+ * {@link #ACTION_SEEK seek} or {@link #ACTION_GET_STATUS get status}. It will also appear
+ * as a parameter in status update broadcasts to identify the associated playback request.
+ * </p><p>
+ * Each media item is scoped to the session in which it was created. Therefore media item
+ * ids are only ever used together with session ids. Media item ids are meaningless
+ * on their own. When the session is invalidated, all of its media items are also
+ * invalidated.
+ * </p>
+ *
+ * <h4>The playback queue</h4>
+ * <p>
+ * Each session has its own playback queue that consists of the media items that
+ * are pending, playing, buffering or paused. Items are added to the queue when
+ * a playback request is issued. Items are removed from the queue when they are no
+ * longer eligible for playback (enter terminal states).
+ * </p><p>
+ * As described in the {@link MediaItemStatus} class, media items initially
+ * start in a pending state, transition to the playing (or buffering or paused) state
+ * during playback, and end in a finished, canceled, invalidated or error state.
+ * Once the current item enters a terminal state, playback proceeds on to the
+ * next item.
+ * </p><p>
+ * The application should determine whether the route supports queuing by checking
+ * whether the {@link #ACTION_ENQUEUE} action is declared in the route's control filter
+ * using {@link MediaRouter.RouteInfo#supportsControlRequest RouteInfo.supportsControlRequest}.
+ * </p><p>
+ * If the {@link #ACTION_ENQUEUE} action is supported by the route, then the route promises
+ * to allow at least two items (possibly more) to be enqueued at a time. Enqueued items play
+ * back to back one after the other as the previous item completes. Ideally there should
+ * be no audible pause between items for standard audio content types.
+ * </p><p>
+ * If the {@link #ACTION_ENQUEUE} action is not supported by the route, then the queue
+ * effectively contains at most one item at a time. Each play action has the effect of
+ * clearing the queue and resetting its state before the next item is played.
+ * </p>
+ *
+ * <h4>Impact of pause, resume, stop and play actions on the playback queue</h4>
+ * <p>
+ * The pause, resume and stop actions affect the session's whole queue. Pause causes
+ * the playback queue to be suspended no matter which item is currently playing.
+ * Resume reverses the effects of pause. Stop clears the queue and also resets
+ * the pause flag just like resume.
+ * </p><p>
+ * As described earlier, the play action has the effect of clearing the queue
+ * and completely resetting its state (like the stop action) then enqueuing a
+ * new media item to be played immediately. Play is therefore equivalent
+ * to stop followed by an action to enqueue an item.
+ * </p><p>
+ * The play action is also special in that it can be used to create new sessions.
+ * An application with simple needs may find that it only needs to use play
+ * (and occasionally stop) to control playback.
+ * </p>
+ *
+ * <h4>Resolving conflicts between applications</h4>
+ * <p>
+ * When an application has a valid session, it is essentially in control of remote playback
+ * on the route. No other application can view or modify the remote playback state
+ * of that application's session without knowing its id.
+ * </p><p>
+ * However, other applications can perform actions that have the effect of stopping
+ * playback and invalidating the current session. When this occurs, the former application
+ * will be informed that it has lost control by way of individual media item status
+ * update broadcasts that indicate that its queued media items have become
+ * {@link MediaItemStatus#PLAYBACK_STATE_INVALIDATED invalidated}. This broadcast
+ * implies that playback was terminated abnormally by an external cause.
+ * </p><p>
+ * Applications should handle conflicts conservatively to allow other applications to
+ * smoothly assume control over the route. When a conflict occurs, the currently playing
+ * application should release its session and allow the new application to use the
+ * route until such time as the user intervenes to take over the route again and begin
+ * a new playback session.
+ * </p>
+ *
+ * <h4>Basic actions</h4>
+ * <p>
+ * The following basic actions must be supported (all or nothing) by all remote
+ * playback routes. These actions form the basis of the remote playback protocol
+ * and are required in all implementations.
+ * </p><ul>
+ * <li>{@link #ACTION_PLAY Play}: Starts playing content specified by a given Uri
+ * and returns a new media item id to describe the request. Implicitly creates a new
+ * session if no session id was specified as a parameter.
+ * <li>{@link #ACTION_SEEK Seek}: Sets the content playback position of a specific media item.
+ * <li>{@link #ACTION_GET_STATUS Get status}: Gets the status of a media item
+ * including the item's current playback position and progress.
+ * <li>{@link #ACTION_PAUSE Pause}: Pauses playback of the queue.
+ * <li>{@link #ACTION_RESUME Resume}: Resumes playback of the queue.
+ * <li>{@link #ACTION_STOP Stop}: Stops playback, clears the queue, and resets the
+ * pause state.
+ * </ul>
+ *
+ * <h4>Queue actions</h4>
+ * <p>
+ * The following queue actions must be supported (all or nothing) by remote
+ * playback routes that offer optional queuing capabilities.
+ * </p><ul>
+ * <li>{@link #ACTION_ENQUEUE Enqueue}: Enqueues content specified by a given Uri
+ * and returns a new media item id to describe the request. Implicitly creates a new
+ * session if no session id was specified as a parameter.
+ * <li>{@link #ACTION_REMOVE Remove}: Removes a specified media item from the queue.
+ * </ul>
+ *
+ * <h4>Session actions</h4>
+ * <p>
+ * The following session actions must be supported (all or nothing) by remote
+ * playback routes that offer optional session management capabilities.
+ * </p><ul>
+ * <li>{@link #ACTION_START_SESSION Start session}: Starts a new session explicitly.
+ * <li>{@link #ACTION_GET_SESSION_STATUS Get session status}: Gets the status of a session.
+ * <li>{@link #ACTION_END_SESSION End session}: Ends a session explicitly.
+ * </ul>
+ *
+ * <h4>Implementation note</h4>
+ * <p>
+ * Implementations of the remote playback protocol must implement <em>all</em> of the
+ * documented actions, parameters and results. Note that the documentation is written from
+ * the perspective of a client of the protocol. In particular, whenever a parameter
+ * is described as being "optional", it is only from the perspective of the client.
+ * Compliant media route provider implementations of this protocol must support all
+ * of the features described herein.
+ * </p>
+ */
+public final class MediaControlIntent {
+ /* Route categories. */
+
+ /**
+ * Media control category: Live audio.
+ * <p>
+ * A route that supports live audio routing will allow the media audio stream
+ * to be sent to supported destinations. This can include internal speakers or
+ * audio jacks on the device itself, A2DP devices, and more.
+ * </p><p>
+ * When a live audio route is selected, audio routing is transparent to the application.
+ * All audio played on the media stream will be routed to the selected destination.
+ * </p><p>
+ * Refer to the class documentation for details about live audio routes.
+ * </p>
+ */
+ public static final String CATEGORY_LIVE_AUDIO = "android.media.intent.category.LIVE_AUDIO";
+
+ /**
+ * Media control category: Live video.
+ * <p>
+ * A route that supports live video routing will allow a mirrored version
+ * of the device's primary display or a customized
+ * {@link android.app.Presentation Presentation} to be sent to supported
+ * destinations.
+ * </p><p>
+ * When a live video route is selected, audio and video routing is transparent
+ * to the application. By default, audio and video is routed to the selected
+ * destination. For certain live video routes, the application may also use a
+ * {@link android.app.Presentation Presentation} to replace the mirrored view
+ * on the external display with different content.
+ * </p><p>
+ * Refer to the class documentation for details about live video routes.
+ * </p>
+ *
+ * @see MediaRouter.RouteInfo#getPresentationDisplay()
+ * @see android.app.Presentation
+ */
+ public static final String CATEGORY_LIVE_VIDEO = "android.media.intent.category.LIVE_VIDEO";
+
+ /**
+ * Media control category: Remote playback.
+ * <p>
+ * A route that supports remote playback routing will allow an application to send
+ * requests to play content remotely to supported destinations.
+ * </p><p>
+ * Remote playback routes destinations operate independently of the local device.
+ * When a remote playback route is selected, the application can control the content
+ * playing on the destination by sending media control actions to the route.
+ * The application may also receive status updates from the route regarding
+ * remote playback.
+ * </p><p>
+ * Refer to the class documentation for details about remote playback routes.
+ * </p>
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ */
+ public static final String CATEGORY_REMOTE_PLAYBACK =
+ "android.media.intent.category.REMOTE_PLAYBACK";
+
+ /* Remote playback actions that affect individual items. */
+
+ /**
+ * Remote playback media control action: Play media item.
+ * <p>
+ * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
+ * media control.
+ * </p><p>
+ * This action causes a remote playback route to start playing content with
+ * the {@link Uri} specified in the {@link Intent}'s {@link Intent#getData() data uri}.
+ * The action returns a media session id and media item id which can be used
+ * to control playback using other remote playback actions.
+ * </p><p>
+ * Once initiated, playback of the specified content will be managed independently
+ * by the destination. The application will receive status updates as the state
+ * of the media item changes.
+ * </p><p>
+ * If the data uri specifies an HTTP or HTTPS scheme, then the destination is
+ * responsible for following HTTP redirects to a reasonable depth of at least 3
+ * levels as might typically be handled by a web browser. If an HTTP error
+ * occurs, then the destination should send a {@link MediaItemStatus status update}
+ * back to the client indicating the {@link MediaItemStatus#PLAYBACK_STATE_ERROR error}
+ * {@link MediaItemStatus#getPlaybackState() playback state}.
+ * </p>
+ *
+ * <h3>One item at a time</h3>
+ * <p>
+ * Each successful play action <em>replaces</em> the previous play action.
+ * If an item is already playing, then it is canceled, the session's playback queue
+ * is cleared and the new item begins playing immediately (regardless of
+ * whether the previously playing item had been paused).
+ * </p><p>
+ * Play is therefore equivalent to {@link #ACTION_STOP stop} followed by an action
+ * to enqueue a new media item to be played immediately.
+ * </p>
+ *
+ * <h3>Sessions</h3>
+ * <p>
+ * This request has the effect of implicitly creating a media session whenever the
+ * application does not specify the {@link #EXTRA_SESSION_ID session id} parameter.
+ * Because there can only be at most one valid session at a time, creating a new session
+ * has the side-effect of invalidating any existing sessions and their media items,
+ * then handling the playback request with a new session.
+ * </p><p>
+ * If the application specifies an invalid session id, then an error is returned.
+ * When this happens, the application should assume that its session
+ * is no longer valid. To obtain a new session, the application may try again
+ * and omit the session id parameter. However, the application should
+ * only retry requests due to an explicit action performed by the user,
+ * such as the user clicking on a "play" button in the UI, since another
+ * application may be trying to take control of the route and the former
+ * application should try to stay out of its way.
+ * </p><p>
+ * For more information on sessions, queues and media items, please refer to the
+ * class documentation.
+ * </p>
+ *
+ * <h3>Request parameters</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(optional)</em>: Specifies the session id of the
+ * session to which the playback request belongs. If omitted, a new session
+ * is created implicitly.
+ * <li>{@link #EXTRA_ITEM_CONTENT_POSITION} <em>(optional)</em>: Specifies the initial
+ * content playback position as a long integer number of milliseconds from
+ * the beginning of the content.
+ * <li>{@link #EXTRA_ITEM_METADATA} <em>(optional)</em>: Specifies metadata associated
+ * with the content such as the title of a song.
+ * <li>{@link #EXTRA_ITEM_STATUS_UPDATE_RECEIVER} <em>(optional)</em>: Specifies a
+ * {@link PendingIntent} for a broadcast receiver that will receive status updates
+ * about the media item.
+ * </ul>
+ *
+ * <h3>Result data</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(always returned)</em>: Specifies the session id of the
+ * session that was affected by the request. This will be a new session in
+ * the case where no session id was supplied as a parameter.
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(optional, old implementations may
+ * omit this key)</em>: Specifies the status of the media session.
+ * <li>{@link #EXTRA_ITEM_ID} <em>(always returned)</em>: Specifies an opaque string identifier
+ * to use to refer to the media item in subsequent requests such as
+ * {@link #ACTION_GET_STATUS}.
+ * <li>{@link #EXTRA_ITEM_STATUS} <em>(always returned)</em>: Specifies the initial status of
+ * the new media item.
+ * </ul>
+ *
+ * <h3>Status updates</h3>
+ * <p>
+ * If the client supplies an
+ * {@link #EXTRA_ITEM_STATUS_UPDATE_RECEIVER item status update receiver}
+ * then the media route provider is responsible for sending status updates to the receiver
+ * when significant media item state changes occur such as when playback starts or
+ * stops. The receiver will not be invoked for content playback position changes.
+ * The application may retrieve the current playback position when necessary
+ * using the {@link #ACTION_GET_STATUS} request.
+ * </p><p>
+ * Refer to {@link MediaItemStatus} for details.
+ * </p>
+ *
+ * <h3>Errors</h3>
+ * <p>
+ * This action returns an error if a session id was provided but is unknown or
+ * no longer valid, if the item Uri or content type is not supported, or if
+ * any other arguments are invalid.
+ * </p><ul>
+ * <li>{@link #EXTRA_ERROR_CODE} <em>(optional)</em>: Specifies the cause of the error.
+ * </ul>
+ *
+ * <h3>Example</h3>
+ * <pre>
+ * MediaRouter mediaRouter = MediaRouter.getInstance(context);
+ * MediaRouter.RouteInfo route = mediaRouter.getSelectedRoute();
+ * Intent intent = new Intent(MediaControlIntent.ACTION_PLAY);
+ * intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ * intent.setDataAndType("http://example.com/videos/movie.mp4", "video/mp4");
+ * if (route.supportsControlRequest(intent)) {
+ * MediaRouter.ControlRequestCallback callback = new MediaRouter.ControlRequestCallback() {
+ * public void onResult(Bundle data) {
+ * // The request succeeded.
+ * // Playback may be controlled using the returned session and item id.
+ * String sessionId = data.getString(MediaControlIntent.EXTRA_SESSION_ID);
+ * String itemId = data.getString(MediaControlIntent.EXTRA_ITEM_ID);
+ * MediaItemStatus status = MediaItemStatus.fromBundle(data.getBundle(
+ * MediaControlIntent.EXTRA_ITEM_STATUS));
+ * // ...
+ * }
+ *
+ * public void onError(String message, Bundle data) {
+ * // An error occurred!
+ * }
+ * };
+ * route.sendControlRequest(intent, callback);
+ * }</pre>
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ * @see #CATEGORY_REMOTE_PLAYBACK
+ * @see #ACTION_SEEK
+ * @see #ACTION_GET_STATUS
+ * @see #ACTION_PAUSE
+ * @see #ACTION_RESUME
+ * @see #ACTION_STOP
+ */
+ public static final String ACTION_PLAY = "android.media.intent.action.PLAY";
+
+ /**
+ * Remote playback media control action: Enqueue media item.
+ * <p>
+ * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
+ * media control.
+ * </p><p>
+ * This action works just like {@link #ACTION_PLAY play} except that it does
+ * not clear the queue or reset the pause state when it enqueues the
+ * new media item into the session's playback queue. This action only
+ * enqueues a media item with no other side-effects on the queue.
+ * </p><p>
+ * If the queue is currently empty and then the item will play immediately
+ * (assuming the queue is not paused). Otherwise, the item will play
+ * after all earlier items in the queue have finished or been removed.
+ * </p><p>
+ * The enqueue action can be used to create new sessions just like play.
+ * Its parameters and results are also the same. Only the queuing behavior
+ * is different.
+ * </p>
+ *
+ * @see #ACTION_PLAY
+ */
+ public static final String ACTION_ENQUEUE = "android.media.intent.action.ENQUEUE";
+
+ /**
+ * Remote playback media control action: Seek media item to a new playback position.
+ * <p>
+ * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
+ * media control.
+ * </p><p>
+ * This action causes a remote playback route to modify the current playback position
+ * of the specified media item.
+ * </p><p>
+ * This action only affects the playback position of the media item; not its playback state.
+ * If the playback queue is paused, then seeking sets the position but the item
+ * remains paused. Likewise if the item is playing, then seeking will cause playback
+ * to jump to the new position and continue playing from that point. If the item has
+ * not yet started playing, then the new playback position is remembered by the
+ * queue and used as the item's initial content position when playback eventually begins.
+ * </p><p>
+ * If successful, the media item's playback position is changed.
+ * </p>
+ *
+ * <h3>Request parameters</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of the session
+ * to which the media item belongs.
+ * <li>{@link #EXTRA_ITEM_ID} <em>(required)</em>: Specifies the media item id of
+ * the media item to seek.
+ * <li>{@link #EXTRA_ITEM_CONTENT_POSITION} <em>(required)</em>: Specifies the new
+ * content position for playback as a long integer number of milliseconds from
+ * the beginning of the content.
+ * </ul>
+ *
+ * <h3>Result data</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(optional, old implementations may
+ * omit this key)</em>: Specifies the status of the media session.
+ * <li>{@link #EXTRA_ITEM_STATUS} <em>(always returned)</em>: Specifies the new status of
+ * the media item.
+ * </ul>
+ *
+ * <h3>Errors</h3>
+ * <p>
+ * This action returns an error if the session id or media item id are unknown
+ * or no longer valid, if the content position is invalid, or if the media item
+ * is in a terminal state.
+ * </p><ul>
+ * <li>{@link #EXTRA_ERROR_CODE} <em>(optional)</em>: Specifies the cause of the error.
+ * </ul>
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ * @see #CATEGORY_REMOTE_PLAYBACK
+ */
+ public static final String ACTION_SEEK = "android.media.intent.action.SEEK";
+
+ /**
+ * Remote playback media control action: Get media item playback status
+ * and progress information.
+ * <p>
+ * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
+ * media control.
+ * </p><p>
+ * This action asks a remote playback route to provide updated playback status and progress
+ * information about the specified media item.
+ * </p>
+ *
+ * <h3>Request parameters</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of the session
+ * to which the media item belongs.
+ * <li>{@link #EXTRA_ITEM_ID} <em>(required)</em>: Specifies the media item id of
+ * the media item to query.
+ * </ul>
+ *
+ * <h3>Result data</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(optional, old implementations may
+ * omit this key)</em>: Specifies the status of the media session.
+ * <li>{@link #EXTRA_ITEM_STATUS} <em>(always returned)</em>: Specifies the current status of
+ * the media item.
+ * </ul>
+ *
+ * <h3>Errors</h3>
+ * <p>
+ * This action returns an error if the session id or media item id are unknown
+ * or no longer valid.
+ * </p><ul>
+ * <li>{@link #EXTRA_ERROR_CODE} <em>(optional)</em>: Specifies the cause of the error.
+ * </ul>
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ * @see #CATEGORY_REMOTE_PLAYBACK
+ * @see #EXTRA_ITEM_STATUS_UPDATE_RECEIVER
+ */
+ public static final String ACTION_GET_STATUS = "android.media.intent.action.GET_STATUS";
+
+ /**
+ * Remote playback media control action: Remove media item from session's queue.
+ * <p>
+ * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
+ * media control.
+ * </p><p>
+ * This action asks a remote playback route to remove the specified media item
+ * from the session's playback queue. If the current item is removed, then
+ * playback will proceed to the next media item (assuming the queue has not been
+ * paused).
+ * </p><p>
+ * This action does not affect the pause state of the queue. If the queue was paused
+ * then it remains paused (even if it is now empty) until a resume, stop or play
+ * action is issued that causes the pause state to be cleared.
+ * </p>
+ *
+ * <h3>Request parameters</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of the session
+ * to which the media item belongs.
+ * <li>{@link #EXTRA_ITEM_ID} <em>(required)</em>: Specifies the media item id of
+ * the media item to remove.
+ * </ul>
+ *
+ * <h3>Result data</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(optional, old implementations may
+ * omit this key)</em>: Specifies the status of the media session.
+ * <li>{@link #EXTRA_ITEM_STATUS} <em>(always returned)</em>: Specifies the new status of
+ * the media item.
+ * </ul>
+ *
+ * <h3>Errors</h3>
+ * <p>
+ * This action returns an error if the session id or media item id are unknown
+ * or no longer valid, or if the media item is in a terminal state (and therefore
+ * no longer in the queue).
+ * </p><ul>
+ * <li>{@link #EXTRA_ERROR_CODE} <em>(optional)</em>: Specifies the cause of the error.
+ * </ul>
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ * @see #CATEGORY_REMOTE_PLAYBACK
+ */
+ public static final String ACTION_REMOVE = "android.media.intent.action.REMOVE";
+
+ /* Remote playback actions that affect the whole playback queue. */
+
+ /**
+ * Remote playback media control action: Pause media playback.
+ * <p>
+ * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
+ * media control.
+ * </p><p>
+ * This action causes the playback queue of the specified session to be paused.
+ * </p>
+ *
+ * <h3>Request parameters</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of the session
+ * whose playback queue is to be paused.
+ * </ul>
+ *
+ * <h3>Result data</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(optional, old implementations may
+ * omit this key)</em>: Specifies the status of the media session.
+ * </ul>
+ *
+ * <h3>Errors</h3>
+ * <p>
+ * This action returns an error if the session id is unknown or no longer valid.
+ * </p><ul>
+ * <li>{@link #EXTRA_ERROR_CODE} <em>(optional)</em>: Specifies the cause of the error.
+ * </ul>
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ * @see #CATEGORY_REMOTE_PLAYBACK
+ * @see #ACTION_RESUME
+ */
+ public static final String ACTION_PAUSE = "android.media.intent.action.PAUSE";
+
+ /**
+ * Remote playback media control action: Resume media playback (unpause).
+ * <p>
+ * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
+ * media control.
+ * </p><p>
+ * This action causes the playback queue of the specified session to be resumed.
+ * Reverses the effects of {@link #ACTION_PAUSE}.
+ * </p>
+ *
+ * <h3>Request parameters</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of the session
+ * whose playback queue is to be resumed.
+ * </ul>
+ *
+ * <h3>Result data</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(optional, old implementations may
+ * omit this key)</em>: Specifies the status of the media session.
+ * </ul>
+ *
+ * <h3>Errors</h3>
+ * <p>
+ * This action returns an error if the session id is unknown or no longer valid.
+ * </p><ul>
+ * <li>{@link #EXTRA_ERROR_CODE} <em>(optional)</em>: Specifies the cause of the error.
+ * </ul>
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ * @see #CATEGORY_REMOTE_PLAYBACK
+ * @see #ACTION_PAUSE
+ */
+ public static final String ACTION_RESUME = "android.media.intent.action.RESUME";
+
+ /**
+ * Remote playback media control action: Stop media playback (clear queue and unpause).
+ * <p>
+ * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
+ * media control.
+ * </p><p>
+ * This action causes a remote playback route to stop playback, cancel and remove
+ * all media items from the session's media item queue and, reset the queue's
+ * pause state.
+ * </p><p>
+ * If successful, the status of all media items in the queue is set to
+ * {@link MediaItemStatus#PLAYBACK_STATE_CANCELED canceled} and a status update is sent
+ * to the appropriate status update receivers indicating the new status of each item.
+ * </p>
+ *
+ * <h3>Request parameters</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of
+ * the session whose playback queue is to be stopped (cleared and unpaused).
+ * </ul>
+ *
+ * <h3>Result data</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(optional, old implementations may
+ * omit this key)</em>: Specifies the status of the media session.
+ * </ul>
+ *
+ * <h3>Errors</h3>
+ * <p>
+ * This action returns an error if the session id is unknown or no longer valid.
+ * </p><ul>
+ * <li>{@link #EXTRA_ERROR_CODE} <em>(optional)</em>: Specifies the cause of the error.
+ * </ul>
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ * @see #CATEGORY_REMOTE_PLAYBACK
+ */
+ public static final String ACTION_STOP = "android.media.intent.action.STOP";
+
+ /**
+ * Remote playback media control action: Start session.
+ * <p>
+ * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
+ * media control.
+ * </p><p>
+ * This action causes a remote playback route to invalidate the current session
+ * and start a new session. The new session initially has an empty queue.
+ * </p><p>
+ * If successful, the status of all media items in the previous session's queue is set to
+ * {@link MediaItemStatus#PLAYBACK_STATE_INVALIDATED invalidated} and a status update
+ * is sent to the appropriate status update receivers indicating the new status
+ * of each item. The previous session becomes no longer valid and the new session
+ * takes control of the route.
+ * </p>
+ *
+ * <h3>Request parameters</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_STATUS_UPDATE_RECEIVER} <em>(optional)</em>: Specifies a
+ * {@link PendingIntent} for a broadcast receiver that will receive status updates
+ * about the media session.
+ * <li>{@link #EXTRA_MESSAGE_RECEIVER} <em>(optional)</em>: Specifies a
+ * {@link PendingIntent} for a broadcast receiver that will receive messages from
+ * the media session.
+ * </ul>
+ *
+ * <h3>Result data</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(always returned)</em>: Specifies the session id of the
+ * session that was started by the request. This will always be a brand new session
+ * distinct from any other previously created sessions.
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(always returned)</em>: Specifies the
+ * status of the media session.
+ * </ul>
+ *
+ * <h3>Status updates</h3>
+ * <p>
+ * If the client supplies a
+ * {@link #EXTRA_SESSION_STATUS_UPDATE_RECEIVER status update receiver}
+ * then the media route provider is responsible for sending status updates to the receiver
+ * when significant media session state changes occur such as when the session's
+ * queue is paused or resumed or when the session is terminated or invalidated.
+ * </p><p>
+ * Refer to {@link MediaSessionStatus} for details.
+ * </p>
+ *
+ * <h3>Custom messages</h3>
+ * <p>
+ * If the client supplies a {@link #EXTRA_MESSAGE_RECEIVER message receiver}
+ * then the media route provider is responsible for sending messages to the receiver
+ * when the session has any messages to send.
+ * </p><p>
+ * Refer to {@link #EXTRA_MESSAGE} for details.
+ * </p>
+ *
+ * <h3>Errors</h3>
+ * <p>
+ * This action returns an error if the session could not be created.
+ * </p><ul>
+ * <li>{@link #EXTRA_ERROR_CODE} <em>(optional)</em>: Specifies the cause of the error.
+ * </ul>
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ * @see #CATEGORY_REMOTE_PLAYBACK
+ */
+ public static final String ACTION_START_SESSION = "android.media.intent.action.START_SESSION";
+
+ /**
+ * Remote playback media control action: Get media session status information.
+ * <p>
+ * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
+ * media control.
+ * </p><p>
+ * This action asks a remote playback route to provide updated status information
+ * about the specified media session.
+ * </p>
+ *
+ * <h3>Request parameters</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of the
+ * session whose status is to be retrieved.
+ * </ul>
+ *
+ * <h3>Result data</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(always returned)</em>: Specifies the
+ * current status of the media session.
+ * </ul>
+ *
+ * <h3>Errors</h3>
+ * <p>
+ * This action returns an error if the session id is unknown or no longer valid.
+ * </p><ul>
+ * <li>{@link #EXTRA_ERROR_CODE} <em>(optional)</em>: Specifies the cause of the error.
+ * </ul>
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ * @see #CATEGORY_REMOTE_PLAYBACK
+ * @see #EXTRA_SESSION_STATUS_UPDATE_RECEIVER
+ */
+ public static final String ACTION_GET_SESSION_STATUS =
+ "android.media.intent.action.GET_SESSION_STATUS";
+
+ /**
+ * Remote playback media control action: End session.
+ * <p>
+ * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
+ * media control.
+ * </p><p>
+ * This action causes a remote playback route to end the specified session.
+ * The session becomes no longer valid and the route ceases to be under control
+ * of the session.
+ * </p><p>
+ * If successful, the status of the session is set to
+ * {@link MediaSessionStatus#SESSION_STATE_ENDED} and a status update is sent to
+ * the session's status update receiver.
+ * </p><p>
+ * Additionally, the status of all media items in the queue is set to
+ * {@link MediaItemStatus#PLAYBACK_STATE_CANCELED canceled} and a status update is sent
+ * to the appropriate status update receivers indicating the new status of each item.
+ * </p>
+ *
+ * <h3>Request parameters</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of
+ * the session to end.
+ * </ul>
+ *
+ * <h3>Result data</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(always returned)</em>: Specifies the
+ * status of the media session.
+ * </ul>
+ *
+ * <h3>Errors</h3>
+ * <p>
+ * This action returns an error if the session id is unknown or no longer valid.
+ * In other words, it is an error to attempt to end a session other than the
+ * current session.
+ * </p><ul>
+ * <li>{@link #EXTRA_ERROR_CODE} <em>(optional)</em>: Specifies the cause of the error.
+ * </ul>
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ * @see #CATEGORY_REMOTE_PLAYBACK
+ */
+ public static final String ACTION_END_SESSION = "android.media.intent.action.END_SESSION";
+
+ /**
+ * Custom media control action: Send {@link #EXTRA_MESSAGE}.
+ * <p>
+ * This action asks a route to handle a message described by EXTRA_MESSAGE.
+ * </p>
+ *
+ * <h3>Request parameters</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of the session
+ * to which will handle this message.
+ * <li>{@link #EXTRA_MESSAGE} <em>(required)</em>: Specifies the message to send.
+ * </ul>
+ *
+ * <h3>Result data</h3>
+ * Any messages defined by each media route provider.
+ *
+ * <h3>Errors</h3>
+ * Any error messages defined by each media route provider.
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ */
+ public static final String ACTION_SEND_MESSAGE = "android.media.intent.action.SEND_MESSAGE";
+
+ /* Extras and related constants. */
+
+ /**
+ * Bundle extra: Media session id.
+ * <p>
+ * An opaque unique identifier that identifies the remote playback media session.
+ * </p><p>
+ * Used with various actions to specify the id of the media session to be controlled.
+ * </p><p>
+ * Included in broadcast intents sent to
+ * {@link #EXTRA_ITEM_STATUS_UPDATE_RECEIVER item status update receivers} to identify
+ * the session to which the item in question belongs.
+ * </p><p>
+ * Included in broadcast intents sent to
+ * {@link #EXTRA_SESSION_STATUS_UPDATE_RECEIVER session status update receivers} to identify
+ * the session.
+ * </p><p>
+ * The value is a unique string value generated by the media route provider
+ * to represent one particular media session.
+ * </p>
+ *
+ * @see #ACTION_PLAY
+ * @see #ACTION_SEEK
+ * @see #ACTION_GET_STATUS
+ * @see #ACTION_PAUSE
+ * @see #ACTION_RESUME
+ * @see #ACTION_STOP
+ * @see #ACTION_START_SESSION
+ * @see #ACTION_GET_SESSION_STATUS
+ * @see #ACTION_END_SESSION
+ */
+ public static final String EXTRA_SESSION_ID =
+ "android.media.intent.extra.SESSION_ID";
+
+ /**
+ * Bundle extra: Media session status.
+ * <p>
+ * Returned as a result from media session actions such as {@link #ACTION_START_SESSION},
+ * {@link #ACTION_PAUSE}, and {@link #ACTION_GET_SESSION_STATUS}
+ * to describe the status of the specified media session.
+ * </p><p>
+ * Included in broadcast intents sent to
+ * {@link #EXTRA_SESSION_STATUS_UPDATE_RECEIVER session status update receivers} to provide
+ * updated status information.
+ * </p><p>
+ * The value is a {@link android.os.Bundle} of data that can be converted into
+ * a {@link MediaSessionStatus} object using
+ * {@link MediaSessionStatus#fromBundle MediaSessionStatus.fromBundle}.
+ * </p>
+ *
+ * @see #ACTION_PLAY
+ * @see #ACTION_SEEK
+ * @see #ACTION_GET_STATUS
+ * @see #ACTION_PAUSE
+ * @see #ACTION_RESUME
+ * @see #ACTION_STOP
+ * @see #ACTION_START_SESSION
+ * @see #ACTION_GET_SESSION_STATUS
+ * @see #ACTION_END_SESSION
+ */
+ public static final String EXTRA_SESSION_STATUS =
+ "android.media.intent.extra.SESSION_STATUS";
+
+ /**
+ * Bundle extra: Media session status update receiver.
+ * <p>
+ * Used with {@link #ACTION_START_SESSION} to specify a {@link PendingIntent} for a
+ * broadcast receiver that will receive status updates about the media session.
+ * </p><p>
+ * Whenever the status of the media session changes, the media route provider will
+ * send a broadcast to the pending intent with extras that identify the session
+ * id and its updated status.
+ * </p><p>
+ * The value is a {@link PendingIntent}.
+ * </p>
+ *
+ * <h3>Broadcast extras</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of
+ * the session.
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(required)</em>: Specifies the status of the
+ * session as a bundle that can be decoded into a {@link MediaSessionStatus} object.
+ * </ul>
+ *
+ * @see #ACTION_START_SESSION
+ */
+ public static final String EXTRA_SESSION_STATUS_UPDATE_RECEIVER =
+ "android.media.intent.extra.SESSION_STATUS_UPDATE_RECEIVER";
+
+ /**
+ * Bundle extra: Media message receiver.
+ * <p>
+ * Used with {@link #ACTION_START_SESSION} to specify a {@link PendingIntent} for a
+ * broadcast receiver that will receive messages from the media session.
+ * </p><p>
+ * When the media session has a message to send, the media route provider will
+ * send a broadcast to the pending intent with extras that identify the session
+ * id and its message.
+ * </p><p>
+ * The value is a {@link PendingIntent}.
+ * </p>
+ *
+ * <h3>Broadcast extras</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of
+ * the session.
+ * <li>{@link #EXTRA_MESSAGE} <em>(required)</em>: Specifies the message from
+ * the session as a bundle object.
+ * </ul>
+ *
+ * @see #ACTION_START_SESSION
+ */
+ public static final String EXTRA_MESSAGE_RECEIVER =
+ "android.media.intent.extra.MESSAGE_RECEIVER";
+
+ /**
+ * Bundle extra: Media item id.
+ * <p>
+ * An opaque unique identifier returned as a result from {@link #ACTION_PLAY} or
+ * {@link #ACTION_ENQUEUE} that represents the media item that was created by the
+ * playback request.
+ * </p><p>
+ * Used with various actions to specify the id of the media item to be controlled.
+ * </p><p>
+ * Included in broadcast intents sent to
+ * {@link #EXTRA_ITEM_STATUS_UPDATE_RECEIVER status update receivers} to identify
+ * the item in question.
+ * </p><p>
+ * The value is a unique string value generated by the media route provider
+ * to represent one particular media item.
+ * </p>
+ *
+ * @see #ACTION_PLAY
+ * @see #ACTION_ENQUEUE
+ * @see #ACTION_SEEK
+ * @see #ACTION_GET_STATUS
+ */
+ public static final String EXTRA_ITEM_ID =
+ "android.media.intent.extra.ITEM_ID";
+
+ /**
+ * Bundle extra: Media item status.
+ * <p>
+ * Returned as a result from media item actions such as {@link #ACTION_PLAY},
+ * {@link #ACTION_ENQUEUE}, {@link #ACTION_SEEK}, and {@link #ACTION_GET_STATUS}
+ * to describe the status of the specified media item.
+ * </p><p>
+ * Included in broadcast intents sent to
+ * {@link #EXTRA_ITEM_STATUS_UPDATE_RECEIVER item status update receivers} to provide
+ * updated status information.
+ * </p><p>
+ * The value is a {@link android.os.Bundle} of data that can be converted into
+ * a {@link MediaItemStatus} object using
+ * {@link MediaItemStatus#fromBundle MediaItemStatus.fromBundle}.
+ * </p>
+ *
+ * @see #ACTION_PLAY
+ * @see #ACTION_ENQUEUE
+ * @see #ACTION_SEEK
+ * @see #ACTION_GET_STATUS
+ */
+ public static final String EXTRA_ITEM_STATUS =
+ "android.media.intent.extra.ITEM_STATUS";
+
+ /**
+ * Long extra: Media item content position.
+ * <p>
+ * Used with {@link #ACTION_PLAY} or {@link #ACTION_ENQUEUE} to specify the
+ * starting playback position.
+ * </p><p>
+ * Used with {@link #ACTION_SEEK} to set a new playback position.
+ * </p><p>
+ * The value is a long integer number of milliseconds from the beginning of the content.
+ * <p>
+ *
+ * @see #ACTION_PLAY
+ * @see #ACTION_ENQUEUE
+ * @see #ACTION_SEEK
+ */
+ public static final String EXTRA_ITEM_CONTENT_POSITION =
+ "android.media.intent.extra.ITEM_POSITION";
+
+ /**
+ * Bundle extra: Media item metadata.
+ * <p>
+ * Used with {@link #ACTION_PLAY} or {@link #ACTION_ENQUEUE} to specify metadata
+ * associated with the content of a media item.
+ * </p><p>
+ * The value is a {@link android.os.Bundle} of metadata key-value pairs as defined
+ * in {@link MediaItemMetadata}.
+ * </p>
+ *
+ * @see #ACTION_PLAY
+ * @see #ACTION_ENQUEUE
+ */
+ public static final String EXTRA_ITEM_METADATA =
+ "android.media.intent.extra.ITEM_METADATA";
+
+ /**
+ * Bundle extra: HTTP request headers.
+ * <p>
+ * Used with {@link #ACTION_PLAY} or {@link #ACTION_ENQUEUE} to specify HTTP request
+ * headers to be included when fetching to the content indicated by the media
+ * item's data Uri.
+ * </p><p>
+ * This extra may be used to provide authentication tokens and other
+ * parameters to the server separately from the media item's data Uri.
+ * </p><p>
+ * The value is a {@link android.os.Bundle} of string based key-value pairs
+ * that describe the HTTP request headers.
+ * </p>
+ *
+ * @see #ACTION_PLAY
+ * @see #ACTION_ENQUEUE
+ */
+ public static final String EXTRA_ITEM_HTTP_HEADERS =
+ "android.media.intent.extra.HTTP_HEADERS";
+
+ /**
+ * Bundle extra: Media item status update receiver.
+ * <p>
+ * Used with {@link #ACTION_PLAY} or {@link #ACTION_ENQUEUE} to specify
+ * a {@link PendingIntent} for a
+ * broadcast receiver that will receive status updates about a particular
+ * media item.
+ * </p><p>
+ * Whenever the status of the media item changes, the media route provider will
+ * send a broadcast to the pending intent with extras that identify the session
+ * to which the item belongs, the session status, the item's id
+ * and the item's updated status.
+ * </p><p>
+ * The same pending intent and broadcast receiver may be shared by any number of
+ * media items since the broadcast intent includes the media session id
+ * and media item id.
+ * </p><p>
+ * The value is a {@link PendingIntent}.
+ * </p>
+ *
+ * <h3>Broadcast extras</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of
+ * the session to which the item in question belongs.
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(optional, old implementations may
+ * omit this key)</em>: Specifies the status of the media session.
+ * <li>{@link #EXTRA_ITEM_ID} <em>(required)</em>: Specifies the media item id of the
+ * media item in question.
+ * <li>{@link #EXTRA_ITEM_STATUS} <em>(required)</em>: Specifies the status of the
+ * item as a bundle that can be decoded into a {@link MediaItemStatus} object.
+ * </ul>
+ *
+ * @see #ACTION_PLAY
+ * @see #ACTION_ENQUEUE
+ */
+ public static final String EXTRA_ITEM_STATUS_UPDATE_RECEIVER =
+ "android.media.intent.extra.ITEM_STATUS_UPDATE_RECEIVER";
+
+ /**
+ * Bundle extra: Message.
+ * <p>
+ * Used with {@link #ACTION_SEND_MESSAGE}, and included in broadcast intents sent to
+ * {@link #EXTRA_MESSAGE_RECEIVER message receivers} to describe a message between a
+ * session and a media route provider.
+ * </p><p>
+ * The value is a {@link android.os.Bundle}.
+ * </p>
+ */
+ public static final String EXTRA_MESSAGE = "android.media.intent.extra.MESSAGE";
+
+ /**
+ * Integer extra: Error code.
+ * <p>
+ * Used with all media control requests to describe the cause of an error.
+ * This extra may be omitted when the error is unknown.
+ * </p><p>
+ * The value is one of: {@link #ERROR_UNKNOWN}, {@link #ERROR_UNSUPPORTED_OPERATION},
+ * {@link #ERROR_INVALID_SESSION_ID}, {@link #ERROR_INVALID_ITEM_ID}.
+ * </p>
+ */
+ public static final String EXTRA_ERROR_CODE = "android.media.intent.extra.ERROR_CODE";
+
+ /**
+ * Error code: An unknown error occurred.
+ *
+ * @see #EXTRA_ERROR_CODE
+ */
+ public static final int ERROR_UNKNOWN = 0;
+
+ /**
+ * Error code: The operation is not supported.
+ *
+ * @see #EXTRA_ERROR_CODE
+ */
+ public static final int ERROR_UNSUPPORTED_OPERATION = 1;
+
+ /**
+ * Error code: The session id specified in the request was invalid.
+ *
+ * @see #EXTRA_ERROR_CODE
+ */
+ public static final int ERROR_INVALID_SESSION_ID = 2;
+
+ /**
+ * Error code: The item id specified in the request was invalid.
+ *
+ * @see #EXTRA_ERROR_CODE
+ */
+ public static final int ERROR_INVALID_ITEM_ID = 3;
+
+ private MediaControlIntent() {
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaItemMetadata.java b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaItemMetadata.java
new file mode 100644
index 0000000..d52ddb6
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaItemMetadata.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.media;
+
+import android.os.Bundle;
+
+/**
+ * Constants for specifying metadata about a media item as a {@link Bundle}.
+ * <p>
+ * This class is part of the remote playback protocol described by the
+ * {@link MediaControlIntent MediaControlIntent} class.
+ * </p><p>
+ * Media item metadata is described as a bundle of key/value pairs as defined
+ * in this class. The documentation specifies the type of value associated
+ * with each key.
+ * </p><p>
+ * An application may specify additional custom metadata keys but there is no guarantee
+ * that they will be recognized by the destination.
+ * </p>
+ */
+public final class MediaItemMetadata {
+ /*
+ * Note: MediaMetadataRetriever also defines a collection of metadata keys that can be
+ * retrieved from a content stream although the representation is somewhat different here
+ * since we are sending the data to a remote endpoint.
+ */
+
+ private MediaItemMetadata() {
+ }
+
+ /**
+ * String key: Album artist name.
+ * <p>
+ * The value is a string suitable for display.
+ * </p>
+ */
+ public static final String KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST";
+
+ /**
+ * String key: Album title.
+ * <p>
+ * The value is a string suitable for display.
+ * </p>
+ */
+ public static final String KEY_ALBUM_TITLE = "android.media.metadata.ALBUM_TITLE";
+
+ /**
+ * String key: Artwork Uri.
+ * <p>
+ * The value is a string URI for an image file associated with the media item,
+ * such as album or cover art.
+ * </p>
+ */
+ public static final String KEY_ARTWORK_URI = "android.media.metadata.ARTWORK_URI";
+
+ /**
+ * String key: Artist name.
+ * <p>
+ * The value is a string suitable for display.
+ * </p>
+ */
+ public static final String KEY_ARTIST = "android.media.metadata.ARTIST";
+
+ /**
+ * String key: Author name.
+ * <p>
+ * The value is a string suitable for display.
+ * </p>
+ */
+ public static final String KEY_AUTHOR = "android.media.metadata.AUTHOR";
+
+ /**
+ * String key: Composer name.
+ * <p>
+ * The value is a string suitable for display.
+ * </p>
+ */
+ public static final String KEY_COMPOSER = "android.media.metadata.COMPOSER";
+
+ /**
+ * String key: Track title.
+ * <p>
+ * The value is a string suitable for display.
+ * </p>
+ */
+ public static final String KEY_TITLE = "android.media.metadata.TITLE";
+
+ /**
+ * Integer key: Year of publication.
+ * <p>
+ * The value is an integer year number.
+ * </p>
+ */
+ public static final String KEY_YEAR = "android.media.metadata.YEAR";
+
+ /**
+ * Integer key: Track number (such as a track on a CD).
+ * <p>
+ * The value is a one-based integer track number.
+ * </p>
+ */
+ public static final String KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER";
+
+ /**
+ * Integer key: Disc number within a collection.
+ * <p>
+ * The value is a one-based integer disc number.
+ * </p>
+ */
+ public static final String KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER";
+
+ /**
+ * Long key: Item playback duration in milliseconds.
+ * <p>
+ * The value is a <code>long</code> number of milliseconds.
+ * </p><p>
+ * The duration metadata is only a hint to enable a remote media player to
+ * guess the duration of the content before it actually opens the media stream.
+ * The remote media player should still determine the actual content duration from
+ * the media stream itself independent of the value that may be specified by this key.
+ * </p>
+ */
+ public static final String KEY_DURATION = "android.media.metadata.DURATION";
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaItemStatus.java b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaItemStatus.java
new file mode 100644
index 0000000..90ea2d5
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaItemStatus.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.media;
+
+import android.app.PendingIntent;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.util.TimeUtils;
+
+/**
+ * Describes the playback status of a media item.
+ * <p>
+ * This class is part of the remote playback protocol described by the
+ * {@link MediaControlIntent MediaControlIntent} class.
+ * </p><p>
+ * As a media item is played, it transitions through a sequence of states including:
+ * {@link #PLAYBACK_STATE_PENDING pending}, {@link #PLAYBACK_STATE_BUFFERING buffering},
+ * {@link #PLAYBACK_STATE_PLAYING playing}, {@link #PLAYBACK_STATE_PAUSED paused},
+ * {@link #PLAYBACK_STATE_FINISHED finished}, {@link #PLAYBACK_STATE_CANCELED canceled},
+ * {@link #PLAYBACK_STATE_INVALIDATED invalidated}, and
+ * {@link #PLAYBACK_STATE_ERROR error}. Refer to the documentation of each state
+ * for an explanation of its meaning.
+ * </p><p>
+ * While the item is playing, the playback status may also include progress information
+ * about the {@link #getContentPosition content position} and
+ * {@link #getContentDuration content duration} although not all route destinations
+ * will report it.
+ * </p><p>
+ * To monitor playback status, the application should supply a {@link PendingIntent} to use as the
+ * {@link MediaControlIntent#EXTRA_ITEM_STATUS_UPDATE_RECEIVER item status update receiver}
+ * for a given {@link MediaControlIntent#ACTION_PLAY playback request}. Note that
+ * the status update receiver will only be invoked for major status changes such as a
+ * transition from playing to finished.
+ * </p><p class="note">
+ * The status update receiver will not be invoked for minor progress updates such as
+ * changes to playback position or duration. If the application wants to monitor
+ * playback progress, then it must use the
+ * {@link MediaControlIntent#ACTION_GET_STATUS get status request} to poll for changes
+ * periodically and estimate the playback position while playing. Note that there may
+ * be a significant power impact to polling so the application is advised only
+ * to poll when the screen is on and never more than about once every 5 seconds or so.
+ * </p><p>
+ * This object is immutable once created using a {@link Builder} instance.
+ * </p>
+ */
+public final class MediaItemStatus {
+ static final String KEY_TIMESTAMP = "timestamp";
+ static final String KEY_PLAYBACK_STATE = "playbackState";
+ static final String KEY_CONTENT_POSITION = "contentPosition";
+ static final String KEY_CONTENT_DURATION = "contentDuration";
+ static final String KEY_EXTRAS = "extras";
+
+ final Bundle mBundle;
+
+ /**
+ * Playback state: Pending.
+ * <p>
+ * Indicates that the media item has not yet started playback but will be played eventually.
+ * </p>
+ */
+ public static final int PLAYBACK_STATE_PENDING = 0;
+
+ /**
+ * Playback state: Playing.
+ * <p>
+ * Indicates that the media item is currently playing.
+ * </p>
+ */
+ public static final int PLAYBACK_STATE_PLAYING = 1;
+
+ /**
+ * Playback state: Paused.
+ * <p>
+ * Indicates that playback of the media item has been paused. Playback can be
+ * resumed using the {@link MediaControlIntent#ACTION_RESUME resume} action.
+ * </p>
+ */
+ public static final int PLAYBACK_STATE_PAUSED = 2;
+
+ /**
+ * Playback state: Buffering or seeking to a new position.
+ * <p>
+ * Indicates that the media item has been temporarily interrupted
+ * to fetch more content. Playback will continue automatically
+ * when enough content has been buffered.
+ * </p>
+ */
+ public static final int PLAYBACK_STATE_BUFFERING = 3;
+
+ /**
+ * Playback state: Finished.
+ * <p>
+ * Indicates that the media item played to the end of the content and finished normally.
+ * </p><p>
+ * A finished media item cannot be resumed. To play the content again, the application
+ * must send a new {@link MediaControlIntent#ACTION_PLAY play} or
+ * {@link MediaControlIntent#ACTION_ENQUEUE enqueue} action.
+ * </p>
+ */
+ public static final int PLAYBACK_STATE_FINISHED = 4;
+
+ /**
+ * Playback state: Canceled.
+ * <p>
+ * Indicates that the media item was explicitly removed from the queue by the
+ * application. Items may be canceled and removed from the queue using
+ * the {@link MediaControlIntent#ACTION_REMOVE remove} or
+ * {@link MediaControlIntent#ACTION_STOP stop} action or by issuing
+ * another {@link MediaControlIntent#ACTION_PLAY play} action that has the
+ * side-effect of clearing the queue.
+ * </p><p>
+ * A canceled media item cannot be resumed. To play the content again, the
+ * application must send a new {@link MediaControlIntent#ACTION_PLAY play} or
+ * {@link MediaControlIntent#ACTION_ENQUEUE enqueue} action.
+ * </p>
+ */
+ public static final int PLAYBACK_STATE_CANCELED = 5;
+
+ /**
+ * Playback state: Invalidated.
+ * <p>
+ * Indicates that the media item was invalidated permanently and involuntarily.
+ * This state is used to indicate that the media item was invalidated and removed
+ * from the queue because the session to which it belongs was invalidated
+ * (typically by another application taking control of the route).
+ * </p><p>
+ * When invalidation occurs, the application should generally wait for the user
+ * to perform an explicit action, such as clicking on a play button in the UI,
+ * before creating a new media session to avoid unnecessarily interrupting
+ * another application that may have just started using the route.
+ * </p><p>
+ * An invalidated media item cannot be resumed. To play the content again, the application
+ * must send a new {@link MediaControlIntent#ACTION_PLAY play} or
+ * {@link MediaControlIntent#ACTION_ENQUEUE enqueue} action.
+ * </p>
+ */
+ public static final int PLAYBACK_STATE_INVALIDATED = 6;
+
+ /**
+ * Playback state: Playback halted or aborted due to an error.
+ * <p>
+ * Examples of errors are no network connectivity when attempting to retrieve content
+ * from a server, or expired user credentials when trying to play subscription-based
+ * content.
+ * </p><p>
+ * A media item in the error state cannot be resumed. To play the content again,
+ * the application must send a new {@link MediaControlIntent#ACTION_PLAY play} or
+ * {@link MediaControlIntent#ACTION_ENQUEUE enqueue} action.
+ * </p>
+ */
+ public static final int PLAYBACK_STATE_ERROR = 7;
+
+ /**
+ * Integer extra: HTTP status code.
+ * <p>
+ * Specifies the HTTP status code that was encountered when the content
+ * was requested after all redirects were followed. This key only needs to
+ * specified when the content uri uses the HTTP or HTTPS scheme and an error
+ * occurred. This key may be omitted if the content was able to be played
+ * successfully; there is no need to report a 200 (OK) status code.
+ * </p><p>
+ * The value is an integer HTTP status code, such as 401 (Unauthorized),
+ * 404 (Not Found), or 500 (Server Error), or 0 if none.
+ * </p>
+ */
+ public static final String EXTRA_HTTP_STATUS_CODE =
+ "android.media.status.extra.HTTP_STATUS_CODE";
+
+ /**
+ * Bundle extra: HTTP response headers.
+ * <p>
+ * Specifies the HTTP response headers that were returned when the content was
+ * requested from the network. The headers may include additional information
+ * about the content or any errors conditions that were encountered while
+ * trying to fetch the content.
+ * </p><p>
+ * The value is a {@link android.os.Bundle} of string based key-value pairs
+ * that describe the HTTP response headers.
+ * </p>
+ */
+ public static final String EXTRA_HTTP_RESPONSE_HEADERS =
+ "android.media.status.extra.HTTP_RESPONSE_HEADERS";
+
+ MediaItemStatus(Bundle bundle) {
+ mBundle = bundle;
+ }
+
+ /**
+ * Gets the timestamp associated with the status information in
+ * milliseconds since boot in the {@link SystemClock#elapsedRealtime} time base.
+ *
+ * @return The status timestamp in the {@link SystemClock#elapsedRealtime()} time base.
+ */
+ public long getTimestamp() {
+ return mBundle.getLong(KEY_TIMESTAMP);
+ }
+
+ /**
+ * Gets the playback state of the media item.
+ *
+ * @return The playback state. One of {@link #PLAYBACK_STATE_PENDING},
+ * {@link #PLAYBACK_STATE_PLAYING}, {@link #PLAYBACK_STATE_PAUSED},
+ * {@link #PLAYBACK_STATE_BUFFERING}, {@link #PLAYBACK_STATE_FINISHED},
+ * {@link #PLAYBACK_STATE_CANCELED}, {@link #PLAYBACK_STATE_INVALIDATED},
+ * or {@link #PLAYBACK_STATE_ERROR}.
+ */
+ public int getPlaybackState() {
+ return mBundle.getInt(KEY_PLAYBACK_STATE, PLAYBACK_STATE_ERROR);
+ }
+
+ /**
+ * Gets the content playback position as a long integer number of milliseconds
+ * from the beginning of the content.
+ *
+ * @return The content playback position in milliseconds, or -1 if unknown.
+ */
+ public long getContentPosition() {
+ return mBundle.getLong(KEY_CONTENT_POSITION, -1);
+ }
+
+ /**
+ * Gets the total duration of the content to be played as a long integer number of
+ * milliseconds.
+ *
+ * @return The content duration in milliseconds, or -1 if unknown.
+ */
+ public long getContentDuration() {
+ return mBundle.getLong(KEY_CONTENT_DURATION, -1);
+ }
+
+ /**
+ * Gets a bundle of extras for this status object.
+ * The extras will be ignored by the media router but they may be used
+ * by applications.
+ */
+ public Bundle getExtras() {
+ return mBundle.getBundle(KEY_EXTRAS);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("MediaItemStatus{ ");
+ result.append("timestamp=");
+ TimeUtils.formatDuration(SystemClock.elapsedRealtime() - getTimestamp(), result);
+ result.append(" ms ago");
+ result.append(", playbackState=").append(playbackStateToString(getPlaybackState()));
+ result.append(", contentPosition=").append(getContentPosition());
+ result.append(", contentDuration=").append(getContentDuration());
+ result.append(", extras=").append(getExtras());
+ result.append(" }");
+ return result.toString();
+ }
+
+ private static String playbackStateToString(int playbackState) {
+ switch (playbackState) {
+ case PLAYBACK_STATE_PENDING:
+ return "pending";
+ case PLAYBACK_STATE_BUFFERING:
+ return "buffering";
+ case PLAYBACK_STATE_PLAYING:
+ return "playing";
+ case PLAYBACK_STATE_PAUSED:
+ return "paused";
+ case PLAYBACK_STATE_FINISHED:
+ return "finished";
+ case PLAYBACK_STATE_CANCELED:
+ return "canceled";
+ case PLAYBACK_STATE_INVALIDATED:
+ return "invalidated";
+ case PLAYBACK_STATE_ERROR:
+ return "error";
+ }
+ return Integer.toString(playbackState);
+ }
+
+ /**
+ * Converts this object to a bundle for serialization.
+ *
+ * @return The contents of the object represented as a bundle.
+ */
+ public Bundle asBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Creates an instance from a bundle.
+ *
+ * @param bundle The bundle, or null if none.
+ * @return The new instance, or null if the bundle was null.
+ */
+ public static MediaItemStatus fromBundle(Bundle bundle) {
+ return bundle != null ? new MediaItemStatus(bundle) : null;
+ }
+
+ /**
+ * Builder for {@link MediaItemStatus media item status objects}.
+ */
+ public static final class Builder {
+ private final Bundle mBundle;
+
+ /**
+ * Creates a media item status builder using the current time as the
+ * reference timestamp.
+ *
+ * @param playbackState The item playback state.
+ */
+ public Builder(int playbackState) {
+ mBundle = new Bundle();
+ setTimestamp(SystemClock.elapsedRealtime());
+ setPlaybackState(playbackState);
+ }
+
+ /**
+ * Creates a media item status builder whose initial contents are
+ * copied from an existing status.
+ */
+ public Builder(MediaItemStatus status) {
+ if (status == null) {
+ throw new IllegalArgumentException("status must not be null");
+ }
+
+ mBundle = new Bundle(status.mBundle);
+ }
+
+ /**
+ * Sets the timestamp associated with the status information in
+ * milliseconds since boot in the {@link SystemClock#elapsedRealtime} time base.
+ */
+ public Builder setTimestamp(long elapsedRealtimeTimestamp) {
+ mBundle.putLong(KEY_TIMESTAMP, elapsedRealtimeTimestamp);
+ return this;
+ }
+
+ /**
+ * Sets the playback state of the media item.
+ */
+ public Builder setPlaybackState(int playbackState) {
+ mBundle.putInt(KEY_PLAYBACK_STATE, playbackState);
+ return this;
+ }
+
+ /**
+ * Sets the content playback position as a long integer number of milliseconds
+ * from the beginning of the content.
+ */
+ public Builder setContentPosition(long positionMilliseconds) {
+ mBundle.putLong(KEY_CONTENT_POSITION, positionMilliseconds);
+ return this;
+ }
+
+ /**
+ * Sets the total duration of the content to be played as a long integer number
+ * of milliseconds.
+ */
+ public Builder setContentDuration(long durationMilliseconds) {
+ mBundle.putLong(KEY_CONTENT_DURATION, durationMilliseconds);
+ return this;
+ }
+
+ /**
+ * Sets a bundle of extras for this status object.
+ * The extras will be ignored by the media router but they may be used
+ * by applications.
+ */
+ public Builder setExtras(Bundle extras) {
+ mBundle.putBundle(KEY_EXTRAS, extras);
+ return this;
+ }
+
+ /**
+ * Builds the {@link MediaItemStatus media item status object}.
+ */
+ public MediaItemStatus build() {
+ return new MediaItemStatus(mBundle);
+ }
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouteDescriptor.java b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouteDescriptor.java
new file mode 100644
index 0000000..6bc84fc
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouteDescriptor.java
@@ -0,0 +1,693 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.support.mediarouter.media;
+
+import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Describes the properties of a route.
+ * <p>
+ * Each route is uniquely identified by an opaque id string. This token
+ * may take any form as long as it is unique within the media route provider.
+ * </p><p>
+ * This object is immutable once created using a {@link Builder} instance.
+ * </p>
+ */
+public final class MediaRouteDescriptor {
+ static final String KEY_ID = "id";
+ static final String KEY_GROUP_MEMBER_IDS = "groupMemberIds";
+ static final String KEY_NAME = "name";
+ static final String KEY_DESCRIPTION = "status";
+ static final String KEY_ICON_URI = "iconUri";
+ static final String KEY_ENABLED = "enabled";
+ static final String KEY_CONNECTING = "connecting";
+ static final String KEY_CONNECTION_STATE = "connectionState";
+ static final String KEY_CONTROL_FILTERS = "controlFilters";
+ static final String KEY_PLAYBACK_TYPE = "playbackType";
+ static final String KEY_PLAYBACK_STREAM = "playbackStream";
+ static final String KEY_DEVICE_TYPE = "deviceType";
+ static final String KEY_VOLUME = "volume";
+ static final String KEY_VOLUME_MAX = "volumeMax";
+ static final String KEY_VOLUME_HANDLING = "volumeHandling";
+ static final String KEY_PRESENTATION_DISPLAY_ID = "presentationDisplayId";
+ static final String KEY_EXTRAS = "extras";
+ static final String KEY_CAN_DISCONNECT = "canDisconnect";
+ static final String KEY_SETTINGS_INTENT = "settingsIntent";
+ static final String KEY_MIN_CLIENT_VERSION = "minClientVersion";
+ static final String KEY_MAX_CLIENT_VERSION = "maxClientVersion";
+
+ final Bundle mBundle;
+ List<IntentFilter> mControlFilters;
+
+ MediaRouteDescriptor(Bundle bundle, List<IntentFilter> controlFilters) {
+ mBundle = bundle;
+ mControlFilters = controlFilters;
+ }
+
+ /**
+ * Gets the unique id of the route.
+ * <p>
+ * The route id associated with a route descriptor functions as a stable
+ * identifier for the route and must be unique among all routes offered
+ * by the provider.
+ * </p>
+ */
+ public String getId() {
+ return mBundle.getString(KEY_ID);
+ }
+
+ /**
+ * Gets the group member ids of the route.
+ * <p>
+ * A route descriptor that has one or more group member route ids
+ * represents a route group. A member route may belong to another group.
+ * </p>
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public List<String> getGroupMemberIds() {
+ return mBundle.getStringArrayList(KEY_GROUP_MEMBER_IDS);
+ }
+
+ /**
+ * Gets the user-visible name of the route.
+ * <p>
+ * The route name identifies the destination represented by the route.
+ * It may be a user-supplied name, an alias, or device serial number.
+ * </p>
+ */
+ public String getName() {
+ return mBundle.getString(KEY_NAME);
+ }
+
+ /**
+ * Gets the user-visible description of the route.
+ * <p>
+ * The route description describes the kind of destination represented by the route.
+ * It may be a user-supplied string, a model number or brand of device.
+ * </p>
+ */
+ public String getDescription() {
+ return mBundle.getString(KEY_DESCRIPTION);
+ }
+
+ /**
+ * Gets the URI of the icon representing this route.
+ * <p>
+ * This icon will be used in picker UIs if available.
+ * </p>
+ */
+ public Uri getIconUri() {
+ String iconUri = mBundle.getString(KEY_ICON_URI);
+ return iconUri == null ? null : Uri.parse(iconUri);
+ }
+
+ /**
+ * Gets whether the route is enabled.
+ */
+ public boolean isEnabled() {
+ return mBundle.getBoolean(KEY_ENABLED, true);
+ }
+
+ /**
+ * Gets whether the route is connecting.
+ * @deprecated Use {@link #getConnectionState} instead
+ */
+ @Deprecated
+ public boolean isConnecting() {
+ return mBundle.getBoolean(KEY_CONNECTING, false);
+ }
+
+ /**
+ * Gets the connection state of the route.
+ *
+ * @return The connection state of this route:
+ * {@link MediaRouter.RouteInfo#CONNECTION_STATE_DISCONNECTED},
+ * {@link MediaRouter.RouteInfo#CONNECTION_STATE_CONNECTING}, or
+ * {@link MediaRouter.RouteInfo#CONNECTION_STATE_CONNECTED}.
+ */
+ public int getConnectionState() {
+ return mBundle.getInt(KEY_CONNECTION_STATE,
+ MediaRouter.RouteInfo.CONNECTION_STATE_DISCONNECTED);
+ }
+
+ /**
+ * Gets whether the route can be disconnected without stopping playback.
+ * <p>
+ * The route can normally be disconnected without stopping playback when
+ * the destination device on the route is connected to two or more source
+ * devices. The route provider should update the route immediately when the
+ * number of connected devices changes.
+ * </p><p>
+ * To specify that the route should disconnect without stopping use
+ * {@link MediaRouter#unselect(int)} with
+ * {@link MediaRouter#UNSELECT_REASON_DISCONNECTED}.
+ * </p>
+ */
+ public boolean canDisconnectAndKeepPlaying() {
+ return mBundle.getBoolean(KEY_CAN_DISCONNECT, false);
+ }
+
+ /**
+ * Gets an {@link IntentSender} for starting a settings activity for this
+ * route. The activity may have specific route settings or general settings
+ * for the connected device or route provider.
+ *
+ * @return An {@link IntentSender} to start a settings activity.
+ */
+ public IntentSender getSettingsActivity() {
+ return mBundle.getParcelable(KEY_SETTINGS_INTENT);
+ }
+
+ /**
+ * Gets the route's {@link MediaControlIntent media control intent} filters.
+ */
+ public List<IntentFilter> getControlFilters() {
+ ensureControlFilters();
+ return mControlFilters;
+ }
+
+ void ensureControlFilters() {
+ if (mControlFilters == null) {
+ mControlFilters = mBundle.<IntentFilter>getParcelableArrayList(KEY_CONTROL_FILTERS);
+ if (mControlFilters == null) {
+ mControlFilters = Collections.<IntentFilter>emptyList();
+ }
+ }
+ }
+
+ /**
+ * Gets the type of playback associated with this route.
+ *
+ * @return The type of playback associated with this route:
+ * {@link MediaRouter.RouteInfo#PLAYBACK_TYPE_LOCAL} or
+ * {@link MediaRouter.RouteInfo#PLAYBACK_TYPE_REMOTE}.
+ */
+ public int getPlaybackType() {
+ return mBundle.getInt(KEY_PLAYBACK_TYPE, MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE);
+ }
+
+ /**
+ * Gets the route's playback stream.
+ */
+ public int getPlaybackStream() {
+ return mBundle.getInt(KEY_PLAYBACK_STREAM, -1);
+ }
+
+ /**
+ * Gets the type of the receiver device associated with this route.
+ *
+ * @return The type of the receiver device associated with this route:
+ * {@link MediaRouter.RouteInfo#DEVICE_TYPE_TV} or
+ * {@link MediaRouter.RouteInfo#DEVICE_TYPE_SPEAKER}.
+ */
+ public int getDeviceType() {
+ return mBundle.getInt(KEY_DEVICE_TYPE);
+ }
+
+ /**
+ * Gets the route's current volume, or 0 if unknown.
+ */
+ public int getVolume() {
+ return mBundle.getInt(KEY_VOLUME);
+ }
+
+ /**
+ * Gets the route's maximum volume, or 0 if unknown.
+ */
+ public int getVolumeMax() {
+ return mBundle.getInt(KEY_VOLUME_MAX);
+ }
+
+ /**
+ * Gets information about how volume is handled on the route.
+ *
+ * @return How volume is handled on the route:
+ * {@link MediaRouter.RouteInfo#PLAYBACK_VOLUME_FIXED} or
+ * {@link MediaRouter.RouteInfo#PLAYBACK_VOLUME_VARIABLE}.
+ */
+ public int getVolumeHandling() {
+ return mBundle.getInt(KEY_VOLUME_HANDLING,
+ MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED);
+ }
+
+ /**
+ * Gets the route's presentation display id, or -1 if none.
+ */
+ public int getPresentationDisplayId() {
+ return mBundle.getInt(
+ KEY_PRESENTATION_DISPLAY_ID, MediaRouter.RouteInfo.PRESENTATION_DISPLAY_ID_NONE);
+ }
+
+ /**
+ * Gets a bundle of extras for this route descriptor.
+ * The extras will be ignored by the media router but they may be used
+ * by applications.
+ */
+ public Bundle getExtras() {
+ return mBundle.getBundle(KEY_EXTRAS);
+ }
+
+ /**
+ * Gets the minimum client version required for this route.
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public int getMinClientVersion() {
+ return mBundle.getInt(KEY_MIN_CLIENT_VERSION,
+ MediaRouteProviderProtocol.CLIENT_VERSION_START);
+ }
+
+ /**
+ * Gets the maximum client version required for this route.
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public int getMaxClientVersion() {
+ return mBundle.getInt(KEY_MAX_CLIENT_VERSION, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Returns true if the route descriptor has all of the required fields.
+ */
+ public boolean isValid() {
+ ensureControlFilters();
+ if (TextUtils.isEmpty(getId())
+ || TextUtils.isEmpty(getName())
+ || mControlFilters.contains(null)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("MediaRouteDescriptor{ ");
+ result.append("id=").append(getId());
+ result.append(", groupMemberIds=").append(getGroupMemberIds());
+ result.append(", name=").append(getName());
+ result.append(", description=").append(getDescription());
+ result.append(", iconUri=").append(getIconUri());
+ result.append(", isEnabled=").append(isEnabled());
+ result.append(", isConnecting=").append(isConnecting());
+ result.append(", connectionState=").append(getConnectionState());
+ result.append(", controlFilters=").append(Arrays.toString(getControlFilters().toArray()));
+ result.append(", playbackType=").append(getPlaybackType());
+ result.append(", playbackStream=").append(getPlaybackStream());
+ result.append(", deviceType=").append(getDeviceType());
+ result.append(", volume=").append(getVolume());
+ result.append(", volumeMax=").append(getVolumeMax());
+ result.append(", volumeHandling=").append(getVolumeHandling());
+ result.append(", presentationDisplayId=").append(getPresentationDisplayId());
+ result.append(", extras=").append(getExtras());
+ result.append(", isValid=").append(isValid());
+ result.append(", minClientVersion=").append(getMinClientVersion());
+ result.append(", maxClientVersion=").append(getMaxClientVersion());
+ result.append(" }");
+ return result.toString();
+ }
+
+ /**
+ * Converts this object to a bundle for serialization.
+ *
+ * @return The contents of the object represented as a bundle.
+ */
+ public Bundle asBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Creates an instance from a bundle.
+ *
+ * @param bundle The bundle, or null if none.
+ * @return The new instance, or null if the bundle was null.
+ */
+ public static MediaRouteDescriptor fromBundle(Bundle bundle) {
+ return bundle != null ? new MediaRouteDescriptor(bundle, null) : null;
+ }
+
+ /**
+ * Builder for {@link MediaRouteDescriptor media route descriptors}.
+ */
+ public static final class Builder {
+ private final Bundle mBundle;
+ private ArrayList<String> mGroupMemberIds;
+ private ArrayList<IntentFilter> mControlFilters;
+
+ /**
+ * Creates a media route descriptor builder.
+ *
+ * @param id The unique id of the route.
+ * @param name The user-visible name of the route.
+ */
+ public Builder(String id, String name) {
+ mBundle = new Bundle();
+ setId(id);
+ setName(name);
+ }
+
+ /**
+ * Creates a media route descriptor builder whose initial contents are
+ * copied from an existing descriptor.
+ */
+ public Builder(MediaRouteDescriptor descriptor) {
+ if (descriptor == null) {
+ throw new IllegalArgumentException("descriptor must not be null");
+ }
+
+ mBundle = new Bundle(descriptor.mBundle);
+
+ descriptor.ensureControlFilters();
+ if (!descriptor.mControlFilters.isEmpty()) {
+ mControlFilters = new ArrayList<IntentFilter>(descriptor.mControlFilters);
+ }
+ }
+
+ /**
+ * Sets the unique id of the route.
+ * <p>
+ * The route id associated with a route descriptor functions as a stable
+ * identifier for the route and must be unique among all routes offered
+ * by the provider.
+ * </p>
+ */
+ public Builder setId(String id) {
+ mBundle.putString(KEY_ID, id);
+ return this;
+ }
+
+ /**
+ * Adds a group member id of the route.
+ * <p>
+ * A route descriptor that has one or more group member route ids
+ * represents a route group. A member route may belong to another group.
+ * </p>
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public Builder addGroupMemberId(String groupMemberId) {
+ if (TextUtils.isEmpty(groupMemberId)) {
+ throw new IllegalArgumentException("groupMemberId must not be empty");
+ }
+
+ if (mGroupMemberIds == null) {
+ mGroupMemberIds = new ArrayList<>();
+ }
+ if (!mGroupMemberIds.contains(groupMemberId)) {
+ mGroupMemberIds.add(groupMemberId);
+ }
+ return this;
+ }
+
+ /**
+ * Adds a list of group member ids of the route.
+ * <p>
+ * A route descriptor that has one or more group member route ids
+ * represents a route group. A member route may belong to another group.
+ * </p>
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public Builder addGroupMemberIds(Collection<String> groupMemberIds) {
+ if (groupMemberIds == null) {
+ throw new IllegalArgumentException("groupMemberIds must not be null");
+ }
+
+ if (!groupMemberIds.isEmpty()) {
+ for (String groupMemberId : groupMemberIds) {
+ addGroupMemberId(groupMemberId);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Sets the user-visible name of the route.
+ * <p>
+ * The route name identifies the destination represented by the route.
+ * It may be a user-supplied name, an alias, or device serial number.
+ * </p>
+ */
+ public Builder setName(String name) {
+ mBundle.putString(KEY_NAME, name);
+ return this;
+ }
+
+ /**
+ * Sets the user-visible description of the route.
+ * <p>
+ * The route description describes the kind of destination represented by the route.
+ * It may be a user-supplied string, a model number or brand of device.
+ * </p>
+ */
+ public Builder setDescription(String description) {
+ mBundle.putString(KEY_DESCRIPTION, description);
+ return this;
+ }
+
+ /**
+ * Sets the URI of the icon representing this route.
+ * <p>
+ * This icon will be used in picker UIs if available.
+ * </p><p>
+ * The URI must be one of the following formats:
+ * <ul>
+ * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
+ * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})
+ * </li>
+ * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li>
+ * </ul>
+ * </p>
+ */
+ public Builder setIconUri(Uri iconUri) {
+ if (iconUri == null) {
+ throw new IllegalArgumentException("iconUri must not be null");
+ }
+ mBundle.putString(KEY_ICON_URI, iconUri.toString());
+ return this;
+ }
+
+ /**
+ * Sets whether the route is enabled.
+ * <p>
+ * Disabled routes represent routes that a route provider knows about, such as paired
+ * Wifi Display receivers, but that are not currently available for use.
+ * </p>
+ */
+ public Builder setEnabled(boolean enabled) {
+ mBundle.putBoolean(KEY_ENABLED, enabled);
+ return this;
+ }
+
+ /**
+ * Sets whether the route is in the process of connecting and is not yet
+ * ready for use.
+ * @deprecated Use {@link #setConnectionState} instead.
+ */
+ @Deprecated
+ public Builder setConnecting(boolean connecting) {
+ mBundle.putBoolean(KEY_CONNECTING, connecting);
+ return this;
+ }
+
+ /**
+ * Sets the route's connection state.
+ *
+ * @param connectionState The connection state of the route:
+ * {@link MediaRouter.RouteInfo#CONNECTION_STATE_DISCONNECTED},
+ * {@link MediaRouter.RouteInfo#CONNECTION_STATE_CONNECTING}, or
+ * {@link MediaRouter.RouteInfo#CONNECTION_STATE_CONNECTED}.
+ */
+ public Builder setConnectionState(int connectionState) {
+ mBundle.putInt(KEY_CONNECTION_STATE, connectionState);
+ return this;
+ }
+
+ /**
+ * Sets whether the route can be disconnected without stopping playback.
+ */
+ public Builder setCanDisconnect(boolean canDisconnect) {
+ mBundle.putBoolean(KEY_CAN_DISCONNECT, canDisconnect);
+ return this;
+ }
+
+ /**
+ * Sets an intent sender for launching the settings activity for this
+ * route.
+ */
+ public Builder setSettingsActivity(IntentSender is) {
+ mBundle.putParcelable(KEY_SETTINGS_INTENT, is);
+ return this;
+ }
+
+ /**
+ * Adds a {@link MediaControlIntent media control intent} filter for the route.
+ */
+ public Builder addControlFilter(IntentFilter filter) {
+ if (filter == null) {
+ throw new IllegalArgumentException("filter must not be null");
+ }
+
+ if (mControlFilters == null) {
+ mControlFilters = new ArrayList<IntentFilter>();
+ }
+ if (!mControlFilters.contains(filter)) {
+ mControlFilters.add(filter);
+ }
+ return this;
+ }
+
+ /**
+ * Adds a list of {@link MediaControlIntent media control intent} filters for the route.
+ */
+ public Builder addControlFilters(Collection<IntentFilter> filters) {
+ if (filters == null) {
+ throw new IllegalArgumentException("filters must not be null");
+ }
+
+ if (!filters.isEmpty()) {
+ for (IntentFilter filter : filters) {
+ addControlFilter(filter);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Sets the route's playback type.
+ *
+ * @param playbackType The playback type of the route:
+ * {@link MediaRouter.RouteInfo#PLAYBACK_TYPE_LOCAL} or
+ * {@link MediaRouter.RouteInfo#PLAYBACK_TYPE_REMOTE}.
+ */
+ public Builder setPlaybackType(int playbackType) {
+ mBundle.putInt(KEY_PLAYBACK_TYPE, playbackType);
+ return this;
+ }
+
+ /**
+ * Sets the route's playback stream.
+ */
+ public Builder setPlaybackStream(int playbackStream) {
+ mBundle.putInt(KEY_PLAYBACK_STREAM, playbackStream);
+ return this;
+ }
+
+ /**
+ * Sets the route's receiver device type.
+ *
+ * @param deviceType The receive device type of the route:
+ * {@link MediaRouter.RouteInfo#DEVICE_TYPE_TV} or
+ * {@link MediaRouter.RouteInfo#DEVICE_TYPE_SPEAKER}.
+ */
+ public Builder setDeviceType(int deviceType) {
+ mBundle.putInt(KEY_DEVICE_TYPE, deviceType);
+ return this;
+ }
+
+ /**
+ * Sets the route's current volume, or 0 if unknown.
+ */
+ public Builder setVolume(int volume) {
+ mBundle.putInt(KEY_VOLUME, volume);
+ return this;
+ }
+
+ /**
+ * Sets the route's maximum volume, or 0 if unknown.
+ */
+ public Builder setVolumeMax(int volumeMax) {
+ mBundle.putInt(KEY_VOLUME_MAX, volumeMax);
+ return this;
+ }
+
+ /**
+ * Sets the route's volume handling.
+ *
+ * @param volumeHandling how volume is handled on the route:
+ * {@link MediaRouter.RouteInfo#PLAYBACK_VOLUME_FIXED} or
+ * {@link MediaRouter.RouteInfo#PLAYBACK_VOLUME_VARIABLE}.
+ */
+ public Builder setVolumeHandling(int volumeHandling) {
+ mBundle.putInt(KEY_VOLUME_HANDLING, volumeHandling);
+ return this;
+ }
+
+ /**
+ * Sets the route's presentation display id, or -1 if none.
+ */
+ public Builder setPresentationDisplayId(int presentationDisplayId) {
+ mBundle.putInt(KEY_PRESENTATION_DISPLAY_ID, presentationDisplayId);
+ return this;
+ }
+
+ /**
+ * Sets a bundle of extras for this route descriptor.
+ * The extras will be ignored by the media router but they may be used
+ * by applications.
+ */
+ public Builder setExtras(Bundle extras) {
+ mBundle.putBundle(KEY_EXTRAS, extras);
+ return this;
+ }
+
+ /**
+ * Sets the route's minimum client version.
+ * A router whose version is lower than this will not be able to connect to this route.
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public Builder setMinClientVersion(int minVersion) {
+ mBundle.putInt(KEY_MIN_CLIENT_VERSION, minVersion);
+ return this;
+ }
+
+ /**
+ * Sets the route's maximum client version.
+ * A router whose version is higher than this will not be able to connect to this route.
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public Builder setMaxClientVersion(int maxVersion) {
+ mBundle.putInt(KEY_MAX_CLIENT_VERSION, maxVersion);
+ return this;
+ }
+
+ /**
+ * Builds the {@link MediaRouteDescriptor media route descriptor}.
+ */
+ public MediaRouteDescriptor build() {
+ if (mControlFilters != null) {
+ mBundle.putParcelableArrayList(KEY_CONTROL_FILTERS, mControlFilters);
+ }
+ if (mGroupMemberIds != null) {
+ mBundle.putStringArrayList(KEY_GROUP_MEMBER_IDS, mGroupMemberIds);
+ }
+ return new MediaRouteDescriptor(mBundle, mControlFilters);
+ }
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouteDiscoveryRequest.java b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouteDiscoveryRequest.java
new file mode 100644
index 0000000..039627f
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouteDiscoveryRequest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.support.mediarouter.media;
+
+import android.os.Bundle;
+
+/**
+ * Describes the kinds of routes that the media router would like to discover
+ * and whether to perform active scanning.
+ * <p>
+ * This object is immutable once created.
+ * </p>
+ */
+public final class MediaRouteDiscoveryRequest {
+ private static final String KEY_SELECTOR = "selector";
+ private static final String KEY_ACTIVE_SCAN = "activeScan";
+
+ private final Bundle mBundle;
+ private MediaRouteSelector mSelector;
+
+ /**
+ * Creates a media route discovery request.
+ *
+ * @param selector The route selector that specifies the kinds of routes to discover.
+ * @param activeScan True if active scanning should be performed.
+ */
+ public MediaRouteDiscoveryRequest(MediaRouteSelector selector, boolean activeScan) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ mBundle = new Bundle();
+ mSelector = selector;
+ mBundle.putBundle(KEY_SELECTOR, selector.asBundle());
+ mBundle.putBoolean(KEY_ACTIVE_SCAN, activeScan);
+ }
+
+ private MediaRouteDiscoveryRequest(Bundle bundle) {
+ mBundle = bundle;
+ }
+
+ /**
+ * Gets the route selector that specifies the kinds of routes to discover.
+ */
+ public MediaRouteSelector getSelector() {
+ ensureSelector();
+ return mSelector;
+ }
+
+ private void ensureSelector() {
+ if (mSelector == null) {
+ mSelector = MediaRouteSelector.fromBundle(mBundle.getBundle(KEY_SELECTOR));
+ if (mSelector == null) {
+ mSelector = MediaRouteSelector.EMPTY;
+ }
+ }
+ }
+
+ /**
+ * Returns true if active scanning should be performed.
+ *
+ * @see MediaRouter#CALLBACK_FLAG_PERFORM_ACTIVE_SCAN
+ */
+ public boolean isActiveScan() {
+ return mBundle.getBoolean(KEY_ACTIVE_SCAN);
+ }
+
+ /**
+ * Returns true if the discovery request has all of the required fields.
+ */
+ public boolean isValid() {
+ ensureSelector();
+ return mSelector.isValid();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof MediaRouteDiscoveryRequest) {
+ MediaRouteDiscoveryRequest other = (MediaRouteDiscoveryRequest)o;
+ return getSelector().equals(other.getSelector())
+ && isActiveScan() == other.isActiveScan();
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return getSelector().hashCode() ^ (isActiveScan() ? 1 : 0);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("DiscoveryRequest{ selector=").append(getSelector());
+ result.append(", activeScan=").append(isActiveScan());
+ result.append(", isValid=").append(isValid());
+ result.append(" }");
+ return result.toString();
+ }
+
+ /**
+ * Converts this object to a bundle for serialization.
+ *
+ * @return The contents of the object represented as a bundle.
+ */
+ public Bundle asBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Creates an instance from a bundle.
+ *
+ * @param bundle The bundle, or null if none.
+ * @return The new instance, or null if the bundle was null.
+ */
+ public static MediaRouteDiscoveryRequest fromBundle(Bundle bundle) {
+ return bundle != null ? new MediaRouteDiscoveryRequest(bundle) : null;
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouteProvider.java b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouteProvider.java
new file mode 100644
index 0000000..91a2e1a
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouteProvider.java
@@ -0,0 +1,447 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.media;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Message;
+import android.support.v4.util.ObjectsCompat;
+
+import com.android.support.mediarouter.media.MediaRouter.ControlRequestCallback;
+
+/**
+ * Media route providers are used to publish additional media routes for
+ * use within an application. Media route providers may also be declared
+ * as a service to publish additional media routes to all applications
+ * in the system.
+ * <p>
+ * The purpose of a media route provider is to discover media routes that satisfy
+ * the criteria specified by the current {@link MediaRouteDiscoveryRequest} and publish a
+ * {@link MediaRouteProviderDescriptor} with information about each route by calling
+ * {@link #setDescriptor} to notify the currently registered {@link Callback}.
+ * </p><p>
+ * The provider should watch for changes to the discovery request by implementing
+ * {@link #onDiscoveryRequestChanged} and updating the set of routes that it is
+ * attempting to discover. It should also handle route control requests such
+ * as volume changes or {@link MediaControlIntent media control intents}
+ * by implementing {@link #onCreateRouteController} to return a {@link RouteController}
+ * for a particular route.
+ * </p><p>
+ * A media route provider may be used privately within the scope of a single
+ * application process by calling {@link MediaRouter#addProvider MediaRouter.addProvider}
+ * to add it to the local {@link MediaRouter}. A media route provider may also be made
+ * available globally to all applications by registering a {@link MediaRouteProviderService}
+ * in the provider's manifest. When the media route provider is registered
+ * as a service, all applications that use the media router API will be able to
+ * discover and used the provider's routes without having to install anything else.
+ * </p><p>
+ * This object must only be accessed on the main thread.
+ * </p>
+ */
+public abstract class MediaRouteProvider {
+ static final int MSG_DELIVER_DESCRIPTOR_CHANGED = 1;
+ static final int MSG_DELIVER_DISCOVERY_REQUEST_CHANGED = 2;
+
+ private final Context mContext;
+ private final ProviderMetadata mMetadata;
+ private final ProviderHandler mHandler = new ProviderHandler();
+
+ private Callback mCallback;
+
+ private MediaRouteDiscoveryRequest mDiscoveryRequest;
+ private boolean mPendingDiscoveryRequestChange;
+
+ private MediaRouteProviderDescriptor mDescriptor;
+ private boolean mPendingDescriptorChange;
+
+ /**
+ * Creates a media route provider.
+ *
+ * @param context The context.
+ */
+ public MediaRouteProvider(@NonNull Context context) {
+ this(context, null);
+ }
+
+ MediaRouteProvider(Context context, ProviderMetadata metadata) {
+ if (context == null) {
+ throw new IllegalArgumentException("context must not be null");
+ }
+
+ mContext = context;
+ if (metadata == null) {
+ mMetadata = new ProviderMetadata(new ComponentName(context, getClass()));
+ } else {
+ mMetadata = metadata;
+ }
+ }
+
+ /**
+ * Gets the context of the media route provider.
+ */
+ public final Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Gets the provider's handler which is associated with the main thread.
+ */
+ public final Handler getHandler() {
+ return mHandler;
+ }
+
+ /**
+ * Gets some metadata about the provider's implementation.
+ */
+ public final ProviderMetadata getMetadata() {
+ return mMetadata;
+ }
+
+ /**
+ * Sets a callback to invoke when the provider's descriptor changes.
+ *
+ * @param callback The callback to use, or null if none.
+ */
+ public final void setCallback(@Nullable Callback callback) {
+ MediaRouter.checkCallingThread();
+ mCallback = callback;
+ }
+
+ /**
+ * Gets the current discovery request which informs the provider about the
+ * kinds of routes to discover and whether to perform active scanning.
+ *
+ * @return The current discovery request, or null if no discovery is needed at this time.
+ *
+ * @see #onDiscoveryRequestChanged
+ */
+ @Nullable
+ public final MediaRouteDiscoveryRequest getDiscoveryRequest() {
+ return mDiscoveryRequest;
+ }
+
+ /**
+ * Sets a discovery request to inform the provider about the kinds of
+ * routes that its clients would like to discover and whether to perform active scanning.
+ *
+ * @param request The discovery request, or null if no discovery is needed at this time.
+ *
+ * @see #onDiscoveryRequestChanged
+ */
+ public final void setDiscoveryRequest(MediaRouteDiscoveryRequest request) {
+ MediaRouter.checkCallingThread();
+
+ if (ObjectsCompat.equals(mDiscoveryRequest, request)) {
+ return;
+ }
+
+ mDiscoveryRequest = request;
+ if (!mPendingDiscoveryRequestChange) {
+ mPendingDiscoveryRequestChange = true;
+ mHandler.sendEmptyMessage(MSG_DELIVER_DISCOVERY_REQUEST_CHANGED);
+ }
+ }
+
+ void deliverDiscoveryRequestChanged() {
+ mPendingDiscoveryRequestChange = false;
+ onDiscoveryRequestChanged(mDiscoveryRequest);
+ }
+
+ /**
+ * Called by the media router when the {@link MediaRouteDiscoveryRequest discovery request}
+ * has changed.
+ * <p>
+ * Whenever an applications calls {@link MediaRouter#addCallback} to register
+ * a callback, it also provides a selector to specify the kinds of routes that
+ * it is interested in. The media router combines all of these selectors together
+ * to generate a {@link MediaRouteDiscoveryRequest} and notifies each provider when a change
+ * occurs by calling {@link #setDiscoveryRequest} which posts a message to invoke
+ * this method asynchronously.
+ * </p><p>
+ * The provider should examine the {@link MediaControlIntent media control categories}
+ * in the discovery request's {@link MediaRouteSelector selector} to determine what
+ * kinds of routes it should try to discover and whether it should perform active
+ * or passive scans. In many cases, the provider may be able to save power by
+ * determining that the selector does not contain any categories that it supports
+ * and it can therefore avoid performing any scans at all.
+ * </p>
+ *
+ * @param request The new discovery request, or null if no discovery is needed at this time.
+ *
+ * @see MediaRouter#addCallback
+ */
+ public void onDiscoveryRequestChanged(@Nullable MediaRouteDiscoveryRequest request) {
+ }
+
+ /**
+ * Gets the provider's descriptor.
+ * <p>
+ * The descriptor describes the state of the media route provider and
+ * the routes that it publishes. Watch for changes to the descriptor
+ * by registering a {@link Callback callback} with {@link #setCallback}.
+ * </p>
+ *
+ * @return The media route provider descriptor, or null if none.
+ *
+ * @see Callback#onDescriptorChanged
+ */
+ @Nullable
+ public final MediaRouteProviderDescriptor getDescriptor() {
+ return mDescriptor;
+ }
+
+ /**
+ * Sets the provider's descriptor.
+ * <p>
+ * The provider must call this method to notify the currently registered
+ * {@link Callback callback} about the change to the provider's descriptor.
+ * </p>
+ *
+ * @param descriptor The updated route provider descriptor, or null if none.
+ *
+ * @see Callback#onDescriptorChanged
+ */
+ public final void setDescriptor(@Nullable MediaRouteProviderDescriptor descriptor) {
+ MediaRouter.checkCallingThread();
+
+ if (mDescriptor != descriptor) {
+ mDescriptor = descriptor;
+ if (!mPendingDescriptorChange) {
+ mPendingDescriptorChange = true;
+ mHandler.sendEmptyMessage(MSG_DELIVER_DESCRIPTOR_CHANGED);
+ }
+ }
+ }
+
+ void deliverDescriptorChanged() {
+ mPendingDescriptorChange = false;
+
+ if (mCallback != null) {
+ mCallback.onDescriptorChanged(this, mDescriptor);
+ }
+ }
+
+ /**
+ * Called by the media router to obtain a route controller for a particular route.
+ * <p>
+ * The media router will invoke the {@link RouteController#onRelease} method of the route
+ * controller when it is no longer needed to allow it to free its resources.
+ * </p>
+ *
+ * @param routeId The unique id of the route.
+ * @return The route controller. Returns null if there is no such route or if the route
+ * cannot be controlled using the route controller interface.
+ */
+ @Nullable
+ public RouteController onCreateRouteController(@NonNull String routeId) {
+ if (routeId == null) {
+ throw new IllegalArgumentException("routeId cannot be null");
+ }
+ return null;
+ }
+
+ /**
+ * Called by the media router to obtain a route controller for a particular route which is a
+ * member of {@link MediaRouter.RouteGroup}.
+ * <p>
+ * The media router will invoke the {@link RouteController#onRelease} method of the route
+ * controller when it is no longer needed to allow it to free its resources.
+ * </p>
+ *
+ * @param routeId The unique id of the member route.
+ * @param routeGroupId The unique id of the route group.
+ * @return The route controller. Returns null if there is no such route or if the route
+ * cannot be controlled using the route controller interface.
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ @Nullable
+ public RouteController onCreateRouteController(@NonNull String routeId,
+ @NonNull String routeGroupId) {
+ if (routeId == null) {
+ throw new IllegalArgumentException("routeId cannot be null");
+ }
+ if (routeGroupId == null) {
+ throw new IllegalArgumentException("routeGroupId cannot be null");
+ }
+ return onCreateRouteController(routeId);
+ }
+
+ /**
+ * Describes properties of the route provider's implementation.
+ * <p>
+ * This object is immutable once created.
+ * </p>
+ */
+ public static final class ProviderMetadata {
+ private final ComponentName mComponentName;
+
+ ProviderMetadata(ComponentName componentName) {
+ if (componentName == null) {
+ throw new IllegalArgumentException("componentName must not be null");
+ }
+ mComponentName = componentName;
+ }
+
+ /**
+ * Gets the provider's package name.
+ */
+ public String getPackageName() {
+ return mComponentName.getPackageName();
+ }
+
+ /**
+ * Gets the provider's component name.
+ */
+ public ComponentName getComponentName() {
+ return mComponentName;
+ }
+
+ @Override
+ public String toString() {
+ return "ProviderMetadata{ componentName="
+ + mComponentName.flattenToShortString() + " }";
+ }
+ }
+
+ /**
+ * Provides control over a particular route.
+ * <p>
+ * The media router obtains a route controller for a route whenever it needs
+ * to control a route. When a route is selected, the media router invokes
+ * the {@link #onSelect} method of its route controller. While selected,
+ * the media router may call other methods of the route controller to
+ * request that it perform certain actions to the route. When a route is
+ * unselected, the media router invokes the {@link #onUnselect} method of its
+ * route controller. When the media route no longer needs the route controller
+ * it will invoke the {@link #onRelease} method to allow the route controller
+ * to free its resources.
+ * </p><p>
+ * There may be multiple route controllers simultaneously active for the
+ * same route. Each route controller will be released separately.
+ * </p><p>
+ * All operations on the route controller are asynchronous and
+ * results are communicated via callbacks.
+ * </p>
+ */
+ public static abstract class RouteController {
+ /**
+ * Releases the route controller, allowing it to free its resources.
+ */
+ public void onRelease() {
+ }
+
+ /**
+ * Selects the route.
+ */
+ public void onSelect() {
+ }
+
+ /**
+ * Unselects the route.
+ */
+ public void onUnselect() {
+ }
+
+ /**
+ * Unselects the route and provides a reason. The default implementation
+ * calls {@link #onUnselect()}.
+ * <p>
+ * The reason provided will be one of the following:
+ * <ul>
+ * <li>{@link MediaRouter#UNSELECT_REASON_UNKNOWN}</li>
+ * <li>{@link MediaRouter#UNSELECT_REASON_DISCONNECTED}</li>
+ * <li>{@link MediaRouter#UNSELECT_REASON_STOPPED}</li>
+ * <li>{@link MediaRouter#UNSELECT_REASON_ROUTE_CHANGED}</li>
+ * </ul>
+ *
+ * @param reason The reason for unselecting the route.
+ */
+ public void onUnselect(int reason) {
+ onUnselect();
+ }
+
+ /**
+ * Requests to set the volume of the route.
+ *
+ * @param volume The new volume value between 0 and {@link MediaRouteDescriptor#getVolumeMax}.
+ */
+ public void onSetVolume(int volume) {
+ }
+
+ /**
+ * Requests an incremental volume update for the route.
+ *
+ * @param delta The delta to add to the current volume.
+ */
+ public void onUpdateVolume(int delta) {
+ }
+
+ /**
+ * Performs a {@link MediaControlIntent media control} request
+ * asynchronously on behalf of the route.
+ *
+ * @param intent A {@link MediaControlIntent media control intent}.
+ * @param callback A {@link ControlRequestCallback} to invoke with the result
+ * of the request, or null if no result is required.
+ * @return True if the controller intends to handle the request and will
+ * invoke the callback when finished. False if the controller will not
+ * handle the request and will not invoke the callback.
+ *
+ * @see MediaControlIntent
+ */
+ public boolean onControlRequest(Intent intent, @Nullable ControlRequestCallback callback) {
+ return false;
+ }
+ }
+
+ /**
+ * Callback which is invoked when route information becomes available or changes.
+ */
+ public static abstract class Callback {
+ /**
+ * Called when information about a route provider and its routes changes.
+ *
+ * @param provider The media route provider that changed, never null.
+ * @param descriptor The new media route provider descriptor, or null if none.
+ */
+ public void onDescriptorChanged(@NonNull MediaRouteProvider provider,
+ @Nullable MediaRouteProviderDescriptor descriptor) {
+ }
+ }
+
+ private final class ProviderHandler extends Handler {
+ ProviderHandler() {
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_DELIVER_DESCRIPTOR_CHANGED:
+ deliverDescriptorChanged();
+ break;
+ case MSG_DELIVER_DISCOVERY_REQUEST_CHANGED:
+ deliverDiscoveryRequestChanged();
+ break;
+ }
+ }
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouteProviderDescriptor.java b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouteProviderDescriptor.java
new file mode 100644
index 0000000..eb1ce09
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouteProviderDescriptor.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.support.mediarouter.media;
+
+import android.os.Bundle;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Describes the state of a media route provider and the routes that it publishes.
+ * <p>
+ * This object is immutable once created using a {@link Builder} instance.
+ * </p>
+ */
+public final class MediaRouteProviderDescriptor {
+ private static final String KEY_ROUTES = "routes";
+
+ private final Bundle mBundle;
+ private List<MediaRouteDescriptor> mRoutes;
+
+ private MediaRouteProviderDescriptor(Bundle bundle, List<MediaRouteDescriptor> routes) {
+ mBundle = bundle;
+ mRoutes = routes;
+ }
+
+ /**
+ * Gets the list of all routes that this provider has published.
+ */
+ public List<MediaRouteDescriptor> getRoutes() {
+ ensureRoutes();
+ return mRoutes;
+ }
+
+ private void ensureRoutes() {
+ if (mRoutes == null) {
+ ArrayList<Bundle> routeBundles = mBundle.<Bundle>getParcelableArrayList(KEY_ROUTES);
+ if (routeBundles == null || routeBundles.isEmpty()) {
+ mRoutes = Collections.<MediaRouteDescriptor>emptyList();
+ } else {
+ final int count = routeBundles.size();
+ mRoutes = new ArrayList<MediaRouteDescriptor>(count);
+ for (int i = 0; i < count; i++) {
+ mRoutes.add(MediaRouteDescriptor.fromBundle(routeBundles.get(i)));
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns true if the route provider descriptor and all of the routes that
+ * it contains have all of the required fields.
+ * <p>
+ * This verification is deep. If the provider descriptor is known to be
+ * valid then it is not necessary to call {@link #isValid} on each of its routes.
+ * </p>
+ */
+ public boolean isValid() {
+ ensureRoutes();
+ final int routeCount = mRoutes.size();
+ for (int i = 0; i < routeCount; i++) {
+ MediaRouteDescriptor route = mRoutes.get(i);
+ if (route == null || !route.isValid()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("MediaRouteProviderDescriptor{ ");
+ result.append("routes=").append(
+ Arrays.toString(getRoutes().toArray()));
+ result.append(", isValid=").append(isValid());
+ result.append(" }");
+ return result.toString();
+ }
+
+ /**
+ * Converts this object to a bundle for serialization.
+ *
+ * @return The contents of the object represented as a bundle.
+ */
+ public Bundle asBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Creates an instance from a bundle.
+ *
+ * @param bundle The bundle, or null if none.
+ * @return The new instance, or null if the bundle was null.
+ */
+ public static MediaRouteProviderDescriptor fromBundle(Bundle bundle) {
+ return bundle != null ? new MediaRouteProviderDescriptor(bundle, null) : null;
+ }
+
+ /**
+ * Builder for {@link MediaRouteProviderDescriptor media route provider descriptors}.
+ */
+ public static final class Builder {
+ private final Bundle mBundle;
+ private ArrayList<MediaRouteDescriptor> mRoutes;
+
+ /**
+ * Creates an empty media route provider descriptor builder.
+ */
+ public Builder() {
+ mBundle = new Bundle();
+ }
+
+ /**
+ * Creates a media route provider descriptor builder whose initial contents are
+ * copied from an existing descriptor.
+ */
+ public Builder(MediaRouteProviderDescriptor descriptor) {
+ if (descriptor == null) {
+ throw new IllegalArgumentException("descriptor must not be null");
+ }
+
+ mBundle = new Bundle(descriptor.mBundle);
+
+ descriptor.ensureRoutes();
+ if (!descriptor.mRoutes.isEmpty()) {
+ mRoutes = new ArrayList<MediaRouteDescriptor>(descriptor.mRoutes);
+ }
+ }
+
+ /**
+ * Adds a route.
+ */
+ public Builder addRoute(MediaRouteDescriptor route) {
+ if (route == null) {
+ throw new IllegalArgumentException("route must not be null");
+ }
+
+ if (mRoutes == null) {
+ mRoutes = new ArrayList<MediaRouteDescriptor>();
+ } else if (mRoutes.contains(route)) {
+ throw new IllegalArgumentException("route descriptor already added");
+ }
+ mRoutes.add(route);
+ return this;
+ }
+
+ /**
+ * Adds a list of routes.
+ */
+ public Builder addRoutes(Collection<MediaRouteDescriptor> routes) {
+ if (routes == null) {
+ throw new IllegalArgumentException("routes must not be null");
+ }
+
+ if (!routes.isEmpty()) {
+ for (MediaRouteDescriptor route : routes) {
+ addRoute(route);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Sets the list of routes.
+ */
+ Builder setRoutes(Collection<MediaRouteDescriptor> routes) {
+ if (routes == null || routes.isEmpty()) {
+ mRoutes = null;
+ mBundle.remove(KEY_ROUTES);
+ } else {
+ mRoutes = new ArrayList<>(routes);
+ }
+ return this;
+ }
+
+ /**
+ * Builds the {@link MediaRouteProviderDescriptor media route provider descriptor}.
+ */
+ public MediaRouteProviderDescriptor build() {
+ if (mRoutes != null) {
+ final int count = mRoutes.size();
+ ArrayList<Bundle> routeBundles = new ArrayList<Bundle>(count);
+ for (int i = 0; i < count; i++) {
+ routeBundles.add(mRoutes.get(i).asBundle());
+ }
+ mBundle.putParcelableArrayList(KEY_ROUTES, routeBundles);
+ }
+ return new MediaRouteProviderDescriptor(mBundle, mRoutes);
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouteProviderProtocol.java b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouteProviderProtocol.java
new file mode 100644
index 0000000..6be9343
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouteProviderProtocol.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.media;
+
+import android.content.Intent;
+import android.os.Messenger;
+
+/**
+ * Defines the communication protocol for media route provider services.
+ */
+abstract class MediaRouteProviderProtocol {
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ * Put this in your manifest.
+ */
+ public static final String SERVICE_INTERFACE =
+ "android.media.MediaRouteProviderService";
+
+ /*
+ * Messages sent from the client to the service.
+ * DO NOT RENUMBER THESE!
+ */
+
+ /** (client v1)
+ * Register client.
+ * - replyTo : client messenger
+ * - arg1 : request id
+ * - arg2 : client version
+ */
+ public static final int CLIENT_MSG_REGISTER = 1;
+
+ /** (client v1)
+ * Unregister client.
+ * - replyTo : client messenger
+ * - arg1 : request id
+ */
+ public static final int CLIENT_MSG_UNREGISTER = 2;
+
+ /** (client v1)
+ * Create route controller.
+ * - replyTo : client messenger
+ * - arg1 : request id
+ * - arg2 : route controller id
+ * - CLIENT_DATA_ROUTE_ID : route id string
+ */
+ public static final int CLIENT_MSG_CREATE_ROUTE_CONTROLLER = 3;
+
+ /** (client v1)
+ * Release route controller.
+ * - replyTo : client messenger
+ * - arg1 : request id
+ * - arg2 : route controller id
+ */
+ public static final int CLIENT_MSG_RELEASE_ROUTE_CONTROLLER = 4;
+
+ /** (client v1)
+ * Select route.
+ * - replyTo : client messenger
+ * - arg1 : request id
+ * - arg2 : route controller id
+ */
+ public static final int CLIENT_MSG_SELECT_ROUTE = 5;
+
+ /** (client v1)
+ * Unselect route.
+ * - replyTo : client messenger
+ * - arg1 : request id
+ * - arg2 : route controller id
+ */
+ public static final int CLIENT_MSG_UNSELECT_ROUTE = 6;
+
+ /** (client v1)
+ * Set route volume.
+ * - replyTo : client messenger
+ * - arg1 : request id
+ * - arg2 : route controller id
+ * - CLIENT_DATA_VOLUME : volume integer
+ */
+ public static final int CLIENT_MSG_SET_ROUTE_VOLUME = 7;
+
+ /** (client v1)
+ * Update route volume.
+ * - replyTo : client messenger
+ * - arg1 : request id
+ * - arg2 : route controller id
+ * - CLIENT_DATA_VOLUME : volume delta integer
+ */
+ public static final int CLIENT_MSG_UPDATE_ROUTE_VOLUME = 8;
+
+ /** (client v1)
+ * Route control request.
+ * - replyTo : client messenger
+ * - arg1 : request id
+ * - arg2 : route controller id
+ * - obj : media control intent
+ */
+ public static final int CLIENT_MSG_ROUTE_CONTROL_REQUEST = 9;
+
+ /** (client v1)
+ * Sets the discovery request.
+ * - replyTo : client messenger
+ * - arg1 : request id
+ * - obj : discovery request bundle, or null if none
+ */
+ public static final int CLIENT_MSG_SET_DISCOVERY_REQUEST = 10;
+
+ public static final String CLIENT_DATA_ROUTE_ID = "routeId";
+ public static final String CLIENT_DATA_ROUTE_LIBRARY_GROUP = "routeGroupId";
+ public static final String CLIENT_DATA_VOLUME = "volume";
+ public static final String CLIENT_DATA_UNSELECT_REASON = "unselectReason";
+
+ /*
+ * Messages sent from the service to the client.
+ * DO NOT RENUMBER THESE!
+ */
+
+ /** (service v1)
+ * Generic failure sent in response to any unrecognized or malformed request.
+ * - arg1 : request id
+ */
+ public static final int SERVICE_MSG_GENERIC_FAILURE = 0;
+
+ /** (service v1)
+ * Generic failure sent in response to a successful message.
+ * - arg1 : request id
+ */
+ public static final int SERVICE_MSG_GENERIC_SUCCESS = 1;
+
+ /** (service v1)
+ * Registration succeeded.
+ * - arg1 : request id
+ * - arg2 : server version
+ * - obj : route provider descriptor bundle, or null
+ */
+ public static final int SERVICE_MSG_REGISTERED = 2;
+
+ /** (service v1)
+ * Route control request success result.
+ * - arg1 : request id
+ * - obj : result data bundle, or null
+ */
+ public static final int SERVICE_MSG_CONTROL_REQUEST_SUCCEEDED = 3;
+
+ /** (service v1)
+ * Route control request failure result.
+ * - arg1 : request id
+ * - obj : result data bundle, or null
+ * - SERVICE_DATA_ERROR: error message
+ */
+ public static final int SERVICE_MSG_CONTROL_REQUEST_FAILED = 4;
+
+ /** (service v1)
+ * Route provider descriptor changed. (unsolicited event)
+ * - arg1 : reserved (0)
+ * - obj : route provider descriptor bundle, or null
+ */
+ public static final int SERVICE_MSG_DESCRIPTOR_CHANGED = 5;
+
+ public static final String SERVICE_DATA_ERROR = "error";
+
+ /*
+ * Recognized client version numbers. (Reserved for future use.)
+ * DO NOT RENUMBER THESE!
+ */
+
+ /**
+ * The client version used from the beginning.
+ */
+ public static final int CLIENT_VERSION_1 = 1;
+
+ /**
+ * The client version used from support library v24.1.0.
+ */
+ public static final int CLIENT_VERSION_2 = 2;
+
+ /**
+ * The current client version.
+ */
+ public static final int CLIENT_VERSION_CURRENT = CLIENT_VERSION_2;
+
+ /*
+ * Recognized server version numbers. (Reserved for future use.)
+ * DO NOT RENUMBER THESE!
+ */
+
+ /**
+ * The service version used from the beginning.
+ */
+ public static final int SERVICE_VERSION_1 = 1;
+
+ /**
+ * The current service version.
+ */
+ public static final int SERVICE_VERSION_CURRENT = SERVICE_VERSION_1;
+
+ static final int CLIENT_VERSION_START = CLIENT_VERSION_1;
+
+ /**
+ * Returns true if the messenger object is valid.
+ * <p>
+ * The messenger constructor and unparceling code does not check whether the
+ * provided IBinder is a valid IMessenger object. As a result, it's possible
+ * for a peer to send an invalid IBinder that will result in crashes downstream.
+ * This method checks that the messenger is in a valid state.
+ * </p>
+ */
+ public static boolean isValidRemoteMessenger(Messenger messenger) {
+ try {
+ return messenger != null && messenger.getBinder() != null;
+ } catch (NullPointerException ex) {
+ // If the messenger was constructed with a binder interface other than
+ // IMessenger then the call to getBinder() will crash with an NPE.
+ return false;
+ }
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouteProviderService.java b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouteProviderService.java
new file mode 100644
index 0000000..43cde10
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouteProviderService.java
@@ -0,0 +1,759 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.media;
+
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_DATA_ROUTE_ID;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_DATA_ROUTE_LIBRARY_GROUP;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_DATA_UNSELECT_REASON;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_DATA_VOLUME;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_CREATE_ROUTE_CONTROLLER;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_MSG_REGISTER;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_RELEASE_ROUTE_CONTROLLER;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_ROUTE_CONTROL_REQUEST;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_MSG_SELECT_ROUTE;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_SET_DISCOVERY_REQUEST;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_SET_ROUTE_VOLUME;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_MSG_UNREGISTER;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_UNSELECT_ROUTE;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_UPDATE_ROUTE_VOLUME;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_VERSION_1;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.SERVICE_DATA_ERROR;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .SERVICE_MSG_CONTROL_REQUEST_FAILED;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .SERVICE_MSG_CONTROL_REQUEST_SUCCEEDED;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .SERVICE_MSG_DESCRIPTOR_CHANGED;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .SERVICE_MSG_GENERIC_FAILURE;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .SERVICE_MSG_GENERIC_SUCCESS;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.SERVICE_MSG_REGISTERED;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.SERVICE_VERSION_CURRENT;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.isValidRemoteMessenger;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.DeadObjectException;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IBinder.DeathRecipient;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.util.ObjectsCompat;
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+/**
+ * Base class for media route provider services.
+ * <p>
+ * A media router will bind to media route provider services when a callback is added via
+ * {@link MediaRouter#addCallback(MediaRouteSelector, MediaRouter.Callback, int)} with a discovery
+ * flag: {@link MediaRouter#CALLBACK_FLAG_REQUEST_DISCOVERY},
+ * {@link MediaRouter#CALLBACK_FLAG_FORCE_DISCOVERY}, or
+ * {@link MediaRouter#CALLBACK_FLAG_PERFORM_ACTIVE_SCAN}, and will unbind when the callback
+ * is removed via {@link MediaRouter#removeCallback(MediaRouter.Callback)}.
+ * </p><p>
+ * To implement your own media route provider service, extend this class and
+ * override the {@link #onCreateMediaRouteProvider} method to return an
+ * instance of your {@link MediaRouteProvider}.
+ * </p><p>
+ * Declare your media route provider service in your application manifest
+ * like this:
+ * </p>
+ * <pre>
+ * <service android:name=".MyMediaRouteProviderService"
+ * android:label="@string/my_media_route_provider_service">
+ * <intent-filter>
+ * <action android:name="android.media.MediaRouteProviderService" />
+ * </intent-filter>
+ * </service>
+ * </pre>
+ */
+public abstract class MediaRouteProviderService extends Service {
+ static final String TAG = "MediaRouteProviderSrv"; // max. 23 chars
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private final ArrayList<ClientRecord> mClients = new ArrayList<ClientRecord>();
+ private final ReceiveHandler mReceiveHandler;
+ private final Messenger mReceiveMessenger;
+ final PrivateHandler mPrivateHandler;
+ private final ProviderCallback mProviderCallback;
+
+ MediaRouteProvider mProvider;
+ private MediaRouteDiscoveryRequest mCompositeDiscoveryRequest;
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ * Put this in your manifest.
+ */
+ public static final String SERVICE_INTERFACE = MediaRouteProviderProtocol.SERVICE_INTERFACE;
+
+ /*
+ * Private messages used internally. (Yes, you can renumber these.)
+ */
+
+ static final int PRIVATE_MSG_CLIENT_DIED = 1;
+
+ /**
+ * Creates a media route provider service.
+ */
+ public MediaRouteProviderService() {
+ mReceiveHandler = new ReceiveHandler(this);
+ mReceiveMessenger = new Messenger(mReceiveHandler);
+ mPrivateHandler = new PrivateHandler();
+ mProviderCallback = new ProviderCallback();
+ }
+
+ /**
+ * Called by the system when it is time to create the media route provider.
+ *
+ * @return The media route provider offered by this service, or null if
+ * this service has decided not to offer a media route provider.
+ */
+ public abstract MediaRouteProvider onCreateMediaRouteProvider();
+
+ /**
+ * Gets the media route provider offered by this service.
+ *
+ * @return The media route provider offered by this service, or null if
+ * it has not yet been created.
+ *
+ * @see #onCreateMediaRouteProvider()
+ */
+ public MediaRouteProvider getMediaRouteProvider() {
+ return mProvider;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (intent.getAction().equals(SERVICE_INTERFACE)) {
+ if (mProvider == null) {
+ MediaRouteProvider provider = onCreateMediaRouteProvider();
+ if (provider != null) {
+ String providerPackage = provider.getMetadata().getPackageName();
+ if (!providerPackage.equals(getPackageName())) {
+ throw new IllegalStateException("onCreateMediaRouteProvider() returned "
+ + "a provider whose package name does not match the package "
+ + "name of the service. A media route provider service can "
+ + "only export its own media route providers. "
+ + "Provider package name: " + providerPackage
+ + ". Service package name: " + getPackageName() + ".");
+ }
+ mProvider = provider;
+ mProvider.setCallback(mProviderCallback);
+ }
+ }
+ if (mProvider != null) {
+ return mReceiveMessenger.getBinder();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ if (mProvider != null) {
+ mProvider.setCallback(null);
+ }
+ return super.onUnbind(intent);
+ }
+
+ boolean onRegisterClient(Messenger messenger, int requestId, int version) {
+ if (version >= CLIENT_VERSION_1) {
+ int index = findClient(messenger);
+ if (index < 0) {
+ ClientRecord client = new ClientRecord(messenger, version);
+ if (client.register()) {
+ mClients.add(client);
+ if (DEBUG) {
+ Log.d(TAG, client + ": Registered, version=" + version);
+ }
+ if (requestId != 0) {
+ MediaRouteProviderDescriptor descriptor = mProvider.getDescriptor();
+ sendReply(messenger, SERVICE_MSG_REGISTERED,
+ requestId, SERVICE_VERSION_CURRENT,
+ createDescriptorBundleForClientVersion(descriptor,
+ client.mVersion), null);
+ }
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ boolean onUnregisterClient(Messenger messenger, int requestId) {
+ int index = findClient(messenger);
+ if (index >= 0) {
+ ClientRecord client = mClients.remove(index);
+ if (DEBUG) {
+ Log.d(TAG, client + ": Unregistered");
+ }
+ client.dispose();
+ sendGenericSuccess(messenger, requestId);
+ return true;
+ }
+ return false;
+ }
+
+ void onBinderDied(Messenger messenger) {
+ int index = findClient(messenger);
+ if (index >= 0) {
+ ClientRecord client = mClients.remove(index);
+ if (DEBUG) {
+ Log.d(TAG, client + ": Binder died");
+ }
+ client.dispose();
+ }
+ }
+
+ boolean onCreateRouteController(Messenger messenger, int requestId,
+ int controllerId, String routeId, String routeGroupId) {
+ ClientRecord client = getClient(messenger);
+ if (client != null) {
+ if (client.createRouteController(routeId, routeGroupId, controllerId)) {
+ if (DEBUG) {
+ Log.d(TAG, client + ": Route controller created, controllerId=" + controllerId
+ + ", routeId=" + routeId + ", routeGroupId=" + routeGroupId);
+ }
+ sendGenericSuccess(messenger, requestId);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean onReleaseRouteController(Messenger messenger, int requestId,
+ int controllerId) {
+ ClientRecord client = getClient(messenger);
+ if (client != null) {
+ if (client.releaseRouteController(controllerId)) {
+ if (DEBUG) {
+ Log.d(TAG, client + ": Route controller released"
+ + ", controllerId=" + controllerId);
+ }
+ sendGenericSuccess(messenger, requestId);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean onSelectRoute(Messenger messenger, int requestId,
+ int controllerId) {
+ ClientRecord client = getClient(messenger);
+ if (client != null) {
+ MediaRouteProvider.RouteController controller =
+ client.getRouteController(controllerId);
+ if (controller != null) {
+ controller.onSelect();
+ if (DEBUG) {
+ Log.d(TAG, client + ": Route selected"
+ + ", controllerId=" + controllerId);
+ }
+ sendGenericSuccess(messenger, requestId);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean onUnselectRoute(Messenger messenger, int requestId,
+ int controllerId, int reason) {
+ ClientRecord client = getClient(messenger);
+ if (client != null) {
+ MediaRouteProvider.RouteController controller =
+ client.getRouteController(controllerId);
+ if (controller != null) {
+ controller.onUnselect(reason);
+ if (DEBUG) {
+ Log.d(TAG, client + ": Route unselected"
+ + ", controllerId=" + controllerId);
+ }
+ sendGenericSuccess(messenger, requestId);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean onSetRouteVolume(Messenger messenger, int requestId,
+ int controllerId, int volume) {
+ ClientRecord client = getClient(messenger);
+ if (client != null) {
+ MediaRouteProvider.RouteController controller =
+ client.getRouteController(controllerId);
+ if (controller != null) {
+ controller.onSetVolume(volume);
+ if (DEBUG) {
+ Log.d(TAG, client + ": Route volume changed"
+ + ", controllerId=" + controllerId + ", volume=" + volume);
+ }
+ sendGenericSuccess(messenger, requestId);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean onUpdateRouteVolume(Messenger messenger, int requestId,
+ int controllerId, int delta) {
+ ClientRecord client = getClient(messenger);
+ if (client != null) {
+ MediaRouteProvider.RouteController controller =
+ client.getRouteController(controllerId);
+ if (controller != null) {
+ controller.onUpdateVolume(delta);
+ if (DEBUG) {
+ Log.d(TAG, client + ": Route volume updated"
+ + ", controllerId=" + controllerId + ", delta=" + delta);
+ }
+ sendGenericSuccess(messenger, requestId);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean onRouteControlRequest(final Messenger messenger, final int requestId,
+ final int controllerId, final Intent intent) {
+ final ClientRecord client = getClient(messenger);
+ if (client != null) {
+ MediaRouteProvider.RouteController controller =
+ client.getRouteController(controllerId);
+ if (controller != null) {
+ MediaRouter.ControlRequestCallback callback = null;
+ if (requestId != 0) {
+ callback = new MediaRouter.ControlRequestCallback() {
+ @Override
+ public void onResult(Bundle data) {
+ if (DEBUG) {
+ Log.d(TAG, client + ": Route control request succeeded"
+ + ", controllerId=" + controllerId
+ + ", intent=" + intent
+ + ", data=" + data);
+ }
+ if (findClient(messenger) >= 0) {
+ sendReply(messenger, SERVICE_MSG_CONTROL_REQUEST_SUCCEEDED,
+ requestId, 0, data, null);
+ }
+ }
+
+ @Override
+ public void onError(String error, Bundle data) {
+ if (DEBUG) {
+ Log.d(TAG, client + ": Route control request failed"
+ + ", controllerId=" + controllerId
+ + ", intent=" + intent
+ + ", error=" + error + ", data=" + data);
+ }
+ if (findClient(messenger) >= 0) {
+ if (error != null) {
+ Bundle bundle = new Bundle();
+ bundle.putString(SERVICE_DATA_ERROR, error);
+ sendReply(messenger, SERVICE_MSG_CONTROL_REQUEST_FAILED,
+ requestId, 0, data, bundle);
+ } else {
+ sendReply(messenger, SERVICE_MSG_CONTROL_REQUEST_FAILED,
+ requestId, 0, data, null);
+ }
+ }
+ }
+ };
+ }
+ if (controller.onControlRequest(intent, callback)) {
+ if (DEBUG) {
+ Log.d(TAG, client + ": Route control request delivered"
+ + ", controllerId=" + controllerId + ", intent=" + intent);
+ }
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ boolean onSetDiscoveryRequest(Messenger messenger, int requestId,
+ MediaRouteDiscoveryRequest request) {
+ ClientRecord client = getClient(messenger);
+ if (client != null) {
+ boolean actuallyChanged = client.setDiscoveryRequest(request);
+ if (DEBUG) {
+ Log.d(TAG, client + ": Set discovery request, request=" + request
+ + ", actuallyChanged=" + actuallyChanged
+ + ", compositeDiscoveryRequest=" + mCompositeDiscoveryRequest);
+ }
+ sendGenericSuccess(messenger, requestId);
+ return true;
+ }
+ return false;
+ }
+
+ void sendDescriptorChanged(MediaRouteProviderDescriptor descriptor) {
+ final int count = mClients.size();
+ for (int i = 0; i < count; i++) {
+ ClientRecord client = mClients.get(i);
+ sendReply(client.mMessenger, SERVICE_MSG_DESCRIPTOR_CHANGED, 0, 0,
+ createDescriptorBundleForClientVersion(descriptor, client.mVersion), null);
+ if (DEBUG) {
+ Log.d(TAG, client + ": Sent descriptor change event, descriptor=" + descriptor);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ static Bundle createDescriptorBundleForClientVersion(MediaRouteProviderDescriptor descriptor,
+ int clientVersion) {
+ if (descriptor == null) {
+ return null;
+ }
+ MediaRouteProviderDescriptor.Builder builder =
+ new MediaRouteProviderDescriptor.Builder(descriptor);
+ builder.setRoutes(null);
+ for (MediaRouteDescriptor route : descriptor.getRoutes()) {
+ if (clientVersion >= route.getMinClientVersion()
+ && clientVersion <= route.getMaxClientVersion()) {
+ builder.addRoute(route);
+ }
+ }
+ return builder.build().asBundle();
+ }
+
+ boolean updateCompositeDiscoveryRequest() {
+ MediaRouteDiscoveryRequest composite = null;
+ MediaRouteSelector.Builder selectorBuilder = null;
+ boolean activeScan = false;
+ final int count = mClients.size();
+ for (int i = 0; i < count; i++) {
+ MediaRouteDiscoveryRequest request = mClients.get(i).mDiscoveryRequest;
+ if (request != null
+ && (!request.getSelector().isEmpty() || request.isActiveScan())) {
+ activeScan |= request.isActiveScan();
+ if (composite == null) {
+ composite = request;
+ } else {
+ if (selectorBuilder == null) {
+ selectorBuilder = new MediaRouteSelector.Builder(composite.getSelector());
+ }
+ selectorBuilder.addSelector(request.getSelector());
+ }
+ }
+ }
+ if (selectorBuilder != null) {
+ composite = new MediaRouteDiscoveryRequest(selectorBuilder.build(), activeScan);
+ }
+ if (!ObjectsCompat.equals(mCompositeDiscoveryRequest, composite)) {
+ mCompositeDiscoveryRequest = composite;
+ mProvider.setDiscoveryRequest(composite);
+ return true;
+ }
+ return false;
+ }
+
+ private ClientRecord getClient(Messenger messenger) {
+ int index = findClient(messenger);
+ return index >= 0 ? mClients.get(index) : null;
+ }
+
+ int findClient(Messenger messenger) {
+ final int count = mClients.size();
+ for (int i = 0; i < count; i++) {
+ ClientRecord client = mClients.get(i);
+ if (client.hasMessenger(messenger)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ static void sendGenericFailure(Messenger messenger, int requestId) {
+ if (requestId != 0) {
+ sendReply(messenger, SERVICE_MSG_GENERIC_FAILURE, requestId, 0, null, null);
+ }
+ }
+
+ private static void sendGenericSuccess(Messenger messenger, int requestId) {
+ if (requestId != 0) {
+ sendReply(messenger, SERVICE_MSG_GENERIC_SUCCESS, requestId, 0, null, null);
+ }
+ }
+
+ static void sendReply(Messenger messenger, int what,
+ int requestId, int arg, Object obj, Bundle data) {
+ Message msg = Message.obtain();
+ msg.what = what;
+ msg.arg1 = requestId;
+ msg.arg2 = arg;
+ msg.obj = obj;
+ msg.setData(data);
+ try {
+ messenger.send(msg);
+ } catch (DeadObjectException ex) {
+ // The client died.
+ } catch (RemoteException ex) {
+ Log.e(TAG, "Could not send message to " + getClientId(messenger), ex);
+ }
+ }
+
+ static String getClientId(Messenger messenger) {
+ return "Client connection " + messenger.getBinder().toString();
+ }
+
+ private final class PrivateHandler extends Handler {
+ PrivateHandler() {
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case PRIVATE_MSG_CLIENT_DIED:
+ onBinderDied((Messenger)msg.obj);
+ break;
+ }
+ }
+ }
+
+ private final class ProviderCallback extends MediaRouteProvider.Callback {
+ ProviderCallback() {
+ }
+
+ @Override
+ public void onDescriptorChanged(MediaRouteProvider provider,
+ MediaRouteProviderDescriptor descriptor) {
+ sendDescriptorChanged(descriptor);
+ }
+ }
+
+ private final class ClientRecord implements DeathRecipient {
+ public final Messenger mMessenger;
+ public final int mVersion;
+ public MediaRouteDiscoveryRequest mDiscoveryRequest;
+
+ private final SparseArray<MediaRouteProvider.RouteController> mControllers =
+ new SparseArray<MediaRouteProvider.RouteController>();
+
+ public ClientRecord(Messenger messenger, int version) {
+ mMessenger = messenger;
+ mVersion = version;
+ }
+
+ public boolean register() {
+ try {
+ mMessenger.getBinder().linkToDeath(this, 0);
+ return true;
+ } catch (RemoteException ex) {
+ binderDied();
+ }
+ return false;
+ }
+
+ public void dispose() {
+ int count = mControllers.size();
+ for (int i = 0; i < count; i++) {
+ mControllers.valueAt(i).onRelease();
+ }
+ mControllers.clear();
+
+ mMessenger.getBinder().unlinkToDeath(this, 0);
+
+ setDiscoveryRequest(null);
+ }
+
+ public boolean hasMessenger(Messenger other) {
+ return mMessenger.getBinder() == other.getBinder();
+ }
+
+ public boolean createRouteController(String routeId, String routeGroupId,
+ int controllerId) {
+ if (mControllers.indexOfKey(controllerId) < 0) {
+ MediaRouteProvider.RouteController controller = routeGroupId == null
+ ? mProvider.onCreateRouteController(routeId)
+ : mProvider.onCreateRouteController(routeId, routeGroupId);
+ if (controller != null) {
+ mControllers.put(controllerId, controller);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean releaseRouteController(int controllerId) {
+ MediaRouteProvider.RouteController controller = mControllers.get(controllerId);
+ if (controller != null) {
+ mControllers.remove(controllerId);
+ controller.onRelease();
+ return true;
+ }
+ return false;
+ }
+
+ public MediaRouteProvider.RouteController getRouteController(int controllerId) {
+ return mControllers.get(controllerId);
+ }
+
+ public boolean setDiscoveryRequest(MediaRouteDiscoveryRequest request) {
+ if (!ObjectsCompat.equals(mDiscoveryRequest, request)) {
+ mDiscoveryRequest = request;
+ return updateCompositeDiscoveryRequest();
+ }
+ return false;
+ }
+
+ // Runs on a binder thread.
+ @Override
+ public void binderDied() {
+ mPrivateHandler.obtainMessage(PRIVATE_MSG_CLIENT_DIED, mMessenger).sendToTarget();
+ }
+
+ @Override
+ public String toString() {
+ return getClientId(mMessenger);
+ }
+ }
+
+ /**
+ * Handler that receives messages from clients.
+ * <p>
+ * This inner class is static and only retains a weak reference to the service
+ * to prevent the service from being leaked in case one of the clients is holding an
+ * active reference to the server's messenger.
+ * </p><p>
+ * This handler should not be used to handle any messages other than those
+ * that come from the client.
+ * </p>
+ */
+ private static final class ReceiveHandler extends Handler {
+ private final WeakReference<MediaRouteProviderService> mServiceRef;
+
+ public ReceiveHandler(MediaRouteProviderService service) {
+ mServiceRef = new WeakReference<MediaRouteProviderService>(service);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ final Messenger messenger = msg.replyTo;
+ if (isValidRemoteMessenger(messenger)) {
+ final int what = msg.what;
+ final int requestId = msg.arg1;
+ final int arg = msg.arg2;
+ final Object obj = msg.obj;
+ final Bundle data = msg.peekData();
+ if (!processMessage(what, messenger, requestId, arg, obj, data)) {
+ if (DEBUG) {
+ Log.d(TAG, getClientId(messenger) + ": Message failed, what=" + what
+ + ", requestId=" + requestId + ", arg=" + arg
+ + ", obj=" + obj + ", data=" + data);
+ }
+ sendGenericFailure(messenger, requestId);
+ }
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "Ignoring message without valid reply messenger.");
+ }
+ }
+ }
+
+ private boolean processMessage(int what,
+ Messenger messenger, int requestId, int arg, Object obj, Bundle data) {
+ MediaRouteProviderService service = mServiceRef.get();
+ if (service != null) {
+ switch (what) {
+ case CLIENT_MSG_REGISTER:
+ return service.onRegisterClient(messenger, requestId, arg);
+
+ case CLIENT_MSG_UNREGISTER:
+ return service.onUnregisterClient(messenger, requestId);
+
+ case CLIENT_MSG_CREATE_ROUTE_CONTROLLER: {
+ String routeId = data.getString(CLIENT_DATA_ROUTE_ID);
+ String routeGroupId = data.getString(CLIENT_DATA_ROUTE_LIBRARY_GROUP);
+ if (routeId != null) {
+ return service.onCreateRouteController(
+ messenger, requestId, arg, routeId, routeGroupId);
+ }
+ break;
+ }
+
+ case CLIENT_MSG_RELEASE_ROUTE_CONTROLLER:
+ return service.onReleaseRouteController(messenger, requestId, arg);
+
+ case CLIENT_MSG_SELECT_ROUTE:
+ return service.onSelectRoute(messenger, requestId, arg);
+
+ case CLIENT_MSG_UNSELECT_ROUTE:
+ int reason = data == null ?
+ MediaRouter.UNSELECT_REASON_UNKNOWN
+ : data.getInt(CLIENT_DATA_UNSELECT_REASON,
+ MediaRouter.UNSELECT_REASON_UNKNOWN);
+ return service.onUnselectRoute(messenger, requestId, arg, reason);
+
+ case CLIENT_MSG_SET_ROUTE_VOLUME: {
+ int volume = data.getInt(CLIENT_DATA_VOLUME, -1);
+ if (volume >= 0) {
+ return service.onSetRouteVolume(
+ messenger, requestId, arg, volume);
+ }
+ break;
+ }
+
+ case CLIENT_MSG_UPDATE_ROUTE_VOLUME: {
+ int delta = data.getInt(CLIENT_DATA_VOLUME, 0);
+ if (delta != 0) {
+ return service.onUpdateRouteVolume(
+ messenger, requestId, arg, delta);
+ }
+ break;
+ }
+
+ case CLIENT_MSG_ROUTE_CONTROL_REQUEST:
+ if (obj instanceof Intent) {
+ return service.onRouteControlRequest(
+ messenger, requestId, arg, (Intent)obj);
+ }
+ break;
+
+ case CLIENT_MSG_SET_DISCOVERY_REQUEST: {
+ if (obj == null || obj instanceof Bundle) {
+ MediaRouteDiscoveryRequest request =
+ MediaRouteDiscoveryRequest.fromBundle((Bundle)obj);
+ return service.onSetDiscoveryRequest(
+ messenger, requestId,
+ request != null && request.isValid() ? request : null);
+ }
+ }
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouteSelector.java b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouteSelector.java
new file mode 100644
index 0000000..5669b19
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouteSelector.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.support.mediarouter.media;
+
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Describes the capabilities of routes that applications would like to discover and use.
+ * <p>
+ * This object is immutable once created using a {@link Builder} instance.
+ * </p>
+ *
+ * <h3>Example</h3>
+ * <pre>
+ * MediaRouteSelector selectorBuilder = new MediaRouteSelector.Builder()
+ * .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO)
+ * .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
+ * .build();
+ *
+ * MediaRouter router = MediaRouter.getInstance(context);
+ * router.addCallback(selector, callback, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
+ * </pre>
+ */
+public final class MediaRouteSelector {
+ static final String KEY_CONTROL_CATEGORIES = "controlCategories";
+
+ private final Bundle mBundle;
+ List<String> mControlCategories;
+
+ /**
+ * An empty media route selector that will not match any routes.
+ */
+ public static final MediaRouteSelector EMPTY = new MediaRouteSelector(new Bundle(), null);
+
+ MediaRouteSelector(Bundle bundle, List<String> controlCategories) {
+ mBundle = bundle;
+ mControlCategories = controlCategories;
+ }
+
+ /**
+ * Gets the list of {@link MediaControlIntent media control categories} in the selector.
+ *
+ * @return The list of categories.
+ */
+ public List<String> getControlCategories() {
+ ensureControlCategories();
+ return mControlCategories;
+ }
+
+ void ensureControlCategories() {
+ if (mControlCategories == null) {
+ mControlCategories = mBundle.getStringArrayList(KEY_CONTROL_CATEGORIES);
+ if (mControlCategories == null || mControlCategories.isEmpty()) {
+ mControlCategories = Collections.<String>emptyList();
+ }
+ }
+ }
+
+ /**
+ * Returns true if the selector contains the specified category.
+ *
+ * @param category The category to check.
+ * @return True if the category is present.
+ */
+ public boolean hasControlCategory(String category) {
+ if (category != null) {
+ ensureControlCategories();
+ final int categoryCount = mControlCategories.size();
+ for (int i = 0; i < categoryCount; i++) {
+ if (mControlCategories.get(i).equals(category)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the selector matches at least one of the specified control filters.
+ *
+ * @param filters The list of control filters to consider.
+ * @return True if a match is found.
+ */
+ public boolean matchesControlFilters(List<IntentFilter> filters) {
+ if (filters != null) {
+ ensureControlCategories();
+ final int categoryCount = mControlCategories.size();
+ if (categoryCount != 0) {
+ final int filterCount = filters.size();
+ for (int i = 0; i < filterCount; i++) {
+ final IntentFilter filter = filters.get(i);
+ if (filter != null) {
+ for (int j = 0; j < categoryCount; j++) {
+ if (filter.hasCategory(mControlCategories.get(j))) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if this selector contains all of the capabilities described
+ * by the specified selector.
+ *
+ * @param selector The selector to be examined.
+ * @return True if this selector contains all of the capabilities described
+ * by the specified selector.
+ */
+ public boolean contains(MediaRouteSelector selector) {
+ if (selector != null) {
+ ensureControlCategories();
+ selector.ensureControlCategories();
+ return mControlCategories.containsAll(selector.mControlCategories);
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the selector does not specify any capabilities.
+ */
+ public boolean isEmpty() {
+ ensureControlCategories();
+ return mControlCategories.isEmpty();
+ }
+
+ /**
+ * Returns true if the selector has all of the required fields.
+ */
+ public boolean isValid() {
+ ensureControlCategories();
+ if (mControlCategories.contains(null)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof MediaRouteSelector) {
+ MediaRouteSelector other = (MediaRouteSelector)o;
+ ensureControlCategories();
+ other.ensureControlCategories();
+ return mControlCategories.equals(other.mControlCategories);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ ensureControlCategories();
+ return mControlCategories.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("MediaRouteSelector{ ");
+ result.append("controlCategories=").append(
+ Arrays.toString(getControlCategories().toArray()));
+ result.append(" }");
+ return result.toString();
+ }
+
+ /**
+ * Converts this object to a bundle for serialization.
+ *
+ * @return The contents of the object represented as a bundle.
+ */
+ public Bundle asBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Creates an instance from a bundle.
+ *
+ * @param bundle The bundle, or null if none.
+ * @return The new instance, or null if the bundle was null.
+ */
+ public static MediaRouteSelector fromBundle(@Nullable Bundle bundle) {
+ return bundle != null ? new MediaRouteSelector(bundle, null) : null;
+ }
+
+ /**
+ * Builder for {@link MediaRouteSelector media route selectors}.
+ */
+ public static final class Builder {
+ private ArrayList<String> mControlCategories;
+
+ /**
+ * Creates an empty media route selector builder.
+ */
+ public Builder() {
+ }
+
+ /**
+ * Creates a media route selector descriptor builder whose initial contents are
+ * copied from an existing selector.
+ */
+ public Builder(@NonNull MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ selector.ensureControlCategories();
+ if (!selector.mControlCategories.isEmpty()) {
+ mControlCategories = new ArrayList<String>(selector.mControlCategories);
+ }
+ }
+
+ /**
+ * Adds a {@link MediaControlIntent media control category} to the builder.
+ *
+ * @param category The category to add to the set of desired capabilities, such as
+ * {@link MediaControlIntent#CATEGORY_LIVE_AUDIO}.
+ * @return The builder instance for chaining.
+ */
+ @NonNull
+ public Builder addControlCategory(@NonNull String category) {
+ if (category == null) {
+ throw new IllegalArgumentException("category must not be null");
+ }
+
+ if (mControlCategories == null) {
+ mControlCategories = new ArrayList<String>();
+ }
+ if (!mControlCategories.contains(category)) {
+ mControlCategories.add(category);
+ }
+ return this;
+ }
+
+ /**
+ * Adds a list of {@link MediaControlIntent media control categories} to the builder.
+ *
+ * @param categories The list categories to add to the set of desired capabilities,
+ * such as {@link MediaControlIntent#CATEGORY_LIVE_AUDIO}.
+ * @return The builder instance for chaining.
+ */
+ @NonNull
+ public Builder addControlCategories(@NonNull Collection<String> categories) {
+ if (categories == null) {
+ throw new IllegalArgumentException("categories must not be null");
+ }
+
+ if (!categories.isEmpty()) {
+ for (String category : categories) {
+ addControlCategory(category);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Adds the contents of an existing media route selector to the builder.
+ *
+ * @param selector The media route selector whose contents are to be added.
+ * @return The builder instance for chaining.
+ */
+ @NonNull
+ public Builder addSelector(@NonNull MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ addControlCategories(selector.getControlCategories());
+ return this;
+ }
+
+ /**
+ * Builds the {@link MediaRouteSelector media route selector}.
+ */
+ @NonNull
+ public MediaRouteSelector build() {
+ if (mControlCategories == null) {
+ return EMPTY;
+ }
+ Bundle bundle = new Bundle();
+ bundle.putStringArrayList(KEY_CONTROL_CATEGORIES, mControlCategories);
+ return new MediaRouteSelector(bundle, mControlCategories);
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouter.java b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouter.java
new file mode 100644
index 0000000..db0052e
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaRouter.java
@@ -0,0 +1,2999 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.media;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.support.v4.app.ActivityManagerCompat;
+import android.support.v4.hardware.display.DisplayManagerCompat;
+import android.support.v4.media.VolumeProviderCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.util.Pair;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Display;
+
+import com.android.support.mediarouter.media.MediaRouteProvider.ProviderMetadata;
+import com.android.support.mediarouter.media.MediaRouteProvider.RouteController;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * MediaRouter allows applications to control the routing of media channels
+ * and streams from the current device to external speakers and destination devices.
+ * <p>
+ * A MediaRouter instance is retrieved through {@link #getInstance}. Applications
+ * can query the media router about the currently selected route and its capabilities
+ * to determine how to send content to the route's destination. Applications can
+ * also {@link RouteInfo#sendControlRequest send control requests} to the route
+ * to ask the route's destination to perform certain remote control functions
+ * such as playing media.
+ * </p><p>
+ * See also {@link MediaRouteProvider} for information on how an application
+ * can publish new media routes to the media router.
+ * </p><p>
+ * The media router API is not thread-safe; all interactions with it must be
+ * done from the main thread of the process.
+ * </p>
+ */
+public final class MediaRouter {
+ static final String TAG = "MediaRouter";
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ /**
+ * Passed to {@link android.support.v7.media.MediaRouteProvider.RouteController#onUnselect(int)}
+ * and {@link Callback#onRouteUnselected(MediaRouter, RouteInfo, int)} when the reason the route
+ * was unselected is unknown.
+ */
+ public static final int UNSELECT_REASON_UNKNOWN = 0;
+ /**
+ * Passed to {@link android.support.v7.media.MediaRouteProvider.RouteController#onUnselect(int)}
+ * and {@link Callback#onRouteUnselected(MediaRouter, RouteInfo, int)} when the user pressed
+ * the disconnect button to disconnect and keep playing.
+ * <p>
+ *
+ * @see MediaRouteDescriptor#canDisconnectAndKeepPlaying()
+ */
+ public static final int UNSELECT_REASON_DISCONNECTED = 1;
+ /**
+ * Passed to {@link android.support.v7.media.MediaRouteProvider.RouteController#onUnselect(int)}
+ * and {@link Callback#onRouteUnselected(MediaRouter, RouteInfo, int)} when the user pressed
+ * the stop casting button.
+ */
+ public static final int UNSELECT_REASON_STOPPED = 2;
+ /**
+ * Passed to {@link android.support.v7.media.MediaRouteProvider.RouteController#onUnselect(int)}
+ * and {@link Callback#onRouteUnselected(MediaRouter, RouteInfo, int)} when the user selected
+ * a different route.
+ */
+ public static final int UNSELECT_REASON_ROUTE_CHANGED = 3;
+
+ // Maintains global media router state for the process.
+ // This field is initialized in MediaRouter.getInstance() before any
+ // MediaRouter objects are instantiated so it is guaranteed to be
+ // valid whenever any instance method is invoked.
+ static GlobalMediaRouter sGlobal;
+
+ // Context-bound state of the media router.
+ final Context mContext;
+ final ArrayList<CallbackRecord> mCallbackRecords = new ArrayList<CallbackRecord>();
+
+ @IntDef(flag = true,
+ value = {
+ CALLBACK_FLAG_PERFORM_ACTIVE_SCAN,
+ CALLBACK_FLAG_REQUEST_DISCOVERY,
+ CALLBACK_FLAG_UNFILTERED_EVENTS
+ }
+ )
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface CallbackFlags {}
+
+ /**
+ * Flag for {@link #addCallback}: Actively scan for routes while this callback
+ * is registered.
+ * <p>
+ * When this flag is specified, the media router will actively scan for new
+ * routes. Certain routes, such as wifi display routes, may not be discoverable
+ * except when actively scanning. This flag is typically used when the route picker
+ * dialog has been opened by the user to ensure that the route information is
+ * up to date.
+ * </p><p>
+ * Active scanning may consume a significant amount of power and may have intrusive
+ * effects on wireless connectivity. Therefore it is important that active scanning
+ * only be requested when it is actually needed to satisfy a user request to
+ * discover and select a new route.
+ * </p><p>
+ * This flag implies {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} but performing
+ * active scans is much more expensive than a normal discovery request.
+ * </p>
+ *
+ * @see #CALLBACK_FLAG_REQUEST_DISCOVERY
+ */
+ public static final int CALLBACK_FLAG_PERFORM_ACTIVE_SCAN = 1 << 0;
+
+ /**
+ * Flag for {@link #addCallback}: Do not filter route events.
+ * <p>
+ * When this flag is specified, the callback will be invoked for events that affect any
+ * route even if they do not match the callback's filter.
+ * </p>
+ */
+ public static final int CALLBACK_FLAG_UNFILTERED_EVENTS = 1 << 1;
+
+ /**
+ * Flag for {@link #addCallback}: Request passive route discovery while this
+ * callback is registered, except on {@link ActivityManager#isLowRamDevice low-RAM devices}.
+ * <p>
+ * When this flag is specified, the media router will try to discover routes.
+ * Although route discovery is intended to be efficient, checking for new routes may
+ * result in some network activity and could slowly drain the battery. Therefore
+ * applications should only specify {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} when
+ * they are running in the foreground and would like to provide the user with the
+ * option of connecting to new routes.
+ * </p><p>
+ * Applications should typically add a callback using this flag in the
+ * {@link android.app.Activity activity's} {@link android.app.Activity#onStart onStart}
+ * method and remove it in the {@link android.app.Activity#onStop onStop} method.
+ * The {@link android.support.v7.app.MediaRouteDiscoveryFragment} fragment may
+ * also be used for this purpose.
+ * </p><p class="note">
+ * On {@link ActivityManager#isLowRamDevice low-RAM devices} this flag
+ * will be ignored. Refer to
+ * {@link #addCallback(MediaRouteSelector, Callback, int) addCallback} for details.
+ * </p>
+ *
+ * @see android.support.v7.app.MediaRouteDiscoveryFragment
+ */
+ public static final int CALLBACK_FLAG_REQUEST_DISCOVERY = 1 << 2;
+
+ /**
+ * Flag for {@link #addCallback}: Request passive route discovery while this
+ * callback is registered, even on {@link ActivityManager#isLowRamDevice low-RAM devices}.
+ * <p class="note">
+ * This flag has a significant performance impact on low-RAM devices
+ * since it may cause many media route providers to be started simultaneously.
+ * It is much better to use {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} instead to avoid
+ * performing passive discovery on these devices altogether. Refer to
+ * {@link #addCallback(MediaRouteSelector, Callback, int) addCallback} for details.
+ * </p>
+ *
+ * @see android.support.v7.app.MediaRouteDiscoveryFragment
+ */
+ public static final int CALLBACK_FLAG_FORCE_DISCOVERY = 1 << 3;
+
+ /**
+ * Flag for {@link #isRouteAvailable}: Ignore the default route.
+ * <p>
+ * This flag is used to determine whether a matching non-default route is available.
+ * This constraint may be used to decide whether to offer the route chooser dialog
+ * to the user. There is no point offering the chooser if there are no
+ * non-default choices.
+ * </p>
+ */
+ public static final int AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE = 1 << 0;
+
+ /**
+ * Flag for {@link #isRouteAvailable}: Require an actual route to be matched.
+ * <p>
+ * If this flag is not set, then {@link #isRouteAvailable} will return true
+ * if it is possible to discover a matching route even if discovery is not in
+ * progress or if no matching route has yet been found. This feature is used to
+ * save resources by removing the need to perform passive route discovery on
+ * {@link ActivityManager#isLowRamDevice low-RAM devices}.
+ * </p><p>
+ * If this flag is set, then {@link #isRouteAvailable} will only return true if
+ * a matching route has actually been discovered.
+ * </p>
+ */
+ public static final int AVAILABILITY_FLAG_REQUIRE_MATCH = 1 << 1;
+
+ private MediaRouter(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Gets an instance of the media router service associated with the context.
+ * <p>
+ * The application is responsible for holding a strong reference to the returned
+ * {@link MediaRouter} instance, such as by storing the instance in a field of
+ * the {@link android.app.Activity}, to ensure that the media router remains alive
+ * as long as the application is using its features.
+ * </p><p>
+ * In other words, the support library only holds a {@link WeakReference weak reference}
+ * to each media router instance. When there are no remaining strong references to the
+ * media router instance, all of its callbacks will be removed and route discovery
+ * will no longer be performed on its behalf.
+ * </p>
+ *
+ * @return The media router instance for the context. The application must hold
+ * a strong reference to this object as long as it is in use.
+ */
+ public static MediaRouter getInstance(@NonNull Context context) {
+ if (context == null) {
+ throw new IllegalArgumentException("context must not be null");
+ }
+ checkCallingThread();
+
+ if (sGlobal == null) {
+ sGlobal = new GlobalMediaRouter(context.getApplicationContext());
+ sGlobal.start();
+ }
+ return sGlobal.getRouter(context);
+ }
+
+ /**
+ * Gets information about the {@link MediaRouter.RouteInfo routes} currently known to
+ * this media router.
+ */
+ public List<RouteInfo> getRoutes() {
+ checkCallingThread();
+ return sGlobal.getRoutes();
+ }
+
+ /**
+ * Gets information about the {@link MediaRouter.ProviderInfo route providers}
+ * currently known to this media router.
+ */
+ public List<ProviderInfo> getProviders() {
+ checkCallingThread();
+ return sGlobal.getProviders();
+ }
+
+ /**
+ * Gets the default route for playing media content on the system.
+ * <p>
+ * The system always provides a default route.
+ * </p>
+ *
+ * @return The default route, which is guaranteed to never be null.
+ */
+ @NonNull
+ public RouteInfo getDefaultRoute() {
+ checkCallingThread();
+ return sGlobal.getDefaultRoute();
+ }
+
+ /**
+ * Gets a bluetooth route for playing media content on the system.
+ *
+ * @return A bluetooth route, if exist, otherwise null.
+ */
+ public RouteInfo getBluetoothRoute() {
+ checkCallingThread();
+ return sGlobal.getBluetoothRoute();
+ }
+
+ /**
+ * Gets the currently selected route.
+ * <p>
+ * The application should examine the route's
+ * {@link RouteInfo#getControlFilters media control intent filters} to assess the
+ * capabilities of the route before attempting to use it.
+ * </p>
+ *
+ * <h3>Example</h3>
+ * <pre>
+ * public boolean playMovie() {
+ * MediaRouter mediaRouter = MediaRouter.getInstance(context);
+ * MediaRouter.RouteInfo route = mediaRouter.getSelectedRoute();
+ *
+ * // First try using the remote playback interface, if supported.
+ * if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
+ * // The route supports remote playback.
+ * // Try to send it the Uri of the movie to play.
+ * Intent intent = new Intent(MediaControlIntent.ACTION_PLAY);
+ * intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ * intent.setDataAndType("http://example.com/videos/movie.mp4", "video/mp4");
+ * if (route.supportsControlRequest(intent)) {
+ * route.sendControlRequest(intent, null);
+ * return true; // sent the request to play the movie
+ * }
+ * }
+ *
+ * // If remote playback was not possible, then play locally.
+ * if (route.supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO)) {
+ * // The route supports live video streaming.
+ * // Prepare to play content locally in a window or in a presentation.
+ * return playMovieInWindow();
+ * }
+ *
+ * // Neither interface is supported, so we can't play the movie to this route.
+ * return false;
+ * }
+ * </pre>
+ *
+ * @return The selected route, which is guaranteed to never be null.
+ *
+ * @see RouteInfo#getControlFilters
+ * @see RouteInfo#supportsControlCategory
+ * @see RouteInfo#supportsControlRequest
+ */
+ @NonNull
+ public RouteInfo getSelectedRoute() {
+ checkCallingThread();
+ return sGlobal.getSelectedRoute();
+ }
+
+ /**
+ * Returns the selected route if it matches the specified selector, otherwise
+ * selects the default route and returns it. If there is one live audio route
+ * (usually Bluetooth A2DP), it will be selected instead of default route.
+ *
+ * @param selector The selector to match.
+ * @return The previously selected route if it matched the selector, otherwise the
+ * newly selected default route which is guaranteed to never be null.
+ *
+ * @see MediaRouteSelector
+ * @see RouteInfo#matchesSelector
+ */
+ @NonNull
+ public RouteInfo updateSelectedRoute(@NonNull MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+ checkCallingThread();
+
+ if (DEBUG) {
+ Log.d(TAG, "updateSelectedRoute: " + selector);
+ }
+ RouteInfo route = sGlobal.getSelectedRoute();
+ if (!route.isDefaultOrBluetooth() && !route.matchesSelector(selector)) {
+ route = sGlobal.chooseFallbackRoute();
+ sGlobal.selectRoute(route);
+ }
+ return route;
+ }
+
+ /**
+ * Selects the specified route.
+ *
+ * @param route The route to select.
+ */
+ public void selectRoute(@NonNull RouteInfo route) {
+ if (route == null) {
+ throw new IllegalArgumentException("route must not be null");
+ }
+ checkCallingThread();
+
+ if (DEBUG) {
+ Log.d(TAG, "selectRoute: " + route);
+ }
+ sGlobal.selectRoute(route);
+ }
+
+ /**
+ * Unselects the current round and selects the default route instead.
+ * <p>
+ * The reason given must be one of:
+ * <ul>
+ * <li>{@link MediaRouter#UNSELECT_REASON_UNKNOWN}</li>
+ * <li>{@link MediaRouter#UNSELECT_REASON_DISCONNECTED}</li>
+ * <li>{@link MediaRouter#UNSELECT_REASON_STOPPED}</li>
+ * <li>{@link MediaRouter#UNSELECT_REASON_ROUTE_CHANGED}</li>
+ * </ul>
+ *
+ * @param reason The reason for disconnecting the current route.
+ */
+ public void unselect(int reason) {
+ if (reason < MediaRouter.UNSELECT_REASON_UNKNOWN ||
+ reason > MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
+ throw new IllegalArgumentException("Unsupported reason to unselect route");
+ }
+ checkCallingThread();
+
+ // Choose the fallback route if it's not already selected.
+ // Otherwise, select the default route.
+ RouteInfo fallbackRoute = sGlobal.chooseFallbackRoute();
+ if (sGlobal.getSelectedRoute() != fallbackRoute) {
+ sGlobal.selectRoute(fallbackRoute, reason);
+ } else {
+ sGlobal.selectRoute(sGlobal.getDefaultRoute(), reason);
+ }
+ }
+
+ /**
+ * Returns true if there is a route that matches the specified selector.
+ * <p>
+ * This method returns true if there are any available routes that match the
+ * selector regardless of whether they are enabled or disabled. If the
+ * {@link #AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE} flag is specified, then
+ * the method will only consider non-default routes.
+ * </p>
+ * <p class="note">
+ * On {@link ActivityManager#isLowRamDevice low-RAM devices} this method
+ * will return true if it is possible to discover a matching route even if
+ * discovery is not in progress or if no matching route has yet been found.
+ * Use {@link #AVAILABILITY_FLAG_REQUIRE_MATCH} to require an actual match.
+ * </p>
+ *
+ * @param selector The selector to match.
+ * @param flags Flags to control the determination of whether a route may be
+ * available. May be zero or some combination of
+ * {@link #AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE} and
+ * {@link #AVAILABILITY_FLAG_REQUIRE_MATCH}.
+ * @return True if a matching route may be available.
+ */
+ public boolean isRouteAvailable(@NonNull MediaRouteSelector selector, int flags) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+ checkCallingThread();
+
+ return sGlobal.isRouteAvailable(selector, flags);
+ }
+
+ /**
+ * Registers a callback to discover routes that match the selector and to receive
+ * events when they change.
+ * <p>
+ * This is a convenience method that has the same effect as calling
+ * {@link #addCallback(MediaRouteSelector, Callback, int)} without flags.
+ * </p>
+ *
+ * @param selector A route selector that indicates the kinds of routes that the
+ * callback would like to discover.
+ * @param callback The callback to add.
+ * @see #removeCallback
+ */
+ public void addCallback(MediaRouteSelector selector, Callback callback) {
+ addCallback(selector, callback, 0);
+ }
+
+ /**
+ * Registers a callback to discover routes that match the selector and to receive
+ * events when they change.
+ * <p>
+ * The selector describes the kinds of routes that the application wants to
+ * discover. For example, if the application wants to use
+ * live audio routes then it should include the
+ * {@link MediaControlIntent#CATEGORY_LIVE_AUDIO live audio media control intent category}
+ * in its selector when it adds a callback to the media router.
+ * The selector may include any number of categories.
+ * </p><p>
+ * If the callback has already been registered, then the selector is added to
+ * the set of selectors being monitored by the callback.
+ * </p><p>
+ * By default, the callback will only be invoked for events that affect routes
+ * that match the specified selector. Event filtering may be disabled by specifying
+ * the {@link #CALLBACK_FLAG_UNFILTERED_EVENTS} flag when the callback is registered.
+ * </p><p>
+ * Applications should use the {@link #isRouteAvailable} method to determine
+ * whether is it possible to discover a route with the desired capabilities
+ * and therefore whether the media route button should be shown to the user.
+ * </p><p>
+ * The {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} flag should be used while the application
+ * is in the foreground to request that passive discovery be performed if there are
+ * sufficient resources to allow continuous passive discovery.
+ * On {@link ActivityManager#isLowRamDevice low-RAM devices} this flag will be
+ * ignored to conserve resources.
+ * </p><p>
+ * The {@link #CALLBACK_FLAG_FORCE_DISCOVERY} flag should be used when
+ * passive discovery absolutely must be performed, even on low-RAM devices.
+ * This flag has a significant performance impact on low-RAM devices
+ * since it may cause many media route providers to be started simultaneously.
+ * It is much better to use {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} instead to avoid
+ * performing passive discovery on these devices altogether.
+ * </p><p>
+ * The {@link #CALLBACK_FLAG_PERFORM_ACTIVE_SCAN} flag should be used when the
+ * media route chooser dialog is showing to confirm the presence of available
+ * routes that the user may connect to. This flag may use substantially more
+ * power.
+ * </p>
+ *
+ * <h3>Example</h3>
+ * <pre>
+ * public class MyActivity extends Activity {
+ * private MediaRouter mRouter;
+ * private MediaRouter.Callback mCallback;
+ * private MediaRouteSelector mSelector;
+ *
+ * protected void onCreate(Bundle savedInstanceState) {
+ * super.onCreate(savedInstanceState);
+ *
+ * mRouter = Mediarouter.getInstance(this);
+ * mCallback = new MyCallback();
+ * mSelector = new MediaRouteSelector.Builder()
+ * .addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
+ * .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
+ * .build();
+ * }
+ *
+ * // Add the callback on start to tell the media router what kinds of routes
+ * // the application is interested in so that it can try to discover suitable ones.
+ * public void onStart() {
+ * super.onStart();
+ *
+ * mediaRouter.addCallback(mSelector, mCallback,
+ * MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
+ *
+ * MediaRouter.RouteInfo route = mediaRouter.updateSelectedRoute(mSelector);
+ * // do something with the route...
+ * }
+ *
+ * // Remove the selector on stop to tell the media router that it no longer
+ * // needs to invest effort trying to discover routes of these kinds for now.
+ * public void onStop() {
+ * super.onStop();
+ *
+ * mediaRouter.removeCallback(mCallback);
+ * }
+ *
+ * private final class MyCallback extends MediaRouter.Callback {
+ * // Implement callback methods as needed.
+ * }
+ * }
+ * </pre>
+ *
+ * @param selector A route selector that indicates the kinds of routes that the
+ * callback would like to discover.
+ * @param callback The callback to add.
+ * @param flags Flags to control the behavior of the callback.
+ * May be zero or a combination of {@link #CALLBACK_FLAG_PERFORM_ACTIVE_SCAN} and
+ * {@link #CALLBACK_FLAG_UNFILTERED_EVENTS}.
+ * @see #removeCallback
+ */
+ public void addCallback(@NonNull MediaRouteSelector selector, @NonNull Callback callback,
+ @CallbackFlags int flags) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("callback must not be null");
+ }
+ checkCallingThread();
+
+ if (DEBUG) {
+ Log.d(TAG, "addCallback: selector=" + selector
+ + ", callback=" + callback + ", flags=" + Integer.toHexString(flags));
+ }
+
+ CallbackRecord record;
+ int index = findCallbackRecord(callback);
+ if (index < 0) {
+ record = new CallbackRecord(this, callback);
+ mCallbackRecords.add(record);
+ } else {
+ record = mCallbackRecords.get(index);
+ }
+ boolean updateNeeded = false;
+ if ((flags & ~record.mFlags) != 0) {
+ record.mFlags |= flags;
+ updateNeeded = true;
+ }
+ if (!record.mSelector.contains(selector)) {
+ record.mSelector = new MediaRouteSelector.Builder(record.mSelector)
+ .addSelector(selector)
+ .build();
+ updateNeeded = true;
+ }
+ if (updateNeeded) {
+ sGlobal.updateDiscoveryRequest();
+ }
+ }
+
+ /**
+ * Removes the specified callback. It will no longer receive events about
+ * changes to media routes.
+ *
+ * @param callback The callback to remove.
+ * @see #addCallback
+ */
+ public void removeCallback(@NonNull Callback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback must not be null");
+ }
+ checkCallingThread();
+
+ if (DEBUG) {
+ Log.d(TAG, "removeCallback: callback=" + callback);
+ }
+
+ int index = findCallbackRecord(callback);
+ if (index >= 0) {
+ mCallbackRecords.remove(index);
+ sGlobal.updateDiscoveryRequest();
+ }
+ }
+
+ private int findCallbackRecord(Callback callback) {
+ final int count = mCallbackRecords.size();
+ for (int i = 0; i < count; i++) {
+ if (mCallbackRecords.get(i).mCallback == callback) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Registers a media route provider within this application process.
+ * <p>
+ * The provider will be added to the list of providers that all {@link MediaRouter}
+ * instances within this process can use to discover routes.
+ * </p>
+ *
+ * @param providerInstance The media route provider instance to add.
+ *
+ * @see MediaRouteProvider
+ * @see #removeCallback
+ */
+ public void addProvider(@NonNull MediaRouteProvider providerInstance) {
+ if (providerInstance == null) {
+ throw new IllegalArgumentException("providerInstance must not be null");
+ }
+ checkCallingThread();
+
+ if (DEBUG) {
+ Log.d(TAG, "addProvider: " + providerInstance);
+ }
+ sGlobal.addProvider(providerInstance);
+ }
+
+ /**
+ * Unregisters a media route provider within this application process.
+ * <p>
+ * The provider will be removed from the list of providers that all {@link MediaRouter}
+ * instances within this process can use to discover routes.
+ * </p>
+ *
+ * @param providerInstance The media route provider instance to remove.
+ *
+ * @see MediaRouteProvider
+ * @see #addCallback
+ */
+ public void removeProvider(@NonNull MediaRouteProvider providerInstance) {
+ if (providerInstance == null) {
+ throw new IllegalArgumentException("providerInstance must not be null");
+ }
+ checkCallingThread();
+
+ if (DEBUG) {
+ Log.d(TAG, "removeProvider: " + providerInstance);
+ }
+ sGlobal.removeProvider(providerInstance);
+ }
+
+ /**
+ * Adds a remote control client to enable remote control of the volume
+ * of the selected route.
+ * <p>
+ * The remote control client must have previously been registered with
+ * the audio manager using the {@link android.media.AudioManager#registerRemoteControlClient
+ * AudioManager.registerRemoteControlClient} method.
+ * </p>
+ *
+ * @param remoteControlClient The {@link android.media.RemoteControlClient} to register.
+ */
+ public void addRemoteControlClient(@NonNull Object remoteControlClient) {
+ if (remoteControlClient == null) {
+ throw new IllegalArgumentException("remoteControlClient must not be null");
+ }
+ checkCallingThread();
+
+ if (DEBUG) {
+ Log.d(TAG, "addRemoteControlClient: " + remoteControlClient);
+ }
+ sGlobal.addRemoteControlClient(remoteControlClient);
+ }
+
+ /**
+ * Removes a remote control client.
+ *
+ * @param remoteControlClient The {@link android.media.RemoteControlClient}
+ * to unregister.
+ */
+ public void removeRemoteControlClient(@NonNull Object remoteControlClient) {
+ if (remoteControlClient == null) {
+ throw new IllegalArgumentException("remoteControlClient must not be null");
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "removeRemoteControlClient: " + remoteControlClient);
+ }
+ sGlobal.removeRemoteControlClient(remoteControlClient);
+ }
+
+ /**
+ * Sets the media session to enable remote control of the volume of the
+ * selected route. This should be used instead of
+ * {@link #addRemoteControlClient} when using media sessions. Set the
+ * session to null to clear it.
+ *
+ * @param mediaSession The {@link android.media.session.MediaSession} to
+ * use.
+ */
+ public void setMediaSession(Object mediaSession) {
+ if (DEBUG) {
+ Log.d(TAG, "addMediaSession: " + mediaSession);
+ }
+ sGlobal.setMediaSession(mediaSession);
+ }
+
+ /**
+ * Sets a compat media session to enable remote control of the volume of the
+ * selected route. This should be used instead of
+ * {@link #addRemoteControlClient} when using {@link MediaSessionCompat}.
+ * Set the session to null to clear it.
+ *
+ * @param mediaSession
+ */
+ public void setMediaSessionCompat(MediaSessionCompat mediaSession) {
+ if (DEBUG) {
+ Log.d(TAG, "addMediaSessionCompat: " + mediaSession);
+ }
+ sGlobal.setMediaSessionCompat(mediaSession);
+ }
+
+ public MediaSessionCompat.Token getMediaSessionToken() {
+ return sGlobal.getMediaSessionToken();
+ }
+
+ /**
+ * Ensures that calls into the media router are on the correct thread.
+ * It pays to be a little paranoid when global state invariants are at risk.
+ */
+ static void checkCallingThread() {
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ throw new IllegalStateException("The media router service must only be "
+ + "accessed on the application's main thread.");
+ }
+ }
+
+ static <T> boolean equal(T a, T b) {
+ return a == b || (a != null && b != null && a.equals(b));
+ }
+
+ /**
+ * Provides information about a media route.
+ * <p>
+ * Each media route has a list of {@link MediaControlIntent media control}
+ * {@link #getControlFilters intent filters} that describe the capabilities of the
+ * route and the manner in which it is used and controlled.
+ * </p>
+ */
+ public static class RouteInfo {
+ private final ProviderInfo mProvider;
+ private final String mDescriptorId;
+ private final String mUniqueId;
+ private String mName;
+ private String mDescription;
+ private Uri mIconUri;
+ private boolean mEnabled;
+ private boolean mConnecting;
+ private int mConnectionState;
+ private boolean mCanDisconnect;
+ private final ArrayList<IntentFilter> mControlFilters = new ArrayList<>();
+ private int mPlaybackType;
+ private int mPlaybackStream;
+ private int mDeviceType;
+ private int mVolumeHandling;
+ private int mVolume;
+ private int mVolumeMax;
+ private Display mPresentationDisplay;
+ private int mPresentationDisplayId = PRESENTATION_DISPLAY_ID_NONE;
+ private Bundle mExtras;
+ private IntentSender mSettingsIntent;
+ MediaRouteDescriptor mDescriptor;
+
+ @IntDef({CONNECTION_STATE_DISCONNECTED, CONNECTION_STATE_CONNECTING,
+ CONNECTION_STATE_CONNECTED})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface ConnectionState {}
+
+ /**
+ * The default connection state indicating the route is disconnected.
+ *
+ * @see #getConnectionState
+ */
+ public static final int CONNECTION_STATE_DISCONNECTED = 0;
+
+ /**
+ * A connection state indicating the route is in the process of connecting and is not yet
+ * ready for use.
+ *
+ * @see #getConnectionState
+ */
+ public static final int CONNECTION_STATE_CONNECTING = 1;
+
+ /**
+ * A connection state indicating the route is connected.
+ *
+ * @see #getConnectionState
+ */
+ public static final int CONNECTION_STATE_CONNECTED = 2;
+
+ @IntDef({PLAYBACK_TYPE_LOCAL,PLAYBACK_TYPE_REMOTE})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface PlaybackType {}
+
+ /**
+ * The default playback type, "local", indicating the presentation of the media
+ * is happening on the same device (e.g. a phone, a tablet) as where it is
+ * controlled from.
+ *
+ * @see #getPlaybackType
+ */
+ public static final int PLAYBACK_TYPE_LOCAL = 0;
+
+ /**
+ * A playback type indicating the presentation of the media is happening on
+ * a different device (i.e. the remote device) than where it is controlled from.
+ *
+ * @see #getPlaybackType
+ */
+ public static final int PLAYBACK_TYPE_REMOTE = 1;
+
+ @IntDef({DEVICE_TYPE_UNKNOWN, DEVICE_TYPE_TV, DEVICE_TYPE_SPEAKER, DEVICE_TYPE_BLUETOOTH})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface DeviceType {}
+
+ /**
+ * The default receiver device type of the route indicating the type is unknown.
+ *
+ * @see #getDeviceType
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public static final int DEVICE_TYPE_UNKNOWN = 0;
+
+ /**
+ * A receiver device type of the route indicating the presentation of the media is happening
+ * on a TV.
+ *
+ * @see #getDeviceType
+ */
+ public static final int DEVICE_TYPE_TV = 1;
+
+ /**
+ * A receiver device type of the route indicating the presentation of the media is happening
+ * on a speaker.
+ *
+ * @see #getDeviceType
+ */
+ public static final int DEVICE_TYPE_SPEAKER = 2;
+
+ /**
+ * A receiver device type of the route indicating the presentation of the media is happening
+ * on a bluetooth device such as a bluetooth speaker.
+ *
+ * @see #getDeviceType
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public static final int DEVICE_TYPE_BLUETOOTH = 3;
+
+ @IntDef({PLAYBACK_VOLUME_FIXED,PLAYBACK_VOLUME_VARIABLE})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface PlaybackVolume {}
+
+ /**
+ * Playback information indicating the playback volume is fixed, i.e. it cannot be
+ * controlled from this object. An example of fixed playback volume is a remote player,
+ * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather
+ * than attenuate at the source.
+ *
+ * @see #getVolumeHandling
+ */
+ public static final int PLAYBACK_VOLUME_FIXED = 0;
+
+ /**
+ * Playback information indicating the playback volume is variable and can be controlled
+ * from this object.
+ *
+ * @see #getVolumeHandling
+ */
+ public static final int PLAYBACK_VOLUME_VARIABLE = 1;
+
+ /**
+ * The default presentation display id indicating no presentation display is associated
+ * with the route.
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public static final int PRESENTATION_DISPLAY_ID_NONE = -1;
+
+ static final int CHANGE_GENERAL = 1 << 0;
+ static final int CHANGE_VOLUME = 1 << 1;
+ static final int CHANGE_PRESENTATION_DISPLAY = 1 << 2;
+
+ // Should match to SystemMediaRouteProvider.PACKAGE_NAME.
+ static final String SYSTEM_MEDIA_ROUTE_PROVIDER_PACKAGE_NAME = "android";
+
+ RouteInfo(ProviderInfo provider, String descriptorId, String uniqueId) {
+ mProvider = provider;
+ mDescriptorId = descriptorId;
+ mUniqueId = uniqueId;
+ }
+
+ /**
+ * Gets information about the provider of this media route.
+ */
+ public ProviderInfo getProvider() {
+ return mProvider;
+ }
+
+ /**
+ * Gets the unique id of the route.
+ * <p>
+ * The route unique id functions as a stable identifier by which the route is known.
+ * For example, an application can use this id as a token to remember the
+ * selected route across restarts or to communicate its identity to a service.
+ * </p>
+ *
+ * @return The unique id of the route, never null.
+ */
+ @NonNull
+ public String getId() {
+ return mUniqueId;
+ }
+
+ /**
+ * Gets the user-visible name of the route.
+ * <p>
+ * The route name identifies the destination represented by the route.
+ * It may be a user-supplied name, an alias, or device serial number.
+ * </p>
+ *
+ * @return The user-visible name of a media route. This is the string presented
+ * to users who may select this as the active route.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Gets the user-visible description of the route.
+ * <p>
+ * The route description describes the kind of destination represented by the route.
+ * It may be a user-supplied string, a model number or brand of device.
+ * </p>
+ *
+ * @return The description of the route, or null if none.
+ */
+ @Nullable
+ public String getDescription() {
+ return mDescription;
+ }
+
+ /**
+ * Gets the URI of the icon representing this route.
+ * <p>
+ * This icon will be used in picker UIs if available.
+ * </p>
+ *
+ * @return The URI of the icon representing this route, or null if none.
+ */
+ public Uri getIconUri() {
+ return mIconUri;
+ }
+
+ /**
+ * Returns true if this route is enabled and may be selected.
+ *
+ * @return True if this route is enabled.
+ */
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ /**
+ * Returns true if the route is in the process of connecting and is not
+ * yet ready for use.
+ *
+ * @return True if this route is in the process of connecting.
+ */
+ public boolean isConnecting() {
+ return mConnecting;
+ }
+
+ /**
+ * Gets the connection state of the route.
+ *
+ * @return The connection state of this route: {@link #CONNECTION_STATE_DISCONNECTED},
+ * {@link #CONNECTION_STATE_CONNECTING}, or {@link #CONNECTION_STATE_CONNECTED}.
+ */
+ @ConnectionState
+ public int getConnectionState() {
+ return mConnectionState;
+ }
+
+ /**
+ * Returns true if this route is currently selected.
+ *
+ * @return True if this route is currently selected.
+ *
+ * @see MediaRouter#getSelectedRoute
+ */
+ public boolean isSelected() {
+ checkCallingThread();
+ return sGlobal.getSelectedRoute() == this;
+ }
+
+ /**
+ * Returns true if this route is the default route.
+ *
+ * @return True if this route is the default route.
+ *
+ * @see MediaRouter#getDefaultRoute
+ */
+ public boolean isDefault() {
+ checkCallingThread();
+ return sGlobal.getDefaultRoute() == this;
+ }
+
+ /**
+ * Returns true if this route is a bluetooth route.
+ *
+ * @return True if this route is a bluetooth route.
+ *
+ * @see MediaRouter#getBluetoothRoute
+ */
+ public boolean isBluetooth() {
+ checkCallingThread();
+ return sGlobal.getBluetoothRoute() == this;
+ }
+
+ /**
+ * Returns true if this route is the default route and the device speaker.
+ *
+ * @return True if this route is the default route and the device speaker.
+ */
+ public boolean isDeviceSpeaker() {
+ int defaultAudioRouteNameResourceId = Resources.getSystem().getIdentifier(
+ "default_audio_route_name", "string", "android");
+ return isDefault()
+ && Resources.getSystem().getText(defaultAudioRouteNameResourceId).equals(mName);
+ }
+
+ /**
+ * Gets a list of {@link MediaControlIntent media control intent} filters that
+ * describe the capabilities of this route and the media control actions that
+ * it supports.
+ *
+ * @return A list of intent filters that specifies the media control intents that
+ * this route supports.
+ *
+ * @see MediaControlIntent
+ * @see #supportsControlCategory
+ * @see #supportsControlRequest
+ */
+ public List<IntentFilter> getControlFilters() {
+ return mControlFilters;
+ }
+
+ /**
+ * Returns true if the route supports at least one of the capabilities
+ * described by a media route selector.
+ *
+ * @param selector The selector that specifies the capabilities to check.
+ * @return True if the route supports at least one of the capabilities
+ * described in the media route selector.
+ */
+ public boolean matchesSelector(@NonNull MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+ checkCallingThread();
+ return selector.matchesControlFilters(mControlFilters);
+ }
+
+ /**
+ * Returns true if the route supports the specified
+ * {@link MediaControlIntent media control} category.
+ * <p>
+ * Media control categories describe the capabilities of this route
+ * such as whether it supports live audio streaming or remote playback.
+ * </p>
+ *
+ * @param category A {@link MediaControlIntent media control} category
+ * such as {@link MediaControlIntent#CATEGORY_LIVE_AUDIO},
+ * {@link MediaControlIntent#CATEGORY_LIVE_VIDEO},
+ * {@link MediaControlIntent#CATEGORY_REMOTE_PLAYBACK}, or a provider-defined
+ * media control category.
+ * @return True if the route supports the specified intent category.
+ *
+ * @see MediaControlIntent
+ * @see #getControlFilters
+ */
+ public boolean supportsControlCategory(@NonNull String category) {
+ if (category == null) {
+ throw new IllegalArgumentException("category must not be null");
+ }
+ checkCallingThread();
+
+ int count = mControlFilters.size();
+ for (int i = 0; i < count; i++) {
+ if (mControlFilters.get(i).hasCategory(category)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the route supports the specified
+ * {@link MediaControlIntent media control} category and action.
+ * <p>
+ * Media control actions describe specific requests that an application
+ * can ask a route to perform.
+ * </p>
+ *
+ * @param category A {@link MediaControlIntent media control} category
+ * such as {@link MediaControlIntent#CATEGORY_LIVE_AUDIO},
+ * {@link MediaControlIntent#CATEGORY_LIVE_VIDEO},
+ * {@link MediaControlIntent#CATEGORY_REMOTE_PLAYBACK}, or a provider-defined
+ * media control category.
+ * @param action A {@link MediaControlIntent media control} action
+ * such as {@link MediaControlIntent#ACTION_PLAY}.
+ * @return True if the route supports the specified intent action.
+ *
+ * @see MediaControlIntent
+ * @see #getControlFilters
+ */
+ public boolean supportsControlAction(@NonNull String category, @NonNull String action) {
+ if (category == null) {
+ throw new IllegalArgumentException("category must not be null");
+ }
+ if (action == null) {
+ throw new IllegalArgumentException("action must not be null");
+ }
+ checkCallingThread();
+
+ int count = mControlFilters.size();
+ for (int i = 0; i < count; i++) {
+ IntentFilter filter = mControlFilters.get(i);
+ if (filter.hasCategory(category) && filter.hasAction(action)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the route supports the specified
+ * {@link MediaControlIntent media control} request.
+ * <p>
+ * Media control requests are used to request the route to perform
+ * actions such as starting remote playback of a media item.
+ * </p>
+ *
+ * @param intent A {@link MediaControlIntent media control intent}.
+ * @return True if the route can handle the specified intent.
+ *
+ * @see MediaControlIntent
+ * @see #getControlFilters
+ */
+ public boolean supportsControlRequest(@NonNull Intent intent) {
+ if (intent == null) {
+ throw new IllegalArgumentException("intent must not be null");
+ }
+ checkCallingThread();
+
+ ContentResolver contentResolver = sGlobal.getContentResolver();
+ int count = mControlFilters.size();
+ for (int i = 0; i < count; i++) {
+ if (mControlFilters.get(i).match(contentResolver, intent, true, TAG) >= 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Sends a {@link MediaControlIntent media control} request to be performed
+ * asynchronously by the route's destination.
+ * <p>
+ * Media control requests are used to request the route to perform
+ * actions such as starting remote playback of a media item.
+ * </p><p>
+ * This function may only be called on a selected route. Control requests
+ * sent to unselected routes will fail.
+ * </p>
+ *
+ * @param intent A {@link MediaControlIntent media control intent}.
+ * @param callback A {@link ControlRequestCallback} to invoke with the result
+ * of the request, or null if no result is required.
+ *
+ * @see MediaControlIntent
+ */
+ public void sendControlRequest(@NonNull Intent intent,
+ @Nullable ControlRequestCallback callback) {
+ if (intent == null) {
+ throw new IllegalArgumentException("intent must not be null");
+ }
+ checkCallingThread();
+
+ sGlobal.sendControlRequest(this, intent, callback);
+ }
+
+ /**
+ * Gets the type of playback associated with this route.
+ *
+ * @return The type of playback associated with this route: {@link #PLAYBACK_TYPE_LOCAL}
+ * or {@link #PLAYBACK_TYPE_REMOTE}.
+ */
+ @PlaybackType
+ public int getPlaybackType() {
+ return mPlaybackType;
+ }
+
+ /**
+ * Gets the audio stream over which the playback associated with this route is performed.
+ *
+ * @return The stream over which the playback associated with this route is performed.
+ */
+ public int getPlaybackStream() {
+ return mPlaybackStream;
+ }
+
+ /**
+ * Gets the type of the receiver device associated with this route.
+ *
+ * @return The type of the receiver device associated with this route:
+ * {@link #DEVICE_TYPE_TV} or {@link #DEVICE_TYPE_SPEAKER}.
+ */
+ public int getDeviceType() {
+ return mDeviceType;
+ }
+
+
+ /**
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public boolean isDefaultOrBluetooth() {
+ if (isDefault() || mDeviceType == DEVICE_TYPE_BLUETOOTH) {
+ return true;
+ }
+ // This is a workaround for platform version 23 or below where the system route
+ // provider doesn't specify device type for bluetooth media routes.
+ return isSystemMediaRouteProvider(this)
+ && supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
+ && !supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
+ }
+
+ /**
+ * Returns {@code true} if the route is selectable.
+ */
+ boolean isSelectable() {
+ // This tests whether the route is still valid and enabled.
+ // The route descriptor field is set to null when the route is removed.
+ return mDescriptor != null && mEnabled;
+ }
+
+ private static boolean isSystemMediaRouteProvider(MediaRouter.RouteInfo route) {
+ return TextUtils.equals(route.getProviderInstance().getMetadata().getPackageName(),
+ SYSTEM_MEDIA_ROUTE_PROVIDER_PACKAGE_NAME);
+ }
+
+ /**
+ * Gets information about how volume is handled on the route.
+ *
+ * @return How volume is handled on the route: {@link #PLAYBACK_VOLUME_FIXED}
+ * or {@link #PLAYBACK_VOLUME_VARIABLE}.
+ */
+ @PlaybackVolume
+ public int getVolumeHandling() {
+ return mVolumeHandling;
+ }
+
+ /**
+ * Gets the current volume for this route. Depending on the route, this may only
+ * be valid if the route is currently selected.
+ *
+ * @return The volume at which the playback associated with this route is performed.
+ */
+ public int getVolume() {
+ return mVolume;
+ }
+
+ /**
+ * Gets the maximum volume at which the playback associated with this route is performed.
+ *
+ * @return The maximum volume at which the playback associated with
+ * this route is performed.
+ */
+ public int getVolumeMax() {
+ return mVolumeMax;
+ }
+
+ /**
+ * Gets whether this route supports disconnecting without interrupting
+ * playback.
+ *
+ * @return True if this route can disconnect without stopping playback,
+ * false otherwise.
+ */
+ public boolean canDisconnect() {
+ return mCanDisconnect;
+ }
+
+ /**
+ * Requests a volume change for this route asynchronously.
+ * <p>
+ * This function may only be called on a selected route. It will have
+ * no effect if the route is currently unselected.
+ * </p>
+ *
+ * @param volume The new volume value between 0 and {@link #getVolumeMax}.
+ */
+ public void requestSetVolume(int volume) {
+ checkCallingThread();
+ sGlobal.requestSetVolume(this, Math.min(mVolumeMax, Math.max(0, volume)));
+ }
+
+ /**
+ * Requests an incremental volume update for this route asynchronously.
+ * <p>
+ * This function may only be called on a selected route. It will have
+ * no effect if the route is currently unselected.
+ * </p>
+ *
+ * @param delta The delta to add to the current volume.
+ */
+ public void requestUpdateVolume(int delta) {
+ checkCallingThread();
+ if (delta != 0) {
+ sGlobal.requestUpdateVolume(this, delta);
+ }
+ }
+
+ /**
+ * Gets the {@link Display} that should be used by the application to show
+ * a {@link android.app.Presentation} on an external display when this route is selected.
+ * Depending on the route, this may only be valid if the route is currently
+ * selected.
+ * <p>
+ * The preferred presentation display may change independently of the route
+ * being selected or unselected. For example, the presentation display
+ * of the default system route may change when an external HDMI display is connected
+ * or disconnected even though the route itself has not changed.
+ * </p><p>
+ * This method may return null if there is no external display associated with
+ * the route or if the display is not ready to show UI yet.
+ * </p><p>
+ * The application should listen for changes to the presentation display
+ * using the {@link Callback#onRoutePresentationDisplayChanged} callback and
+ * show or dismiss its {@link android.app.Presentation} accordingly when the display
+ * becomes available or is removed.
+ * </p><p>
+ * This method only makes sense for
+ * {@link MediaControlIntent#CATEGORY_LIVE_VIDEO live video} routes.
+ * </p>
+ *
+ * @return The preferred presentation display to use when this route is
+ * selected or null if none.
+ *
+ * @see MediaControlIntent#CATEGORY_LIVE_VIDEO
+ * @see android.app.Presentation
+ */
+ @Nullable
+ public Display getPresentationDisplay() {
+ checkCallingThread();
+ if (mPresentationDisplayId >= 0 && mPresentationDisplay == null) {
+ mPresentationDisplay = sGlobal.getDisplay(mPresentationDisplayId);
+ }
+ return mPresentationDisplay;
+ }
+
+ /**
+ * Gets the route's presentation display id, or -1 if none.
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public int getPresentationDisplayId() {
+ return mPresentationDisplayId;
+ }
+
+ /**
+ * Gets a collection of extra properties about this route that were supplied
+ * by its media route provider, or null if none.
+ */
+ @Nullable
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Gets an intent sender for launching a settings activity for this
+ * route.
+ */
+ @Nullable
+ public IntentSender getSettingsIntent() {
+ return mSettingsIntent;
+ }
+
+ /**
+ * Selects this media route.
+ */
+ public void select() {
+ checkCallingThread();
+ sGlobal.selectRoute(this);
+ }
+
+ @Override
+ public String toString() {
+ return "MediaRouter.RouteInfo{ uniqueId=" + mUniqueId
+ + ", name=" + mName
+ + ", description=" + mDescription
+ + ", iconUri=" + mIconUri
+ + ", enabled=" + mEnabled
+ + ", connecting=" + mConnecting
+ + ", connectionState=" + mConnectionState
+ + ", canDisconnect=" + mCanDisconnect
+ + ", playbackType=" + mPlaybackType
+ + ", playbackStream=" + mPlaybackStream
+ + ", deviceType=" + mDeviceType
+ + ", volumeHandling=" + mVolumeHandling
+ + ", volume=" + mVolume
+ + ", volumeMax=" + mVolumeMax
+ + ", presentationDisplayId=" + mPresentationDisplayId
+ + ", extras=" + mExtras
+ + ", settingsIntent=" + mSettingsIntent
+ + ", providerPackageName=" + mProvider.getPackageName()
+ + " }";
+ }
+
+ int maybeUpdateDescriptor(MediaRouteDescriptor descriptor) {
+ int changes = 0;
+ if (mDescriptor != descriptor) {
+ changes = updateDescriptor(descriptor);
+ }
+ return changes;
+ }
+
+ int updateDescriptor(MediaRouteDescriptor descriptor) {
+ int changes = 0;
+ mDescriptor = descriptor;
+ if (descriptor != null) {
+ if (!equal(mName, descriptor.getName())) {
+ mName = descriptor.getName();
+ changes |= CHANGE_GENERAL;
+ }
+ if (!equal(mDescription, descriptor.getDescription())) {
+ mDescription = descriptor.getDescription();
+ changes |= CHANGE_GENERAL;
+ }
+ if (!equal(mIconUri, descriptor.getIconUri())) {
+ mIconUri = descriptor.getIconUri();
+ changes |= CHANGE_GENERAL;
+ }
+ if (mEnabled != descriptor.isEnabled()) {
+ mEnabled = descriptor.isEnabled();
+ changes |= CHANGE_GENERAL;
+ }
+ if (mConnecting != descriptor.isConnecting()) {
+ mConnecting = descriptor.isConnecting();
+ changes |= CHANGE_GENERAL;
+ }
+ if (mConnectionState != descriptor.getConnectionState()) {
+ mConnectionState = descriptor.getConnectionState();
+ changes |= CHANGE_GENERAL;
+ }
+ if (!mControlFilters.equals(descriptor.getControlFilters())) {
+ mControlFilters.clear();
+ mControlFilters.addAll(descriptor.getControlFilters());
+ changes |= CHANGE_GENERAL;
+ }
+ if (mPlaybackType != descriptor.getPlaybackType()) {
+ mPlaybackType = descriptor.getPlaybackType();
+ changes |= CHANGE_GENERAL;
+ }
+ if (mPlaybackStream != descriptor.getPlaybackStream()) {
+ mPlaybackStream = descriptor.getPlaybackStream();
+ changes |= CHANGE_GENERAL;
+ }
+ if (mDeviceType != descriptor.getDeviceType()) {
+ mDeviceType = descriptor.getDeviceType();
+ changes |= CHANGE_GENERAL;
+ }
+ if (mVolumeHandling != descriptor.getVolumeHandling()) {
+ mVolumeHandling = descriptor.getVolumeHandling();
+ changes |= CHANGE_GENERAL | CHANGE_VOLUME;
+ }
+ if (mVolume != descriptor.getVolume()) {
+ mVolume = descriptor.getVolume();
+ changes |= CHANGE_GENERAL | CHANGE_VOLUME;
+ }
+ if (mVolumeMax != descriptor.getVolumeMax()) {
+ mVolumeMax = descriptor.getVolumeMax();
+ changes |= CHANGE_GENERAL | CHANGE_VOLUME;
+ }
+ if (mPresentationDisplayId != descriptor.getPresentationDisplayId()) {
+ mPresentationDisplayId = descriptor.getPresentationDisplayId();
+ mPresentationDisplay = null;
+ changes |= CHANGE_GENERAL | CHANGE_PRESENTATION_DISPLAY;
+ }
+ if (!equal(mExtras, descriptor.getExtras())) {
+ mExtras = descriptor.getExtras();
+ changes |= CHANGE_GENERAL;
+ }
+ if (!equal(mSettingsIntent, descriptor.getSettingsActivity())) {
+ mSettingsIntent = descriptor.getSettingsActivity();
+ changes |= CHANGE_GENERAL;
+ }
+ if (mCanDisconnect != descriptor.canDisconnectAndKeepPlaying()) {
+ mCanDisconnect = descriptor.canDisconnectAndKeepPlaying();
+ changes |= CHANGE_GENERAL | CHANGE_PRESENTATION_DISPLAY;
+ }
+ }
+ return changes;
+ }
+
+ String getDescriptorId() {
+ return mDescriptorId;
+ }
+
+ /** @hide */
+ // @RestrictTo(LIBRARY_GROUP)
+ public MediaRouteProvider getProviderInstance() {
+ return mProvider.getProviderInstance();
+ }
+ }
+
+ /**
+ * Information about a route that consists of multiple other routes in a group.
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public static class RouteGroup extends RouteInfo {
+ private List<RouteInfo> mRoutes = new ArrayList<>();
+
+ RouteGroup(ProviderInfo provider, String descriptorId, String uniqueId) {
+ super(provider, descriptorId, uniqueId);
+ }
+
+ /**
+ * @return The number of routes in this group
+ */
+ public int getRouteCount() {
+ return mRoutes.size();
+ }
+
+ /**
+ * Returns the route in this group at the specified index
+ *
+ * @param index Index to fetch
+ * @return The route at index
+ */
+ public RouteInfo getRouteAt(int index) {
+ return mRoutes.get(index);
+ }
+
+ /**
+ * Returns the routes in this group
+ *
+ * @return The list of the routes in this group
+ */
+ public List<RouteInfo> getRoutes() {
+ return mRoutes;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(super.toString());
+ sb.append('[');
+ final int count = mRoutes.size();
+ for (int i = 0; i < count; i++) {
+ if (i > 0) sb.append(", ");
+ sb.append(mRoutes.get(i));
+ }
+ sb.append(']');
+ return sb.toString();
+ }
+
+ @Override
+ int maybeUpdateDescriptor(MediaRouteDescriptor descriptor) {
+ boolean changed = false;
+ if (mDescriptor != descriptor) {
+ mDescriptor = descriptor;
+ if (descriptor != null) {
+ List<String> groupMemberIds = descriptor.getGroupMemberIds();
+ List<RouteInfo> routes = new ArrayList<>();
+ changed = groupMemberIds.size() != mRoutes.size();
+ for (String groupMemberId : groupMemberIds) {
+ String uniqueId = sGlobal.getUniqueId(getProvider(), groupMemberId);
+ RouteInfo groupMember = sGlobal.getRoute(uniqueId);
+ if (groupMember != null) {
+ routes.add(groupMember);
+ if (!changed && !mRoutes.contains(groupMember)) {
+ changed = true;
+ }
+ }
+ }
+ if (changed) {
+ mRoutes = routes;
+ }
+ }
+ }
+ return (changed ? CHANGE_GENERAL : 0) | super.updateDescriptor(descriptor);
+ }
+ }
+
+ /**
+ * Provides information about a media route provider.
+ * <p>
+ * This object may be used to determine which media route provider has
+ * published a particular route.
+ * </p>
+ */
+ public static final class ProviderInfo {
+ private final MediaRouteProvider mProviderInstance;
+ private final List<RouteInfo> mRoutes = new ArrayList<>();
+
+ private final ProviderMetadata mMetadata;
+ private MediaRouteProviderDescriptor mDescriptor;
+ private Resources mResources;
+ private boolean mResourcesNotAvailable;
+
+ ProviderInfo(MediaRouteProvider provider) {
+ mProviderInstance = provider;
+ mMetadata = provider.getMetadata();
+ }
+
+ /**
+ * Gets the provider's underlying {@link MediaRouteProvider} instance.
+ */
+ public MediaRouteProvider getProviderInstance() {
+ checkCallingThread();
+ return mProviderInstance;
+ }
+
+ /**
+ * Gets the package name of the media route provider.
+ */
+ public String getPackageName() {
+ return mMetadata.getPackageName();
+ }
+
+ /**
+ * Gets the component name of the media route provider.
+ */
+ public ComponentName getComponentName() {
+ return mMetadata.getComponentName();
+ }
+
+ /**
+ * Gets the {@link MediaRouter.RouteInfo routes} published by this route provider.
+ */
+ public List<RouteInfo> getRoutes() {
+ checkCallingThread();
+ return mRoutes;
+ }
+
+ Resources getResources() {
+ if (mResources == null && !mResourcesNotAvailable) {
+ String packageName = getPackageName();
+ Context context = sGlobal.getProviderContext(packageName);
+ if (context != null) {
+ mResources = context.getResources();
+ } else {
+ Log.w(TAG, "Unable to obtain resources for route provider package: "
+ + packageName);
+ mResourcesNotAvailable = true;
+ }
+ }
+ return mResources;
+ }
+
+ boolean updateDescriptor(MediaRouteProviderDescriptor descriptor) {
+ if (mDescriptor != descriptor) {
+ mDescriptor = descriptor;
+ return true;
+ }
+ return false;
+ }
+
+ int findRouteByDescriptorId(String id) {
+ final int count = mRoutes.size();
+ for (int i = 0; i < count; i++) {
+ if (mRoutes.get(i).mDescriptorId.equals(id)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public String toString() {
+ return "MediaRouter.RouteProviderInfo{ packageName=" + getPackageName()
+ + " }";
+ }
+ }
+
+ /**
+ * Interface for receiving events about media routing changes.
+ * All methods of this interface will be called from the application's main thread.
+ * <p>
+ * A Callback will only receive events relevant to routes that the callback
+ * was registered for unless the {@link MediaRouter#CALLBACK_FLAG_UNFILTERED_EVENTS}
+ * flag was specified in {@link MediaRouter#addCallback(MediaRouteSelector, Callback, int)}.
+ * </p>
+ *
+ * @see MediaRouter#addCallback(MediaRouteSelector, Callback, int)
+ * @see MediaRouter#removeCallback(Callback)
+ */
+ public static abstract class Callback {
+ /**
+ * Called when the supplied media route becomes selected as the active route.
+ *
+ * @param router The media router reporting the event.
+ * @param route The route that has been selected.
+ */
+ public void onRouteSelected(MediaRouter router, RouteInfo route) {
+ }
+
+ /**
+ * Called when the supplied media route becomes unselected as the active route.
+ * For detailed reason, override {@link #onRouteUnselected(MediaRouter, RouteInfo, int)}
+ * instead.
+ *
+ * @param router The media router reporting the event.
+ * @param route The route that has been unselected.
+ */
+ public void onRouteUnselected(MediaRouter router, RouteInfo route) {
+ }
+
+ /**
+ * Called when the supplied media route becomes unselected as the active route.
+ * The default implementation calls {@link #onRouteUnselected}.
+ * <p>
+ * The reason provided will be one of the following:
+ * <ul>
+ * <li>{@link MediaRouter#UNSELECT_REASON_UNKNOWN}</li>
+ * <li>{@link MediaRouter#UNSELECT_REASON_DISCONNECTED}</li>
+ * <li>{@link MediaRouter#UNSELECT_REASON_STOPPED}</li>
+ * <li>{@link MediaRouter#UNSELECT_REASON_ROUTE_CHANGED}</li>
+ * </ul>
+ *
+ * @param router The media router reporting the event.
+ * @param route The route that has been unselected.
+ * @param reason The reason for unselecting the route.
+ */
+ public void onRouteUnselected(MediaRouter router, RouteInfo route, int reason) {
+ onRouteUnselected(router, route);
+ }
+
+ /**
+ * Called when a media route has been added.
+ *
+ * @param router The media router reporting the event.
+ * @param route The route that has become available for use.
+ */
+ public void onRouteAdded(MediaRouter router, RouteInfo route) {
+ }
+
+ /**
+ * Called when a media route has been removed.
+ *
+ * @param router The media router reporting the event.
+ * @param route The route that has been removed from availability.
+ */
+ public void onRouteRemoved(MediaRouter router, RouteInfo route) {
+ }
+
+ /**
+ * Called when a property of the indicated media route has changed.
+ *
+ * @param router The media router reporting the event.
+ * @param route The route that was changed.
+ */
+ public void onRouteChanged(MediaRouter router, RouteInfo route) {
+ }
+
+ /**
+ * Called when a media route's volume changes.
+ *
+ * @param router The media router reporting the event.
+ * @param route The route whose volume changed.
+ */
+ public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) {
+ }
+
+ /**
+ * Called when a media route's presentation display changes.
+ * <p>
+ * This method is called whenever the route's presentation display becomes
+ * available, is removed or has changes to some of its properties (such as its size).
+ * </p>
+ *
+ * @param router The media router reporting the event.
+ * @param route The route whose presentation display changed.
+ *
+ * @see RouteInfo#getPresentationDisplay()
+ */
+ public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo route) {
+ }
+
+ /**
+ * Called when a media route provider has been added.
+ *
+ * @param router The media router reporting the event.
+ * @param provider The provider that has become available for use.
+ */
+ public void onProviderAdded(MediaRouter router, ProviderInfo provider) {
+ }
+
+ /**
+ * Called when a media route provider has been removed.
+ *
+ * @param router The media router reporting the event.
+ * @param provider The provider that has been removed from availability.
+ */
+ public void onProviderRemoved(MediaRouter router, ProviderInfo provider) {
+ }
+
+ /**
+ * Called when a property of the indicated media route provider has changed.
+ *
+ * @param router The media router reporting the event.
+ * @param provider The provider that was changed.
+ */
+ public void onProviderChanged(MediaRouter router, ProviderInfo provider) {
+ }
+ }
+
+ /**
+ * Callback which is invoked with the result of a media control request.
+ *
+ * @see RouteInfo#sendControlRequest
+ */
+ public static abstract class ControlRequestCallback {
+ /**
+ * Called when a media control request succeeds.
+ *
+ * @param data Result data, or null if none.
+ * Contents depend on the {@link MediaControlIntent media control action}.
+ */
+ public void onResult(Bundle data) {
+ }
+
+ /**
+ * Called when a media control request fails.
+ *
+ * @param error A localized error message which may be shown to the user, or null
+ * if the cause of the error is unclear.
+ * @param data Error data, or null if none.
+ * Contents depend on the {@link MediaControlIntent media control action}.
+ */
+ public void onError(String error, Bundle data) {
+ }
+ }
+
+ private static final class CallbackRecord {
+ public final MediaRouter mRouter;
+ public final Callback mCallback;
+ public MediaRouteSelector mSelector;
+ public int mFlags;
+
+ public CallbackRecord(MediaRouter router, Callback callback) {
+ mRouter = router;
+ mCallback = callback;
+ mSelector = MediaRouteSelector.EMPTY;
+ }
+
+ public boolean filterRouteEvent(RouteInfo route) {
+ return (mFlags & CALLBACK_FLAG_UNFILTERED_EVENTS) != 0
+ || route.matchesSelector(mSelector);
+ }
+ }
+
+ /**
+ * Global state for the media router.
+ * <p>
+ * Media routes and media route providers are global to the process; their
+ * state and the bulk of the media router implementation lives here.
+ * </p>
+ */
+ private static final class GlobalMediaRouter
+ implements SystemMediaRouteProvider.SyncCallback,
+ RegisteredMediaRouteProviderWatcher.Callback {
+ final Context mApplicationContext;
+ final ArrayList<WeakReference<MediaRouter>> mRouters = new ArrayList<>();
+ private final ArrayList<RouteInfo> mRoutes = new ArrayList<>();
+ private final Map<Pair<String, String>, String> mUniqueIdMap = new HashMap<>();
+ private final ArrayList<ProviderInfo> mProviders = new ArrayList<>();
+ private final ArrayList<RemoteControlClientRecord> mRemoteControlClients =
+ new ArrayList<>();
+ final RemoteControlClientCompat.PlaybackInfo mPlaybackInfo =
+ new RemoteControlClientCompat.PlaybackInfo();
+ private final ProviderCallback mProviderCallback = new ProviderCallback();
+ final CallbackHandler mCallbackHandler = new CallbackHandler();
+ private final DisplayManagerCompat mDisplayManager;
+ final SystemMediaRouteProvider mSystemProvider;
+ private final boolean mLowRam;
+
+ private RegisteredMediaRouteProviderWatcher mRegisteredProviderWatcher;
+ private RouteInfo mDefaultRoute;
+ private RouteInfo mBluetoothRoute;
+ RouteInfo mSelectedRoute;
+ private RouteController mSelectedRouteController;
+ // A map from route descriptor ID to RouteController for the member routes in the currently
+ // selected route group.
+ private final Map<String, RouteController> mRouteControllerMap = new HashMap<>();
+ private MediaRouteDiscoveryRequest mDiscoveryRequest;
+ private MediaSessionRecord mMediaSession;
+ MediaSessionCompat mRccMediaSession;
+ private MediaSessionCompat mCompatSession;
+ private MediaSessionCompat.OnActiveChangeListener mSessionActiveListener =
+ new MediaSessionCompat.OnActiveChangeListener() {
+ @Override
+ public void onActiveChanged() {
+ if(mRccMediaSession != null) {
+ if (mRccMediaSession.isActive()) {
+ addRemoteControlClient(mRccMediaSession.getRemoteControlClient());
+ } else {
+ removeRemoteControlClient(mRccMediaSession.getRemoteControlClient());
+ }
+ }
+ }
+ };
+
+ GlobalMediaRouter(Context applicationContext) {
+ mApplicationContext = applicationContext;
+ mDisplayManager = DisplayManagerCompat.getInstance(applicationContext);
+ mLowRam = ActivityManagerCompat.isLowRamDevice(
+ (ActivityManager)applicationContext.getSystemService(
+ Context.ACTIVITY_SERVICE));
+
+ // Add the system media route provider for interoperating with
+ // the framework media router. This one is special and receives
+ // synchronization messages from the media router.
+ mSystemProvider = SystemMediaRouteProvider.obtain(applicationContext, this);
+ }
+
+ public void start() {
+ addProvider(mSystemProvider);
+
+ // Start watching for routes published by registered media route
+ // provider services.
+ mRegisteredProviderWatcher = new RegisteredMediaRouteProviderWatcher(
+ mApplicationContext, this);
+ mRegisteredProviderWatcher.start();
+ }
+
+ public MediaRouter getRouter(Context context) {
+ MediaRouter router;
+ for (int i = mRouters.size(); --i >= 0; ) {
+ router = mRouters.get(i).get();
+ if (router == null) {
+ mRouters.remove(i);
+ } else if (router.mContext == context) {
+ return router;
+ }
+ }
+ router = new MediaRouter(context);
+ mRouters.add(new WeakReference<MediaRouter>(router));
+ return router;
+ }
+
+ public ContentResolver getContentResolver() {
+ return mApplicationContext.getContentResolver();
+ }
+
+ public Context getProviderContext(String packageName) {
+ if (packageName.equals(SystemMediaRouteProvider.PACKAGE_NAME)) {
+ return mApplicationContext;
+ }
+ try {
+ return mApplicationContext.createPackageContext(
+ packageName, Context.CONTEXT_RESTRICTED);
+ } catch (NameNotFoundException ex) {
+ return null;
+ }
+ }
+
+ public Display getDisplay(int displayId) {
+ return mDisplayManager.getDisplay(displayId);
+ }
+
+ public void sendControlRequest(RouteInfo route,
+ Intent intent, ControlRequestCallback callback) {
+ if (route == mSelectedRoute && mSelectedRouteController != null) {
+ if (mSelectedRouteController.onControlRequest(intent, callback)) {
+ return;
+ }
+ }
+ if (callback != null) {
+ callback.onError(null, null);
+ }
+ }
+
+ public void requestSetVolume(RouteInfo route, int volume) {
+ if (route == mSelectedRoute && mSelectedRouteController != null) {
+ mSelectedRouteController.onSetVolume(volume);
+ } else if (!mRouteControllerMap.isEmpty()) {
+ RouteController controller = mRouteControllerMap.get(route.mDescriptorId);
+ if (controller != null) {
+ controller.onSetVolume(volume);
+ }
+ }
+ }
+
+ public void requestUpdateVolume(RouteInfo route, int delta) {
+ if (route == mSelectedRoute && mSelectedRouteController != null) {
+ mSelectedRouteController.onUpdateVolume(delta);
+ }
+ }
+
+ public RouteInfo getRoute(String uniqueId) {
+ for (RouteInfo info : mRoutes) {
+ if (info.mUniqueId.equals(uniqueId)) {
+ return info;
+ }
+ }
+ return null;
+ }
+
+ public List<RouteInfo> getRoutes() {
+ return mRoutes;
+ }
+
+ List<ProviderInfo> getProviders() {
+ return mProviders;
+ }
+
+ @NonNull RouteInfo getDefaultRoute() {
+ if (mDefaultRoute == null) {
+ // This should never happen once the media router has been fully
+ // initialized but it is good to check for the error in case there
+ // is a bug in provider initialization.
+ throw new IllegalStateException("There is no default route. "
+ + "The media router has not yet been fully initialized.");
+ }
+ return mDefaultRoute;
+ }
+
+ RouteInfo getBluetoothRoute() {
+ return mBluetoothRoute;
+ }
+
+ @NonNull RouteInfo getSelectedRoute() {
+ if (mSelectedRoute == null) {
+ // This should never happen once the media router has been fully
+ // initialized but it is good to check for the error in case there
+ // is a bug in provider initialization.
+ throw new IllegalStateException("There is no currently selected route. "
+ + "The media router has not yet been fully initialized.");
+ }
+ return mSelectedRoute;
+ }
+
+ void selectRoute(@NonNull RouteInfo route) {
+ selectRoute(route, MediaRouter.UNSELECT_REASON_ROUTE_CHANGED);
+ }
+
+ void selectRoute(@NonNull RouteInfo route, int unselectReason) {
+ if (!mRoutes.contains(route)) {
+ Log.w(TAG, "Ignoring attempt to select removed route: " + route);
+ return;
+ }
+ if (!route.mEnabled) {
+ Log.w(TAG, "Ignoring attempt to select disabled route: " + route);
+ return;
+ }
+ setSelectedRouteInternal(route, unselectReason);
+ }
+
+ public boolean isRouteAvailable(MediaRouteSelector selector, int flags) {
+ if (selector.isEmpty()) {
+ return false;
+ }
+
+ // On low-RAM devices, do not rely on actual discovery results unless asked to.
+ if ((flags & AVAILABILITY_FLAG_REQUIRE_MATCH) == 0 && mLowRam) {
+ return true;
+ }
+
+ // Check whether any existing routes match the selector.
+ final int routeCount = mRoutes.size();
+ for (int i = 0; i < routeCount; i++) {
+ RouteInfo route = mRoutes.get(i);
+ if ((flags & AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE) != 0
+ && route.isDefaultOrBluetooth()) {
+ continue;
+ }
+ if (route.matchesSelector(selector)) {
+ return true;
+ }
+ }
+
+ // It doesn't look like we can find a matching route right now.
+ return false;
+ }
+
+ public void updateDiscoveryRequest() {
+ // Combine all of the callback selectors and active scan flags.
+ boolean discover = false;
+ boolean activeScan = false;
+ MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
+ for (int i = mRouters.size(); --i >= 0; ) {
+ MediaRouter router = mRouters.get(i).get();
+ if (router == null) {
+ mRouters.remove(i);
+ } else {
+ final int count = router.mCallbackRecords.size();
+ for (int j = 0; j < count; j++) {
+ CallbackRecord callback = router.mCallbackRecords.get(j);
+ builder.addSelector(callback.mSelector);
+ if ((callback.mFlags & CALLBACK_FLAG_PERFORM_ACTIVE_SCAN) != 0) {
+ activeScan = true;
+ discover = true; // perform active scan implies request discovery
+ }
+ if ((callback.mFlags & CALLBACK_FLAG_REQUEST_DISCOVERY) != 0) {
+ if (!mLowRam) {
+ discover = true;
+ }
+ }
+ if ((callback.mFlags & CALLBACK_FLAG_FORCE_DISCOVERY) != 0) {
+ discover = true;
+ }
+ }
+ }
+ }
+ MediaRouteSelector selector = discover ? builder.build() : MediaRouteSelector.EMPTY;
+
+ // Create a new discovery request.
+ if (mDiscoveryRequest != null
+ && mDiscoveryRequest.getSelector().equals(selector)
+ && mDiscoveryRequest.isActiveScan() == activeScan) {
+ return; // no change
+ }
+ if (selector.isEmpty() && !activeScan) {
+ // Discovery is not needed.
+ if (mDiscoveryRequest == null) {
+ return; // no change
+ }
+ mDiscoveryRequest = null;
+ } else {
+ // Discovery is needed.
+ mDiscoveryRequest = new MediaRouteDiscoveryRequest(selector, activeScan);
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Updated discovery request: " + mDiscoveryRequest);
+ }
+ if (discover && !activeScan && mLowRam) {
+ Log.i(TAG, "Forcing passive route discovery on a low-RAM device, "
+ + "system performance may be affected. Please consider using "
+ + "CALLBACK_FLAG_REQUEST_DISCOVERY instead of "
+ + "CALLBACK_FLAG_FORCE_DISCOVERY.");
+ }
+
+ // Notify providers.
+ final int providerCount = mProviders.size();
+ for (int i = 0; i < providerCount; i++) {
+ mProviders.get(i).mProviderInstance.setDiscoveryRequest(mDiscoveryRequest);
+ }
+ }
+
+ @Override
+ public void addProvider(MediaRouteProvider providerInstance) {
+ int index = findProviderInfo(providerInstance);
+ if (index < 0) {
+ // 1. Add the provider to the list.
+ ProviderInfo provider = new ProviderInfo(providerInstance);
+ mProviders.add(provider);
+ if (DEBUG) {
+ Log.d(TAG, "Provider added: " + provider);
+ }
+ mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_ADDED, provider);
+ // 2. Create the provider's contents.
+ updateProviderContents(provider, providerInstance.getDescriptor());
+ // 3. Register the provider callback.
+ providerInstance.setCallback(mProviderCallback);
+ // 4. Set the discovery request.
+ providerInstance.setDiscoveryRequest(mDiscoveryRequest);
+ }
+ }
+
+ @Override
+ public void removeProvider(MediaRouteProvider providerInstance) {
+ int index = findProviderInfo(providerInstance);
+ if (index >= 0) {
+ // 1. Unregister the provider callback.
+ providerInstance.setCallback(null);
+ // 2. Clear the discovery request.
+ providerInstance.setDiscoveryRequest(null);
+ // 3. Delete the provider's contents.
+ ProviderInfo provider = mProviders.get(index);
+ updateProviderContents(provider, null);
+ // 4. Remove the provider from the list.
+ if (DEBUG) {
+ Log.d(TAG, "Provider removed: " + provider);
+ }
+ mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_REMOVED, provider);
+ mProviders.remove(index);
+ }
+ }
+
+ void updateProviderDescriptor(MediaRouteProvider providerInstance,
+ MediaRouteProviderDescriptor descriptor) {
+ int index = findProviderInfo(providerInstance);
+ if (index >= 0) {
+ // Update the provider's contents.
+ ProviderInfo provider = mProviders.get(index);
+ updateProviderContents(provider, descriptor);
+ }
+ }
+
+ private int findProviderInfo(MediaRouteProvider providerInstance) {
+ final int count = mProviders.size();
+ for (int i = 0; i < count; i++) {
+ if (mProviders.get(i).mProviderInstance == providerInstance) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private void updateProviderContents(ProviderInfo provider,
+ MediaRouteProviderDescriptor providerDescriptor) {
+ if (provider.updateDescriptor(providerDescriptor)) {
+ // Update all existing routes and reorder them to match
+ // the order of their descriptors.
+ int targetIndex = 0;
+ boolean selectedRouteDescriptorChanged = false;
+ if (providerDescriptor != null) {
+ if (providerDescriptor.isValid()) {
+ final List<MediaRouteDescriptor> routeDescriptors =
+ providerDescriptor.getRoutes();
+ final int routeCount = routeDescriptors.size();
+ // Updating route group's contents requires all member routes' information.
+ // Add the groups to the lists and update them later.
+ List<Pair<RouteInfo, MediaRouteDescriptor>> addedGroups = new ArrayList<>();
+ List<Pair<RouteInfo, MediaRouteDescriptor>> updatedGroups =
+ new ArrayList<>();
+ for (int i = 0; i < routeCount; i++) {
+ final MediaRouteDescriptor routeDescriptor = routeDescriptors.get(i);
+ final String id = routeDescriptor.getId();
+ final int sourceIndex = provider.findRouteByDescriptorId(id);
+ if (sourceIndex < 0) {
+ // 1. Add the route to the list.
+ String uniqueId = assignRouteUniqueId(provider, id);
+ boolean isGroup = routeDescriptor.getGroupMemberIds() != null;
+ RouteInfo route = isGroup ? new RouteGroup(provider, id, uniqueId) :
+ new RouteInfo(provider, id, uniqueId);
+ provider.mRoutes.add(targetIndex++, route);
+ mRoutes.add(route);
+ // 2. Create the route's contents.
+ if (isGroup) {
+ addedGroups.add(new Pair<>(route, routeDescriptor));
+ } else {
+ route.maybeUpdateDescriptor(routeDescriptor);
+ // 3. Notify clients about addition.
+ if (DEBUG) {
+ Log.d(TAG, "Route added: " + route);
+ }
+ mCallbackHandler.post(CallbackHandler.MSG_ROUTE_ADDED, route);
+ }
+
+ } else if (sourceIndex < targetIndex) {
+ Log.w(TAG, "Ignoring route descriptor with duplicate id: "
+ + routeDescriptor);
+ } else {
+ // 1. Reorder the route within the list.
+ RouteInfo route = provider.mRoutes.get(sourceIndex);
+ Collections.swap(provider.mRoutes,
+ sourceIndex, targetIndex++);
+ // 2. Update the route's contents.
+ if (route instanceof RouteGroup) {
+ updatedGroups.add(new Pair<>(route, routeDescriptor));
+ } else {
+ // 3. Notify clients about changes.
+ if (updateRouteDescriptorAndNotify(route, routeDescriptor)
+ != 0) {
+ if (route == mSelectedRoute) {
+ selectedRouteDescriptorChanged = true;
+ }
+ }
+ }
+ }
+ }
+ // Update the new and/or existing groups.
+ for (Pair<RouteInfo, MediaRouteDescriptor> pair : addedGroups) {
+ RouteInfo route = pair.first;
+ route.maybeUpdateDescriptor(pair.second);
+ if (DEBUG) {
+ Log.d(TAG, "Route added: " + route);
+ }
+ mCallbackHandler.post(CallbackHandler.MSG_ROUTE_ADDED, route);
+ }
+ for (Pair<RouteInfo, MediaRouteDescriptor> pair : updatedGroups) {
+ RouteInfo route = pair.first;
+ if (updateRouteDescriptorAndNotify(route, pair.second) != 0) {
+ if (route == mSelectedRoute) {
+ selectedRouteDescriptorChanged = true;
+ }
+ }
+ }
+ } else {
+ Log.w(TAG, "Ignoring invalid provider descriptor: " + providerDescriptor);
+ }
+ }
+
+ // Dispose all remaining routes that do not have matching descriptors.
+ for (int i = provider.mRoutes.size() - 1; i >= targetIndex; i--) {
+ // 1. Delete the route's contents.
+ RouteInfo route = provider.mRoutes.get(i);
+ route.maybeUpdateDescriptor(null);
+ // 2. Remove the route from the list.
+ mRoutes.remove(route);
+ }
+
+ // Update the selected route if needed.
+ updateSelectedRouteIfNeeded(selectedRouteDescriptorChanged);
+
+ // Now notify clients about routes that were removed.
+ // We do this after updating the selected route to ensure
+ // that the framework media router observes the new route
+ // selection before the removal since removing the currently
+ // selected route may have side-effects.
+ for (int i = provider.mRoutes.size() - 1; i >= targetIndex; i--) {
+ RouteInfo route = provider.mRoutes.remove(i);
+ if (DEBUG) {
+ Log.d(TAG, "Route removed: " + route);
+ }
+ mCallbackHandler.post(CallbackHandler.MSG_ROUTE_REMOVED, route);
+ }
+
+ // Notify provider changed.
+ if (DEBUG) {
+ Log.d(TAG, "Provider changed: " + provider);
+ }
+ mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_CHANGED, provider);
+ }
+ }
+
+ private int updateRouteDescriptorAndNotify(RouteInfo route,
+ MediaRouteDescriptor routeDescriptor) {
+ int changes = route.maybeUpdateDescriptor(routeDescriptor);
+ if (changes != 0) {
+ if ((changes & RouteInfo.CHANGE_GENERAL) != 0) {
+ if (DEBUG) {
+ Log.d(TAG, "Route changed: " + route);
+ }
+ mCallbackHandler.post(
+ CallbackHandler.MSG_ROUTE_CHANGED, route);
+ }
+ if ((changes & RouteInfo.CHANGE_VOLUME) != 0) {
+ if (DEBUG) {
+ Log.d(TAG, "Route volume changed: " + route);
+ }
+ mCallbackHandler.post(
+ CallbackHandler.MSG_ROUTE_VOLUME_CHANGED, route);
+ }
+ if ((changes & RouteInfo.CHANGE_PRESENTATION_DISPLAY) != 0) {
+ if (DEBUG) {
+ Log.d(TAG, "Route presentation display changed: "
+ + route);
+ }
+ mCallbackHandler.post(CallbackHandler.
+ MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED, route);
+ }
+ }
+ return changes;
+ }
+
+ private String assignRouteUniqueId(ProviderInfo provider, String routeDescriptorId) {
+ // Although route descriptor ids are unique within a provider, it's
+ // possible for there to be two providers with the same package name.
+ // Therefore we must dedupe the composite id.
+ String componentName = provider.getComponentName().flattenToShortString();
+ String uniqueId = componentName + ":" + routeDescriptorId;
+ if (findRouteByUniqueId(uniqueId) < 0) {
+ mUniqueIdMap.put(new Pair<>(componentName, routeDescriptorId), uniqueId);
+ return uniqueId;
+ }
+ Log.w(TAG, "Either " + routeDescriptorId + " isn't unique in " + componentName
+ + " or we're trying to assign a unique ID for an already added route");
+ for (int i = 2; ; i++) {
+ String newUniqueId = String.format(Locale.US, "%s_%d", uniqueId, i);
+ if (findRouteByUniqueId(newUniqueId) < 0) {
+ mUniqueIdMap.put(new Pair<>(componentName, routeDescriptorId), newUniqueId);
+ return newUniqueId;
+ }
+ }
+ }
+
+ private int findRouteByUniqueId(String uniqueId) {
+ final int count = mRoutes.size();
+ for (int i = 0; i < count; i++) {
+ if (mRoutes.get(i).mUniqueId.equals(uniqueId)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private String getUniqueId(ProviderInfo provider, String routeDescriptorId) {
+ String componentName = provider.getComponentName().flattenToShortString();
+ return mUniqueIdMap.get(new Pair<>(componentName, routeDescriptorId));
+ }
+
+ private void updateSelectedRouteIfNeeded(boolean selectedRouteDescriptorChanged) {
+ // Update default route.
+ if (mDefaultRoute != null && !mDefaultRoute.isSelectable()) {
+ Log.i(TAG, "Clearing the default route because it "
+ + "is no longer selectable: " + mDefaultRoute);
+ mDefaultRoute = null;
+ }
+ if (mDefaultRoute == null && !mRoutes.isEmpty()) {
+ for (RouteInfo route : mRoutes) {
+ if (isSystemDefaultRoute(route) && route.isSelectable()) {
+ mDefaultRoute = route;
+ Log.i(TAG, "Found default route: " + mDefaultRoute);
+ break;
+ }
+ }
+ }
+
+ // Update bluetooth route.
+ if (mBluetoothRoute != null && !mBluetoothRoute.isSelectable()) {
+ Log.i(TAG, "Clearing the bluetooth route because it "
+ + "is no longer selectable: " + mBluetoothRoute);
+ mBluetoothRoute = null;
+ }
+ if (mBluetoothRoute == null && !mRoutes.isEmpty()) {
+ for (RouteInfo route : mRoutes) {
+ if (isSystemLiveAudioOnlyRoute(route) && route.isSelectable()) {
+ mBluetoothRoute = route;
+ Log.i(TAG, "Found bluetooth route: " + mBluetoothRoute);
+ break;
+ }
+ }
+ }
+
+ // Update selected route.
+ if (mSelectedRoute == null || !mSelectedRoute.isSelectable()) {
+ Log.i(TAG, "Unselecting the current route because it "
+ + "is no longer selectable: " + mSelectedRoute);
+ setSelectedRouteInternal(chooseFallbackRoute(),
+ MediaRouter.UNSELECT_REASON_UNKNOWN);
+ } else if (selectedRouteDescriptorChanged) {
+ // In case the selected route is a route group, select/unselect route controllers
+ // for the added/removed route members.
+ if (mSelectedRoute instanceof RouteGroup) {
+ List<RouteInfo> routes = ((RouteGroup) mSelectedRoute).getRoutes();
+ // Build a set of descriptor IDs for the new route group.
+ Set<String> idSet = new HashSet<>();
+ for (RouteInfo route : routes) {
+ idSet.add(route.mDescriptorId);
+ }
+ // Unselect route controllers for the removed routes.
+ Iterator<Map.Entry<String, RouteController>> iter =
+ mRouteControllerMap.entrySet().iterator();
+ while (iter.hasNext()) {
+ Map.Entry<String, RouteController> entry = iter.next();
+ if (!idSet.contains(entry.getKey())) {
+ RouteController controller = entry.getValue();
+ controller.onUnselect();
+ controller.onRelease();
+ iter.remove();
+ }
+ }
+ // Select route controllers for the added routes.
+ for (RouteInfo route : routes) {
+ if (!mRouteControllerMap.containsKey(route.mDescriptorId)) {
+ RouteController controller = route.getProviderInstance()
+ .onCreateRouteController(
+ route.mDescriptorId, mSelectedRoute.mDescriptorId);
+ controller.onSelect();
+ mRouteControllerMap.put(route.mDescriptorId, controller);
+ }
+ }
+ }
+ // Update the playback info because the properties of the route have changed.
+ updatePlaybackInfoFromSelectedRoute();
+ }
+ }
+
+ RouteInfo chooseFallbackRoute() {
+ // When the current route is removed or no longer selectable,
+ // we want to revert to a live audio route if there is
+ // one (usually Bluetooth A2DP). Failing that, use
+ // the default route.
+ for (RouteInfo route : mRoutes) {
+ if (route != mDefaultRoute
+ && isSystemLiveAudioOnlyRoute(route)
+ && route.isSelectable()) {
+ return route;
+ }
+ }
+ return mDefaultRoute;
+ }
+
+ private boolean isSystemLiveAudioOnlyRoute(RouteInfo route) {
+ return route.getProviderInstance() == mSystemProvider
+ && route.supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
+ && !route.supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
+ }
+
+ private boolean isSystemDefaultRoute(RouteInfo route) {
+ return route.getProviderInstance() == mSystemProvider
+ && route.mDescriptorId.equals(
+ SystemMediaRouteProvider.DEFAULT_ROUTE_ID);
+ }
+
+ private void setSelectedRouteInternal(@NonNull RouteInfo route, int unselectReason) {
+ // TODO: Remove the following logging when no longer needed.
+ if (sGlobal == null || (mBluetoothRoute != null && route.isDefault())) {
+ final StackTraceElement[] callStack = Thread.currentThread().getStackTrace();
+ StringBuilder sb = new StringBuilder();
+ // callStack[3] is the caller of this method.
+ for (int i = 3; i < callStack.length; i++) {
+ StackTraceElement caller = callStack[i];
+ sb.append(caller.getClassName())
+ .append(".")
+ .append(caller.getMethodName())
+ .append(":")
+ .append(caller.getLineNumber())
+ .append(" ");
+ }
+ if (sGlobal == null) {
+ Log.w(TAG, "setSelectedRouteInternal is called while sGlobal is null: pkgName="
+ + mApplicationContext.getPackageName() + ", callers=" + sb.toString());
+ } else {
+ Log.w(TAG, "Default route is selected while a BT route is available: pkgName="
+ + mApplicationContext.getPackageName() + ", callers=" + sb.toString());
+ }
+ }
+
+ if (mSelectedRoute != route) {
+ if (mSelectedRoute != null) {
+ if (DEBUG) {
+ Log.d(TAG, "Route unselected: " + mSelectedRoute + " reason: "
+ + unselectReason);
+ }
+ mCallbackHandler.post(CallbackHandler.MSG_ROUTE_UNSELECTED, mSelectedRoute,
+ unselectReason);
+ if (mSelectedRouteController != null) {
+ mSelectedRouteController.onUnselect(unselectReason);
+ mSelectedRouteController.onRelease();
+ mSelectedRouteController = null;
+ }
+ if (!mRouteControllerMap.isEmpty()) {
+ for (RouteController controller : mRouteControllerMap.values()) {
+ controller.onUnselect(unselectReason);
+ controller.onRelease();
+ }
+ mRouteControllerMap.clear();
+ }
+ }
+
+ mSelectedRoute = route;
+ mSelectedRouteController = route.getProviderInstance().onCreateRouteController(
+ route.mDescriptorId);
+ if (mSelectedRouteController != null) {
+ mSelectedRouteController.onSelect();
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Route selected: " + mSelectedRoute);
+ }
+ mCallbackHandler.post(CallbackHandler.MSG_ROUTE_SELECTED, mSelectedRoute);
+
+ if (mSelectedRoute instanceof RouteGroup) {
+ List<RouteInfo> routes = ((RouteGroup) mSelectedRoute).getRoutes();
+ mRouteControllerMap.clear();
+ for (RouteInfo r : routes) {
+ RouteController controller =
+ r.getProviderInstance().onCreateRouteController(
+ r.mDescriptorId, mSelectedRoute.mDescriptorId);
+ controller.onSelect();
+ mRouteControllerMap.put(r.mDescriptorId, controller);
+ }
+ }
+
+ updatePlaybackInfoFromSelectedRoute();
+ }
+ }
+
+ @Override
+ public void onSystemRouteSelectedByDescriptorId(String id) {
+ // System route is selected, do not sync the route we selected before.
+ mCallbackHandler.removeMessages(CallbackHandler.MSG_ROUTE_SELECTED);
+ int providerIndex = findProviderInfo(mSystemProvider);
+ if (providerIndex >= 0) {
+ ProviderInfo provider = mProviders.get(providerIndex);
+ int routeIndex = provider.findRouteByDescriptorId(id);
+ if (routeIndex >= 0) {
+ provider.mRoutes.get(routeIndex).select();
+ }
+ }
+ }
+
+ public void addRemoteControlClient(Object rcc) {
+ int index = findRemoteControlClientRecord(rcc);
+ if (index < 0) {
+ RemoteControlClientRecord record = new RemoteControlClientRecord(rcc);
+ mRemoteControlClients.add(record);
+ }
+ }
+
+ public void removeRemoteControlClient(Object rcc) {
+ int index = findRemoteControlClientRecord(rcc);
+ if (index >= 0) {
+ RemoteControlClientRecord record = mRemoteControlClients.remove(index);
+ record.disconnect();
+ }
+ }
+
+ public void setMediaSession(Object session) {
+ setMediaSessionRecord(session != null ? new MediaSessionRecord(session) : null);
+ }
+
+ public void setMediaSessionCompat(final MediaSessionCompat session) {
+ mCompatSession = session;
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ setMediaSessionRecord(session != null ? new MediaSessionRecord(session) : null);
+ } else if (android.os.Build.VERSION.SDK_INT >= 14) {
+ if (mRccMediaSession != null) {
+ removeRemoteControlClient(mRccMediaSession.getRemoteControlClient());
+ mRccMediaSession.removeOnActiveChangeListener(mSessionActiveListener);
+ }
+ mRccMediaSession = session;
+ if (session != null) {
+ session.addOnActiveChangeListener(mSessionActiveListener);
+ if (session.isActive()) {
+ addRemoteControlClient(session.getRemoteControlClient());
+ }
+ }
+ }
+ }
+
+ private void setMediaSessionRecord(MediaSessionRecord mediaSessionRecord) {
+ if (mMediaSession != null) {
+ mMediaSession.clearVolumeHandling();
+ }
+ mMediaSession = mediaSessionRecord;
+ if (mediaSessionRecord != null) {
+ updatePlaybackInfoFromSelectedRoute();
+ }
+ }
+
+ public MediaSessionCompat.Token getMediaSessionToken() {
+ if (mMediaSession != null) {
+ return mMediaSession.getToken();
+ } else if (mCompatSession != null) {
+ return mCompatSession.getSessionToken();
+ }
+ return null;
+ }
+
+ private int findRemoteControlClientRecord(Object rcc) {
+ final int count = mRemoteControlClients.size();
+ for (int i = 0; i < count; i++) {
+ RemoteControlClientRecord record = mRemoteControlClients.get(i);
+ if (record.getRemoteControlClient() == rcc) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private void updatePlaybackInfoFromSelectedRoute() {
+ if (mSelectedRoute != null) {
+ mPlaybackInfo.volume = mSelectedRoute.getVolume();
+ mPlaybackInfo.volumeMax = mSelectedRoute.getVolumeMax();
+ mPlaybackInfo.volumeHandling = mSelectedRoute.getVolumeHandling();
+ mPlaybackInfo.playbackStream = mSelectedRoute.getPlaybackStream();
+ mPlaybackInfo.playbackType = mSelectedRoute.getPlaybackType();
+
+ final int count = mRemoteControlClients.size();
+ for (int i = 0; i < count; i++) {
+ RemoteControlClientRecord record = mRemoteControlClients.get(i);
+ record.updatePlaybackInfo();
+ }
+ if (mMediaSession != null) {
+ if (mSelectedRoute == getDefaultRoute()
+ || mSelectedRoute == getBluetoothRoute()) {
+ // Local route
+ mMediaSession.clearVolumeHandling();
+ } else {
+ @VolumeProviderCompat.ControlType int controlType =
+ VolumeProviderCompat.VOLUME_CONTROL_FIXED;
+ if (mPlaybackInfo.volumeHandling
+ == MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) {
+ controlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
+ }
+ mMediaSession.configureVolume(controlType, mPlaybackInfo.volumeMax,
+ mPlaybackInfo.volume);
+ }
+ }
+ } else {
+ if (mMediaSession != null) {
+ mMediaSession.clearVolumeHandling();
+ }
+ }
+ }
+
+ private final class ProviderCallback extends MediaRouteProvider.Callback {
+ ProviderCallback() {
+ }
+
+ @Override
+ public void onDescriptorChanged(MediaRouteProvider provider,
+ MediaRouteProviderDescriptor descriptor) {
+ updateProviderDescriptor(provider, descriptor);
+ }
+ }
+
+ private final class MediaSessionRecord {
+ private final MediaSessionCompat mMsCompat;
+
+ private @VolumeProviderCompat.ControlType int mControlType;
+ private int mMaxVolume;
+ private VolumeProviderCompat mVpCompat;
+
+ public MediaSessionRecord(Object mediaSession) {
+ mMsCompat = MediaSessionCompat.fromMediaSession(mApplicationContext, mediaSession);
+ }
+
+ public MediaSessionRecord(MediaSessionCompat mediaSessionCompat) {
+ mMsCompat = mediaSessionCompat;
+ }
+
+ public void configureVolume(@VolumeProviderCompat.ControlType int controlType,
+ int max, int current) {
+ if (mVpCompat != null && controlType == mControlType && max == mMaxVolume) {
+ // If we haven't changed control type or max just set the
+ // new current volume
+ mVpCompat.setCurrentVolume(current);
+ } else {
+ // Otherwise create a new provider and update
+ mVpCompat = new VolumeProviderCompat(controlType, max, current) {
+ @Override
+ public void onSetVolumeTo(final int volume) {
+ mCallbackHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mSelectedRoute != null) {
+ mSelectedRoute.requestSetVolume(volume);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onAdjustVolume(final int direction) {
+ mCallbackHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mSelectedRoute != null) {
+ mSelectedRoute.requestUpdateVolume(direction);
+ }
+ }
+ });
+ }
+ };
+ mMsCompat.setPlaybackToRemote(mVpCompat);
+ }
+ }
+
+ public void clearVolumeHandling() {
+ mMsCompat.setPlaybackToLocal(mPlaybackInfo.playbackStream);
+ mVpCompat = null;
+ }
+
+ public MediaSessionCompat.Token getToken() {
+ return mMsCompat.getSessionToken();
+ }
+ }
+
+ private final class RemoteControlClientRecord
+ implements RemoteControlClientCompat.VolumeCallback {
+ private final RemoteControlClientCompat mRccCompat;
+ private boolean mDisconnected;
+
+ public RemoteControlClientRecord(Object rcc) {
+ mRccCompat = RemoteControlClientCompat.obtain(mApplicationContext, rcc);
+ mRccCompat.setVolumeCallback(this);
+ updatePlaybackInfo();
+ }
+
+ public Object getRemoteControlClient() {
+ return mRccCompat.getRemoteControlClient();
+ }
+
+ public void disconnect() {
+ mDisconnected = true;
+ mRccCompat.setVolumeCallback(null);
+ }
+
+ public void updatePlaybackInfo() {
+ mRccCompat.setPlaybackInfo(mPlaybackInfo);
+ }
+
+ @Override
+ public void onVolumeSetRequest(int volume) {
+ if (!mDisconnected && mSelectedRoute != null) {
+ mSelectedRoute.requestSetVolume(volume);
+ }
+ }
+
+ @Override
+ public void onVolumeUpdateRequest(int direction) {
+ if (!mDisconnected && mSelectedRoute != null) {
+ mSelectedRoute.requestUpdateVolume(direction);
+ }
+ }
+ }
+
+ private final class CallbackHandler extends Handler {
+ private final ArrayList<CallbackRecord> mTempCallbackRecords =
+ new ArrayList<CallbackRecord>();
+
+ private static final int MSG_TYPE_MASK = 0xff00;
+ private static final int MSG_TYPE_ROUTE = 0x0100;
+ private static final int MSG_TYPE_PROVIDER = 0x0200;
+
+ public static final int MSG_ROUTE_ADDED = MSG_TYPE_ROUTE | 1;
+ public static final int MSG_ROUTE_REMOVED = MSG_TYPE_ROUTE | 2;
+ public static final int MSG_ROUTE_CHANGED = MSG_TYPE_ROUTE | 3;
+ public static final int MSG_ROUTE_VOLUME_CHANGED = MSG_TYPE_ROUTE | 4;
+ public static final int MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED = MSG_TYPE_ROUTE | 5;
+ public static final int MSG_ROUTE_SELECTED = MSG_TYPE_ROUTE | 6;
+ public static final int MSG_ROUTE_UNSELECTED = MSG_TYPE_ROUTE | 7;
+
+ public static final int MSG_PROVIDER_ADDED = MSG_TYPE_PROVIDER | 1;
+ public static final int MSG_PROVIDER_REMOVED = MSG_TYPE_PROVIDER | 2;
+ public static final int MSG_PROVIDER_CHANGED = MSG_TYPE_PROVIDER | 3;
+
+ CallbackHandler() {
+ }
+
+ public void post(int msg, Object obj) {
+ obtainMessage(msg, obj).sendToTarget();
+ }
+
+ public void post(int msg, Object obj, int arg) {
+ Message message = obtainMessage(msg, obj);
+ message.arg1 = arg;
+ message.sendToTarget();
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ final int what = msg.what;
+ final Object obj = msg.obj;
+ final int arg = msg.arg1;
+
+ if (what == MSG_ROUTE_CHANGED
+ && getSelectedRoute().getId().equals(((RouteInfo) obj).getId())) {
+ updateSelectedRouteIfNeeded(true);
+ }
+
+ // Synchronize state with the system media router.
+ syncWithSystemProvider(what, obj);
+
+ // Invoke all registered callbacks.
+ // Build a list of callbacks before invoking them in case callbacks
+ // are added or removed during dispatch.
+ try {
+ for (int i = mRouters.size(); --i >= 0; ) {
+ MediaRouter router = mRouters.get(i).get();
+ if (router == null) {
+ mRouters.remove(i);
+ } else {
+ mTempCallbackRecords.addAll(router.mCallbackRecords);
+ }
+ }
+
+ final int callbackCount = mTempCallbackRecords.size();
+ for (int i = 0; i < callbackCount; i++) {
+ invokeCallback(mTempCallbackRecords.get(i), what, obj, arg);
+ }
+ } finally {
+ mTempCallbackRecords.clear();
+ }
+ }
+
+ private void syncWithSystemProvider(int what, Object obj) {
+ switch (what) {
+ case MSG_ROUTE_ADDED:
+ mSystemProvider.onSyncRouteAdded((RouteInfo) obj);
+ break;
+ case MSG_ROUTE_REMOVED:
+ mSystemProvider.onSyncRouteRemoved((RouteInfo) obj);
+ break;
+ case MSG_ROUTE_CHANGED:
+ mSystemProvider.onSyncRouteChanged((RouteInfo) obj);
+ break;
+ case MSG_ROUTE_SELECTED:
+ mSystemProvider.onSyncRouteSelected((RouteInfo) obj);
+ break;
+ }
+ }
+
+ private void invokeCallback(CallbackRecord record, int what, Object obj, int arg) {
+ final MediaRouter router = record.mRouter;
+ final MediaRouter.Callback callback = record.mCallback;
+ switch (what & MSG_TYPE_MASK) {
+ case MSG_TYPE_ROUTE: {
+ final RouteInfo route = (RouteInfo)obj;
+ if (!record.filterRouteEvent(route)) {
+ break;
+ }
+ switch (what) {
+ case MSG_ROUTE_ADDED:
+ callback.onRouteAdded(router, route);
+ break;
+ case MSG_ROUTE_REMOVED:
+ callback.onRouteRemoved(router, route);
+ break;
+ case MSG_ROUTE_CHANGED:
+ callback.onRouteChanged(router, route);
+ break;
+ case MSG_ROUTE_VOLUME_CHANGED:
+ callback.onRouteVolumeChanged(router, route);
+ break;
+ case MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED:
+ callback.onRoutePresentationDisplayChanged(router, route);
+ break;
+ case MSG_ROUTE_SELECTED:
+ callback.onRouteSelected(router, route);
+ break;
+ case MSG_ROUTE_UNSELECTED:
+ callback.onRouteUnselected(router, route, arg);
+ break;
+ }
+ break;
+ }
+ case MSG_TYPE_PROVIDER: {
+ final ProviderInfo provider = (ProviderInfo)obj;
+ switch (what) {
+ case MSG_PROVIDER_ADDED:
+ callback.onProviderAdded(router, provider);
+ break;
+ case MSG_PROVIDER_REMOVED:
+ callback.onProviderRemoved(router, provider);
+ break;
+ case MSG_PROVIDER_CHANGED:
+ callback.onProviderChanged(router, provider);
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaSessionStatus.java b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaSessionStatus.java
new file mode 100644
index 0000000..3206596
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/media/MediaSessionStatus.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.media;
+
+import android.app.PendingIntent;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.util.TimeUtils;
+
+/**
+ * Describes the playback status of a media session.
+ * <p>
+ * This class is part of the remote playback protocol described by the
+ * {@link MediaControlIntent MediaControlIntent} class.
+ * </p><p>
+ * When a media session is created, it is initially in the
+ * {@link #SESSION_STATE_ACTIVE active} state. When the media session ends
+ * normally, it transitions to the {@link #SESSION_STATE_ENDED ended} state.
+ * If the media session is invalidated due to another session forcibly taking
+ * control of the route, then it transitions to the
+ * {@link #SESSION_STATE_INVALIDATED invalidated} state.
+ * Refer to the documentation of each state for an explanation of its meaning.
+ * </p><p>
+ * To monitor session status, the application should supply a {@link PendingIntent} to use as the
+ * {@link MediaControlIntent#EXTRA_SESSION_STATUS_UPDATE_RECEIVER session status update receiver}
+ * for a given {@link MediaControlIntent#ACTION_START_SESSION session start request}.
+ * </p><p>
+ * This object is immutable once created using a {@link Builder} instance.
+ * </p>
+ */
+public final class MediaSessionStatus {
+ static final String KEY_TIMESTAMP = "timestamp";
+ static final String KEY_SESSION_STATE = "sessionState";
+ static final String KEY_QUEUE_PAUSED = "queuePaused";
+ static final String KEY_EXTRAS = "extras";
+
+ final Bundle mBundle;
+
+ /**
+ * Session state: Active.
+ * <p>
+ * Indicates that the media session is active and in control of the route.
+ * </p>
+ */
+ public static final int SESSION_STATE_ACTIVE = 0;
+
+ /**
+ * Session state: Ended.
+ * <p>
+ * Indicates that the media session was ended normally using the
+ * {@link MediaControlIntent#ACTION_END_SESSION end session} action.
+ * </p><p>
+ * A terminated media session cannot be used anymore. To play more media, the
+ * application must start a new session.
+ * </p>
+ */
+ public static final int SESSION_STATE_ENDED = 1;
+
+ /**
+ * Session state: Invalidated.
+ * <p>
+ * Indicates that the media session was invalidated involuntarily due to
+ * another session taking control of the route.
+ * </p><p>
+ * An invalidated media session cannot be used anymore. To play more media, the
+ * application must start a new session.
+ * </p>
+ */
+ public static final int SESSION_STATE_INVALIDATED = 2;
+
+ MediaSessionStatus(Bundle bundle) {
+ mBundle = bundle;
+ }
+
+ /**
+ * Gets the timestamp associated with the status information in
+ * milliseconds since boot in the {@link SystemClock#elapsedRealtime} time base.
+ *
+ * @return The status timestamp in the {@link SystemClock#elapsedRealtime()} time base.
+ */
+ public long getTimestamp() {
+ return mBundle.getLong(KEY_TIMESTAMP);
+ }
+
+ /**
+ * Gets the session state.
+ *
+ * @return The session state. One of {@link #SESSION_STATE_ACTIVE},
+ * {@link #SESSION_STATE_ENDED}, or {@link #SESSION_STATE_INVALIDATED}.
+ */
+ public int getSessionState() {
+ return mBundle.getInt(KEY_SESSION_STATE, SESSION_STATE_INVALIDATED);
+ }
+
+ /**
+ * Returns true if the session's queue is paused.
+ *
+ * @return True if the session's queue is paused.
+ */
+ public boolean isQueuePaused() {
+ return mBundle.getBoolean(KEY_QUEUE_PAUSED);
+ }
+
+ /**
+ * Gets a bundle of extras for this status object.
+ * The extras will be ignored by the media router but they may be used
+ * by applications.
+ */
+ public Bundle getExtras() {
+ return mBundle.getBundle(KEY_EXTRAS);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("MediaSessionStatus{ ");
+ result.append("timestamp=");
+ TimeUtils.formatDuration(SystemClock.elapsedRealtime() - getTimestamp(), result);
+ result.append(" ms ago");
+ result.append(", sessionState=").append(sessionStateToString(getSessionState()));
+ result.append(", queuePaused=").append(isQueuePaused());
+ result.append(", extras=").append(getExtras());
+ result.append(" }");
+ return result.toString();
+ }
+
+ private static String sessionStateToString(int sessionState) {
+ switch (sessionState) {
+ case SESSION_STATE_ACTIVE:
+ return "active";
+ case SESSION_STATE_ENDED:
+ return "ended";
+ case SESSION_STATE_INVALIDATED:
+ return "invalidated";
+ }
+ return Integer.toString(sessionState);
+ }
+
+ /**
+ * Converts this object to a bundle for serialization.
+ *
+ * @return The contents of the object represented as a bundle.
+ */
+ public Bundle asBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Creates an instance from a bundle.
+ *
+ * @param bundle The bundle, or null if none.
+ * @return The new instance, or null if the bundle was null.
+ */
+ public static MediaSessionStatus fromBundle(Bundle bundle) {
+ return bundle != null ? new MediaSessionStatus(bundle) : null;
+ }
+
+ /**
+ * Builder for {@link MediaSessionStatus media session status objects}.
+ */
+ public static final class Builder {
+ private final Bundle mBundle;
+
+ /**
+ * Creates a media session status builder using the current time as the
+ * reference timestamp.
+ *
+ * @param sessionState The session state.
+ */
+ public Builder(int sessionState) {
+ mBundle = new Bundle();
+ setTimestamp(SystemClock.elapsedRealtime());
+ setSessionState(sessionState);
+ }
+
+ /**
+ * Creates a media session status builder whose initial contents are
+ * copied from an existing status.
+ */
+ public Builder(MediaSessionStatus status) {
+ if (status == null) {
+ throw new IllegalArgumentException("status must not be null");
+ }
+
+ mBundle = new Bundle(status.mBundle);
+ }
+
+ /**
+ * Sets the timestamp associated with the status information in
+ * milliseconds since boot in the {@link SystemClock#elapsedRealtime} time base.
+ */
+ public Builder setTimestamp(long elapsedRealtimeTimestamp) {
+ mBundle.putLong(KEY_TIMESTAMP, elapsedRealtimeTimestamp);
+ return this;
+ }
+
+ /**
+ * Sets the session state.
+ */
+ public Builder setSessionState(int sessionState) {
+ mBundle.putInt(KEY_SESSION_STATE, sessionState);
+ return this;
+ }
+
+ /**
+ * Sets whether the queue is paused.
+ */
+ public Builder setQueuePaused(boolean queuePaused) {
+ mBundle.putBoolean(KEY_QUEUE_PAUSED, queuePaused);
+ return this;
+ }
+
+ /**
+ * Sets a bundle of extras for this status object.
+ * The extras will be ignored by the media router but they may be used
+ * by applications.
+ */
+ public Builder setExtras(Bundle extras) {
+ mBundle.putBundle(KEY_EXTRAS, extras);
+ return this;
+ }
+
+ /**
+ * Builds the {@link MediaSessionStatus media session status object}.
+ */
+ public MediaSessionStatus build() {
+ return new MediaSessionStatus(mBundle);
+ }
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/media/RegisteredMediaRouteProvider.java b/packages/MediaComponents/src/com/android/support/mediarouter/media/RegisteredMediaRouteProvider.java
new file mode 100644
index 0000000..98e4e28
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/media/RegisteredMediaRouteProvider.java
@@ -0,0 +1,741 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.media;
+
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_DATA_ROUTE_ID;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_DATA_ROUTE_LIBRARY_GROUP;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_DATA_UNSELECT_REASON;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_DATA_VOLUME;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_CREATE_ROUTE_CONTROLLER;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_MSG_REGISTER;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_RELEASE_ROUTE_CONTROLLER;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_ROUTE_CONTROL_REQUEST;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_MSG_SELECT_ROUTE;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_SET_DISCOVERY_REQUEST;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_SET_ROUTE_VOLUME;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_MSG_UNREGISTER;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_UNSELECT_ROUTE;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_UPDATE_ROUTE_VOLUME;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_VERSION_CURRENT;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.SERVICE_DATA_ERROR;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .SERVICE_MSG_CONTROL_REQUEST_FAILED;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .SERVICE_MSG_CONTROL_REQUEST_SUCCEEDED;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .SERVICE_MSG_DESCRIPTOR_CHANGED;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .SERVICE_MSG_GENERIC_FAILURE;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .SERVICE_MSG_GENERIC_SUCCESS;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.SERVICE_MSG_REGISTERED;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.SERVICE_VERSION_1;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.isValidRemoteMessenger;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.DeadObjectException;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IBinder.DeathRecipient;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.support.mediarouter.media.MediaRouter.ControlRequestCallback;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Maintains a connection to a particular media route provider service.
+ */
+final class RegisteredMediaRouteProvider extends MediaRouteProvider
+ implements ServiceConnection {
+ static final String TAG = "MediaRouteProviderProxy"; // max. 23 chars
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private final ComponentName mComponentName;
+ final PrivateHandler mPrivateHandler;
+ private final ArrayList<Controller> mControllers = new ArrayList<Controller>();
+
+ private boolean mStarted;
+ private boolean mBound;
+ private Connection mActiveConnection;
+ private boolean mConnectionReady;
+
+ public RegisteredMediaRouteProvider(Context context, ComponentName componentName) {
+ super(context, new ProviderMetadata(componentName));
+
+ mComponentName = componentName;
+ mPrivateHandler = new PrivateHandler();
+ }
+
+ @Override
+ public RouteController onCreateRouteController(@NonNull String routeId) {
+ if (routeId == null) {
+ throw new IllegalArgumentException("routeId cannot be null");
+ }
+ return createRouteController(routeId, null);
+ }
+
+ @Override
+ public RouteController onCreateRouteController(
+ @NonNull String routeId, @NonNull String routeGroupId) {
+ if (routeId == null) {
+ throw new IllegalArgumentException("routeId cannot be null");
+ }
+ if (routeGroupId == null) {
+ throw new IllegalArgumentException("routeGroupId cannot be null");
+ }
+ return createRouteController(routeId, routeGroupId);
+ }
+
+ @Override
+ public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) {
+ if (mConnectionReady) {
+ mActiveConnection.setDiscoveryRequest(request);
+ }
+ updateBinding();
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (DEBUG) {
+ Log.d(TAG, this + ": Connected");
+ }
+
+ if (mBound) {
+ disconnect();
+
+ Messenger messenger = (service != null ? new Messenger(service) : null);
+ if (isValidRemoteMessenger(messenger)) {
+ Connection connection = new Connection(messenger);
+ if (connection.register()) {
+ mActiveConnection = connection;
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, this + ": Registration failed");
+ }
+ }
+ } else {
+ Log.e(TAG, this + ": Service returned invalid messenger binder");
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (DEBUG) {
+ Log.d(TAG, this + ": Service disconnected");
+ }
+ disconnect();
+ }
+
+ @Override
+ public String toString() {
+ return "Service connection " + mComponentName.flattenToShortString();
+ }
+
+ public boolean hasComponentName(String packageName, String className) {
+ return mComponentName.getPackageName().equals(packageName)
+ && mComponentName.getClassName().equals(className);
+ }
+
+ public void start() {
+ if (!mStarted) {
+ if (DEBUG) {
+ Log.d(TAG, this + ": Starting");
+ }
+
+ mStarted = true;
+ updateBinding();
+ }
+ }
+
+ public void stop() {
+ if (mStarted) {
+ if (DEBUG) {
+ Log.d(TAG, this + ": Stopping");
+ }
+
+ mStarted = false;
+ updateBinding();
+ }
+ }
+
+ public void rebindIfDisconnected() {
+ if (mActiveConnection == null && shouldBind()) {
+ unbind();
+ bind();
+ }
+ }
+
+ private void updateBinding() {
+ if (shouldBind()) {
+ bind();
+ } else {
+ unbind();
+ }
+ }
+
+ private boolean shouldBind() {
+ if (mStarted) {
+ // Bind whenever there is a discovery request.
+ if (getDiscoveryRequest() != null) {
+ return true;
+ }
+
+ // Bind whenever the application has an active route controller.
+ // This means that one of this provider's routes is selected.
+ if (!mControllers.isEmpty()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void bind() {
+ if (!mBound) {
+ if (DEBUG) {
+ Log.d(TAG, this + ": Binding");
+ }
+
+ Intent service = new Intent(MediaRouteProviderProtocol.SERVICE_INTERFACE);
+ service.setComponent(mComponentName);
+ try {
+ mBound = getContext().bindService(service, this, Context.BIND_AUTO_CREATE);
+ if (!mBound && DEBUG) {
+ Log.d(TAG, this + ": Bind failed");
+ }
+ } catch (SecurityException ex) {
+ if (DEBUG) {
+ Log.d(TAG, this + ": Bind failed", ex);
+ }
+ }
+ }
+ }
+
+ private void unbind() {
+ if (mBound) {
+ if (DEBUG) {
+ Log.d(TAG, this + ": Unbinding");
+ }
+
+ mBound = false;
+ disconnect();
+ getContext().unbindService(this);
+ }
+ }
+
+ private RouteController createRouteController(String routeId, String routeGroupId) {
+ MediaRouteProviderDescriptor descriptor = getDescriptor();
+ if (descriptor != null) {
+ List<MediaRouteDescriptor> routes = descriptor.getRoutes();
+ final int count = routes.size();
+ for (int i = 0; i < count; i++) {
+ final MediaRouteDescriptor route = routes.get(i);
+ if (route.getId().equals(routeId)) {
+ Controller controller = new Controller(routeId, routeGroupId);
+ mControllers.add(controller);
+ if (mConnectionReady) {
+ controller.attachConnection(mActiveConnection);
+ }
+ updateBinding();
+ return controller;
+ }
+ }
+ }
+ return null;
+ }
+
+ void onConnectionReady(Connection connection) {
+ if (mActiveConnection == connection) {
+ mConnectionReady = true;
+ attachControllersToConnection();
+
+ MediaRouteDiscoveryRequest request = getDiscoveryRequest();
+ if (request != null) {
+ mActiveConnection.setDiscoveryRequest(request);
+ }
+ }
+ }
+
+ void onConnectionDied(Connection connection) {
+ if (mActiveConnection == connection) {
+ if (DEBUG) {
+ Log.d(TAG, this + ": Service connection died");
+ }
+ disconnect();
+ }
+ }
+
+ void onConnectionError(Connection connection, String error) {
+ if (mActiveConnection == connection) {
+ if (DEBUG) {
+ Log.d(TAG, this + ": Service connection error - " + error);
+ }
+ unbind();
+ }
+ }
+
+ void onConnectionDescriptorChanged(Connection connection,
+ MediaRouteProviderDescriptor descriptor) {
+ if (mActiveConnection == connection) {
+ if (DEBUG) {
+ Log.d(TAG, this + ": Descriptor changed, descriptor=" + descriptor);
+ }
+ setDescriptor(descriptor);
+ }
+ }
+
+ private void disconnect() {
+ if (mActiveConnection != null) {
+ setDescriptor(null);
+ mConnectionReady = false;
+ detachControllersFromConnection();
+ mActiveConnection.dispose();
+ mActiveConnection = null;
+ }
+ }
+
+ void onControllerReleased(Controller controller) {
+ mControllers.remove(controller);
+ controller.detachConnection();
+ updateBinding();
+ }
+
+ private void attachControllersToConnection() {
+ int count = mControllers.size();
+ for (int i = 0; i < count; i++) {
+ mControllers.get(i).attachConnection(mActiveConnection);
+ }
+ }
+
+ private void detachControllersFromConnection() {
+ int count = mControllers.size();
+ for (int i = 0; i < count; i++) {
+ mControllers.get(i).detachConnection();
+ }
+ }
+
+ private final class Controller extends RouteController {
+ private final String mRouteId;
+ private final String mRouteGroupId;
+
+ private boolean mSelected;
+ private int mPendingSetVolume = -1;
+ private int mPendingUpdateVolumeDelta;
+
+ private Connection mConnection;
+ private int mControllerId;
+
+ public Controller(String routeId, String routeGroupId) {
+ mRouteId = routeId;
+ mRouteGroupId = routeGroupId;
+ }
+
+ public void attachConnection(Connection connection) {
+ mConnection = connection;
+ mControllerId = connection.createRouteController(mRouteId, mRouteGroupId);
+ if (mSelected) {
+ connection.selectRoute(mControllerId);
+ if (mPendingSetVolume >= 0) {
+ connection.setVolume(mControllerId, mPendingSetVolume);
+ mPendingSetVolume = -1;
+ }
+ if (mPendingUpdateVolumeDelta != 0) {
+ connection.updateVolume(mControllerId, mPendingUpdateVolumeDelta);
+ mPendingUpdateVolumeDelta = 0;
+ }
+ }
+ }
+
+ public void detachConnection() {
+ if (mConnection != null) {
+ mConnection.releaseRouteController(mControllerId);
+ mConnection = null;
+ mControllerId = 0;
+ }
+ }
+
+ @Override
+ public void onRelease() {
+ onControllerReleased(this);
+ }
+
+ @Override
+ public void onSelect() {
+ mSelected = true;
+ if (mConnection != null) {
+ mConnection.selectRoute(mControllerId);
+ }
+ }
+
+ @Override
+ public void onUnselect() {
+ onUnselect(MediaRouter.UNSELECT_REASON_UNKNOWN);
+ }
+
+ @Override
+ public void onUnselect(int reason) {
+ mSelected = false;
+ if (mConnection != null) {
+ mConnection.unselectRoute(mControllerId, reason);
+ }
+ }
+
+ @Override
+ public void onSetVolume(int volume) {
+ if (mConnection != null) {
+ mConnection.setVolume(mControllerId, volume);
+ } else {
+ mPendingSetVolume = volume;
+ mPendingUpdateVolumeDelta = 0;
+ }
+ }
+
+ @Override
+ public void onUpdateVolume(int delta) {
+ if (mConnection != null) {
+ mConnection.updateVolume(mControllerId, delta);
+ } else {
+ mPendingUpdateVolumeDelta += delta;
+ }
+ }
+
+ @Override
+ public boolean onControlRequest(Intent intent, ControlRequestCallback callback) {
+ if (mConnection != null) {
+ return mConnection.sendControlRequest(mControllerId, intent, callback);
+ }
+ return false;
+ }
+ }
+
+ private final class Connection implements DeathRecipient {
+ private final Messenger mServiceMessenger;
+ private final ReceiveHandler mReceiveHandler;
+ private final Messenger mReceiveMessenger;
+
+ private int mNextRequestId = 1;
+ private int mNextControllerId = 1;
+ private int mServiceVersion; // non-zero when registration complete
+
+ private int mPendingRegisterRequestId;
+ private final SparseArray<ControlRequestCallback> mPendingCallbacks =
+ new SparseArray<ControlRequestCallback>();
+
+ public Connection(Messenger serviceMessenger) {
+ mServiceMessenger = serviceMessenger;
+ mReceiveHandler = new ReceiveHandler(this);
+ mReceiveMessenger = new Messenger(mReceiveHandler);
+ }
+
+ public boolean register() {
+ mPendingRegisterRequestId = mNextRequestId++;
+ if (!sendRequest(CLIENT_MSG_REGISTER,
+ mPendingRegisterRequestId,
+ CLIENT_VERSION_CURRENT, null, null)) {
+ return false;
+ }
+
+ try {
+ mServiceMessenger.getBinder().linkToDeath(this, 0);
+ return true;
+ } catch (RemoteException ex) {
+ binderDied();
+ }
+ return false;
+ }
+
+ public void dispose() {
+ sendRequest(CLIENT_MSG_UNREGISTER, 0, 0, null, null);
+ mReceiveHandler.dispose();
+ mServiceMessenger.getBinder().unlinkToDeath(this, 0);
+
+ mPrivateHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ failPendingCallbacks();
+ }
+ });
+ }
+
+ void failPendingCallbacks() {
+ int count = 0;
+ for (int i = 0; i < mPendingCallbacks.size(); i++) {
+ mPendingCallbacks.valueAt(i).onError(null, null);
+ }
+ mPendingCallbacks.clear();
+ }
+
+ public boolean onGenericFailure(int requestId) {
+ if (requestId == mPendingRegisterRequestId) {
+ mPendingRegisterRequestId = 0;
+ onConnectionError(this, "Registration failed");
+ }
+ ControlRequestCallback callback = mPendingCallbacks.get(requestId);
+ if (callback != null) {
+ mPendingCallbacks.remove(requestId);
+ callback.onError(null, null);
+ }
+ return true;
+ }
+
+ public boolean onGenericSuccess(int requestId) {
+ return true;
+ }
+
+ public boolean onRegistered(int requestId, int serviceVersion,
+ Bundle descriptorBundle) {
+ if (mServiceVersion == 0
+ && requestId == mPendingRegisterRequestId
+ && serviceVersion >= SERVICE_VERSION_1) {
+ mPendingRegisterRequestId = 0;
+ mServiceVersion = serviceVersion;
+ onConnectionDescriptorChanged(this,
+ MediaRouteProviderDescriptor.fromBundle(descriptorBundle));
+ onConnectionReady(this);
+ return true;
+ }
+ return false;
+ }
+
+ public boolean onDescriptorChanged(Bundle descriptorBundle) {
+ if (mServiceVersion != 0) {
+ onConnectionDescriptorChanged(this,
+ MediaRouteProviderDescriptor.fromBundle(descriptorBundle));
+ return true;
+ }
+ return false;
+ }
+
+ public boolean onControlRequestSucceeded(int requestId, Bundle data) {
+ ControlRequestCallback callback = mPendingCallbacks.get(requestId);
+ if (callback != null) {
+ mPendingCallbacks.remove(requestId);
+ callback.onResult(data);
+ return true;
+ }
+ return false;
+ }
+
+ public boolean onControlRequestFailed(int requestId, String error, Bundle data) {
+ ControlRequestCallback callback = mPendingCallbacks.get(requestId);
+ if (callback != null) {
+ mPendingCallbacks.remove(requestId);
+ callback.onError(error, data);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void binderDied() {
+ mPrivateHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ onConnectionDied(Connection.this);
+ }
+ });
+ }
+
+ public int createRouteController(String routeId, String routeGroupId) {
+ int controllerId = mNextControllerId++;
+ Bundle data = new Bundle();
+ data.putString(CLIENT_DATA_ROUTE_ID, routeId);
+ data.putString(CLIENT_DATA_ROUTE_LIBRARY_GROUP, routeGroupId);
+ sendRequest(CLIENT_MSG_CREATE_ROUTE_CONTROLLER,
+ mNextRequestId++, controllerId, null, data);
+ return controllerId;
+ }
+
+ public void releaseRouteController(int controllerId) {
+ sendRequest(CLIENT_MSG_RELEASE_ROUTE_CONTROLLER,
+ mNextRequestId++, controllerId, null, null);
+ }
+
+ public void selectRoute(int controllerId) {
+ sendRequest(CLIENT_MSG_SELECT_ROUTE,
+ mNextRequestId++, controllerId, null, null);
+ }
+
+ public void unselectRoute(int controllerId, int reason) {
+ Bundle extras = new Bundle();
+ extras.putInt(CLIENT_DATA_UNSELECT_REASON, reason);
+ sendRequest(CLIENT_MSG_UNSELECT_ROUTE,
+ mNextRequestId++, controllerId, null, extras);
+ }
+
+ public void setVolume(int controllerId, int volume) {
+ Bundle data = new Bundle();
+ data.putInt(CLIENT_DATA_VOLUME, volume);
+ sendRequest(CLIENT_MSG_SET_ROUTE_VOLUME,
+ mNextRequestId++, controllerId, null, data);
+ }
+
+ public void updateVolume(int controllerId, int delta) {
+ Bundle data = new Bundle();
+ data.putInt(CLIENT_DATA_VOLUME, delta);
+ sendRequest(CLIENT_MSG_UPDATE_ROUTE_VOLUME,
+ mNextRequestId++, controllerId, null, data);
+ }
+
+ public boolean sendControlRequest(int controllerId, Intent intent,
+ ControlRequestCallback callback) {
+ int requestId = mNextRequestId++;
+ if (sendRequest(CLIENT_MSG_ROUTE_CONTROL_REQUEST,
+ requestId, controllerId, intent, null)) {
+ if (callback != null) {
+ mPendingCallbacks.put(requestId, callback);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ public void setDiscoveryRequest(MediaRouteDiscoveryRequest request) {
+ sendRequest(CLIENT_MSG_SET_DISCOVERY_REQUEST,
+ mNextRequestId++, 0, request != null ? request.asBundle() : null, null);
+ }
+
+ private boolean sendRequest(int what, int requestId, int arg, Object obj, Bundle data) {
+ Message msg = Message.obtain();
+ msg.what = what;
+ msg.arg1 = requestId;
+ msg.arg2 = arg;
+ msg.obj = obj;
+ msg.setData(data);
+ msg.replyTo = mReceiveMessenger;
+ try {
+ mServiceMessenger.send(msg);
+ return true;
+ } catch (DeadObjectException ex) {
+ // The service died.
+ } catch (RemoteException ex) {
+ if (what != CLIENT_MSG_UNREGISTER) {
+ Log.e(TAG, "Could not send message to service.", ex);
+ }
+ }
+ return false;
+ }
+ }
+
+ private static final class PrivateHandler extends Handler {
+ PrivateHandler() {
+ }
+ }
+
+ /**
+ * Handler that receives messages from the server.
+ * <p>
+ * This inner class is static and only retains a weak reference to the connection
+ * to prevent the client from being leaked in case the service is holding an
+ * active reference to the client's messenger.
+ * </p><p>
+ * This handler should not be used to handle any messages other than those
+ * that come from the service.
+ * </p>
+ */
+ private static final class ReceiveHandler extends Handler {
+ private final WeakReference<Connection> mConnectionRef;
+
+ public ReceiveHandler(Connection connection) {
+ mConnectionRef = new WeakReference<Connection>(connection);
+ }
+
+ public void dispose() {
+ mConnectionRef.clear();
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ Connection connection = mConnectionRef.get();
+ if (connection != null) {
+ final int what = msg.what;
+ final int requestId = msg.arg1;
+ final int arg = msg.arg2;
+ final Object obj = msg.obj;
+ final Bundle data = msg.peekData();
+ if (!processMessage(connection, what, requestId, arg, obj, data)) {
+ if (DEBUG) {
+ Log.d(TAG, "Unhandled message from server: " + msg);
+ }
+ }
+ }
+ }
+
+ private boolean processMessage(Connection connection,
+ int what, int requestId, int arg, Object obj, Bundle data) {
+ switch (what) {
+ case SERVICE_MSG_GENERIC_FAILURE:
+ connection.onGenericFailure(requestId);
+ return true;
+
+ case SERVICE_MSG_GENERIC_SUCCESS:
+ connection.onGenericSuccess(requestId);
+ return true;
+
+ case SERVICE_MSG_REGISTERED:
+ if (obj == null || obj instanceof Bundle) {
+ return connection.onRegistered(requestId, arg, (Bundle)obj);
+ }
+ break;
+
+ case SERVICE_MSG_DESCRIPTOR_CHANGED:
+ if (obj == null || obj instanceof Bundle) {
+ return connection.onDescriptorChanged((Bundle)obj);
+ }
+ break;
+
+ case SERVICE_MSG_CONTROL_REQUEST_SUCCEEDED:
+ if (obj == null || obj instanceof Bundle) {
+ return connection.onControlRequestSucceeded(
+ requestId, (Bundle)obj);
+ }
+ break;
+
+ case SERVICE_MSG_CONTROL_REQUEST_FAILED:
+ if (obj == null || obj instanceof Bundle) {
+ String error = (data == null ? null :
+ data.getString(SERVICE_DATA_ERROR));
+ return connection.onControlRequestFailed(
+ requestId, error, (Bundle)obj);
+ }
+ break;
+ }
+ return false;
+ }
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/media/RegisteredMediaRouteProviderWatcher.java b/packages/MediaComponents/src/com/android/support/mediarouter/media/RegisteredMediaRouteProviderWatcher.java
new file mode 100644
index 0000000..ba1f647
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/media/RegisteredMediaRouteProviderWatcher.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.media;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Handler;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * Watches for media route provider services to be installed.
+ * Adds a provider to the media router for each registered service.
+ *
+ * @see RegisteredMediaRouteProvider
+ */
+final class RegisteredMediaRouteProviderWatcher {
+ private final Context mContext;
+ private final Callback mCallback;
+ private final Handler mHandler;
+ private final PackageManager mPackageManager;
+
+ private final ArrayList<RegisteredMediaRouteProvider> mProviders =
+ new ArrayList<RegisteredMediaRouteProvider>();
+ private boolean mRunning;
+
+ public RegisteredMediaRouteProviderWatcher(Context context, Callback callback) {
+ mContext = context;
+ mCallback = callback;
+ mHandler = new Handler();
+ mPackageManager = context.getPackageManager();
+ }
+
+ public void start() {
+ if (!mRunning) {
+ mRunning = true;
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+ filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+ filter.addAction(Intent.ACTION_PACKAGE_RESTARTED);
+ filter.addDataScheme("package");
+ mContext.registerReceiver(mScanPackagesReceiver, filter, null, mHandler);
+
+ // Scan packages.
+ // Also has the side-effect of restarting providers if needed.
+ mHandler.post(mScanPackagesRunnable);
+ }
+ }
+
+ public void stop() {
+ if (mRunning) {
+ mRunning = false;
+
+ mContext.unregisterReceiver(mScanPackagesReceiver);
+ mHandler.removeCallbacks(mScanPackagesRunnable);
+
+ // Stop all providers.
+ for (int i = mProviders.size() - 1; i >= 0; i--) {
+ mProviders.get(i).stop();
+ }
+ }
+ }
+
+ void scanPackages() {
+ if (!mRunning) {
+ return;
+ }
+
+ // Add providers for all new services.
+ // Reorder the list so that providers left at the end will be the ones to remove.
+ int targetIndex = 0;
+ Intent intent = new Intent(MediaRouteProviderService.SERVICE_INTERFACE);
+ for (ResolveInfo resolveInfo : mPackageManager.queryIntentServices(intent, 0)) {
+ ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+ if (serviceInfo != null) {
+ int sourceIndex = findProvider(serviceInfo.packageName, serviceInfo.name);
+ if (sourceIndex < 0) {
+ RegisteredMediaRouteProvider provider =
+ new RegisteredMediaRouteProvider(mContext,
+ new ComponentName(serviceInfo.packageName, serviceInfo.name));
+ provider.start();
+ mProviders.add(targetIndex++, provider);
+ mCallback.addProvider(provider);
+ } else if (sourceIndex >= targetIndex) {
+ RegisteredMediaRouteProvider provider = mProviders.get(sourceIndex);
+ provider.start(); // restart the provider if needed
+ provider.rebindIfDisconnected();
+ Collections.swap(mProviders, sourceIndex, targetIndex++);
+ }
+ }
+ }
+
+ // Remove providers for missing services.
+ if (targetIndex < mProviders.size()) {
+ for (int i = mProviders.size() - 1; i >= targetIndex; i--) {
+ RegisteredMediaRouteProvider provider = mProviders.get(i);
+ mCallback.removeProvider(provider);
+ mProviders.remove(provider);
+ provider.stop();
+ }
+ }
+ }
+
+ private int findProvider(String packageName, String className) {
+ int count = mProviders.size();
+ for (int i = 0; i < count; i++) {
+ RegisteredMediaRouteProvider provider = mProviders.get(i);
+ if (provider.hasComponentName(packageName, className)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private final BroadcastReceiver mScanPackagesReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ scanPackages();
+ }
+ };
+
+ private final Runnable mScanPackagesRunnable = new Runnable() {
+ @Override
+ public void run() {
+ scanPackages();
+ }
+ };
+
+ public interface Callback {
+ void addProvider(MediaRouteProvider provider);
+ void removeProvider(MediaRouteProvider provider);
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/media/RemoteControlClientCompat.java b/packages/MediaComponents/src/com/android/support/mediarouter/media/RemoteControlClientCompat.java
new file mode 100644
index 0000000..826449b
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/media/RemoteControlClientCompat.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.support.mediarouter.media;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.os.Build;
+import android.support.annotation.RequiresApi;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Provides access to features of the remote control client.
+ *
+ * Hidden for now but we might want to make this available to applications
+ * in the future.
+ */
+abstract class RemoteControlClientCompat {
+ protected final Context mContext;
+ protected final Object mRcc;
+ protected VolumeCallback mVolumeCallback;
+
+ protected RemoteControlClientCompat(Context context, Object rcc) {
+ mContext = context;
+ mRcc = rcc;
+ }
+
+ public static RemoteControlClientCompat obtain(Context context, Object rcc) {
+ if (Build.VERSION.SDK_INT >= 16) {
+ return new JellybeanImpl(context, rcc);
+ }
+ return new LegacyImpl(context, rcc);
+ }
+
+ public Object getRemoteControlClient() {
+ return mRcc;
+ }
+
+ /**
+ * Sets the current playback information.
+ * Must be called at least once to attach to the remote control client.
+ *
+ * @param info The playback information. Must not be null.
+ */
+ public void setPlaybackInfo(PlaybackInfo info) {
+ }
+
+ /**
+ * Sets a callback to receive volume change requests from the remote control client.
+ *
+ * @param callback The volume callback to use or null if none.
+ */
+ public void setVolumeCallback(VolumeCallback callback) {
+ mVolumeCallback = callback;
+ }
+
+ /**
+ * Specifies information about the playback.
+ */
+ public static final class PlaybackInfo {
+ public int volume;
+ public int volumeMax;
+ public int volumeHandling = MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED;
+ public int playbackStream = AudioManager.STREAM_MUSIC;
+ public int playbackType = MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE;
+ }
+
+ /**
+ * Called when volume updates are requested by the remote control client.
+ */
+ public interface VolumeCallback {
+ /**
+ * Called when the volume should be increased or decreased.
+ *
+ * @param direction An integer indicating whether the volume is to be increased
+ * (positive value) or decreased (negative value).
+ * For bundled changes, the absolute value indicates the number of changes
+ * in the same direction, e.g. +3 corresponds to three "volume up" changes.
+ */
+ public void onVolumeUpdateRequest(int direction);
+
+ /**
+ * Called when the volume for the route should be set to the given value.
+ *
+ * @param volume An integer indicating the new volume value that should be used,
+ * always between 0 and the value set by {@link PlaybackInfo#volumeMax}.
+ */
+ public void onVolumeSetRequest(int volume);
+ }
+
+ /**
+ * Legacy implementation for platform versions prior to Jellybean.
+ * Does nothing.
+ */
+ static class LegacyImpl extends RemoteControlClientCompat {
+ public LegacyImpl(Context context, Object rcc) {
+ super(context, rcc);
+ }
+ }
+
+ /**
+ * Implementation for Jellybean.
+ *
+ * The basic idea of this implementation is to attach the RCC to a UserRouteInfo
+ * in order to hook up stream metadata and volume callbacks because there is no
+ * other API available to do so in this platform version. The UserRouteInfo itself
+ * is not attached to the MediaRouter so it is transparent to the user.
+ */
+ // @@RequiresApi(16)
+ static class JellybeanImpl extends RemoteControlClientCompat {
+ private final Object mRouterObj;
+ private final Object mUserRouteCategoryObj;
+ private final Object mUserRouteObj;
+ private boolean mRegistered;
+
+ public JellybeanImpl(Context context, Object rcc) {
+ super(context, rcc);
+
+ mRouterObj = MediaRouterJellybean.getMediaRouter(context);
+ mUserRouteCategoryObj = MediaRouterJellybean.createRouteCategory(
+ mRouterObj, "", false);
+ mUserRouteObj = MediaRouterJellybean.createUserRoute(
+ mRouterObj, mUserRouteCategoryObj);
+ }
+
+ @Override
+ public void setPlaybackInfo(PlaybackInfo info) {
+ MediaRouterJellybean.UserRouteInfo.setVolume(
+ mUserRouteObj, info.volume);
+ MediaRouterJellybean.UserRouteInfo.setVolumeMax(
+ mUserRouteObj, info.volumeMax);
+ MediaRouterJellybean.UserRouteInfo.setVolumeHandling(
+ mUserRouteObj, info.volumeHandling);
+ MediaRouterJellybean.UserRouteInfo.setPlaybackStream(
+ mUserRouteObj, info.playbackStream);
+ MediaRouterJellybean.UserRouteInfo.setPlaybackType(
+ mUserRouteObj, info.playbackType);
+
+ if (!mRegistered) {
+ mRegistered = true;
+ MediaRouterJellybean.UserRouteInfo.setVolumeCallback(mUserRouteObj,
+ MediaRouterJellybean.createVolumeCallback(
+ new VolumeCallbackWrapper(this)));
+ MediaRouterJellybean.UserRouteInfo.setRemoteControlClient(mUserRouteObj, mRcc);
+ }
+ }
+
+ private static final class VolumeCallbackWrapper
+ implements MediaRouterJellybean.VolumeCallback {
+ // Unfortunately, the framework never unregisters its volume observer from
+ // the audio service so the UserRouteInfo object may leak along with
+ // any callbacks that we attach to it. Use a weak reference to prevent
+ // the volume callback from holding strong references to anything important.
+ private final WeakReference<JellybeanImpl> mImplWeak;
+
+ public VolumeCallbackWrapper(JellybeanImpl impl) {
+ mImplWeak = new WeakReference<JellybeanImpl>(impl);
+ }
+
+ @Override
+ public void onVolumeUpdateRequest(Object routeObj, int direction) {
+ JellybeanImpl impl = mImplWeak.get();
+ if (impl != null && impl.mVolumeCallback != null) {
+ impl.mVolumeCallback.onVolumeUpdateRequest(direction);
+ }
+ }
+
+ @Override
+ public void onVolumeSetRequest(Object routeObj, int volume) {
+ JellybeanImpl impl = mImplWeak.get();
+ if (impl != null && impl.mVolumeCallback != null) {
+ impl.mVolumeCallback.onVolumeSetRequest(volume);
+ }
+ }
+ }
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/media/RemotePlaybackClient.java b/packages/MediaComponents/src/com/android/support/mediarouter/media/RemotePlaybackClient.java
new file mode 100644
index 0000000..f6e1497
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/media/RemotePlaybackClient.java
@@ -0,0 +1,1044 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.support.mediarouter.media;
+
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.util.ObjectsCompat;
+import android.util.Log;
+
+/**
+ * A helper class for playing media on remote routes using the remote playback protocol
+ * defined by {@link MediaControlIntent}.
+ * <p>
+ * The client maintains session state and offers a simplified interface for issuing
+ * remote playback media control intents to a single route.
+ * </p>
+ */
+public class RemotePlaybackClient {
+ static final String TAG = "RemotePlaybackClient";
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private final Context mContext;
+ private final MediaRouter.RouteInfo mRoute;
+ private final ActionReceiver mActionReceiver;
+ private final PendingIntent mItemStatusPendingIntent;
+ private final PendingIntent mSessionStatusPendingIntent;
+ private final PendingIntent mMessagePendingIntent;
+
+ private boolean mRouteSupportsRemotePlayback;
+ private boolean mRouteSupportsQueuing;
+ private boolean mRouteSupportsSessionManagement;
+ private boolean mRouteSupportsMessaging;
+
+ String mSessionId;
+ StatusCallback mStatusCallback;
+ OnMessageReceivedListener mOnMessageReceivedListener;
+
+ /**
+ * Creates a remote playback client for a route.
+ *
+ * @param route The media route.
+ */
+ public RemotePlaybackClient(Context context, MediaRouter.RouteInfo route) {
+ if (context == null) {
+ throw new IllegalArgumentException("context must not be null");
+ }
+ if (route == null) {
+ throw new IllegalArgumentException("route must not be null");
+ }
+
+ mContext = context;
+ mRoute = route;
+
+ IntentFilter actionFilter = new IntentFilter();
+ actionFilter.addAction(ActionReceiver.ACTION_ITEM_STATUS_CHANGED);
+ actionFilter.addAction(ActionReceiver.ACTION_SESSION_STATUS_CHANGED);
+ actionFilter.addAction(ActionReceiver.ACTION_MESSAGE_RECEIVED);
+ mActionReceiver = new ActionReceiver();
+ context.registerReceiver(mActionReceiver, actionFilter);
+
+ Intent itemStatusIntent = new Intent(ActionReceiver.ACTION_ITEM_STATUS_CHANGED);
+ itemStatusIntent.setPackage(context.getPackageName());
+ mItemStatusPendingIntent = PendingIntent.getBroadcast(
+ context, 0, itemStatusIntent, 0);
+
+ Intent sessionStatusIntent = new Intent(ActionReceiver.ACTION_SESSION_STATUS_CHANGED);
+ sessionStatusIntent.setPackage(context.getPackageName());
+ mSessionStatusPendingIntent = PendingIntent.getBroadcast(
+ context, 0, sessionStatusIntent, 0);
+
+ Intent messageIntent = new Intent(ActionReceiver.ACTION_MESSAGE_RECEIVED);
+ messageIntent.setPackage(context.getPackageName());
+ mMessagePendingIntent = PendingIntent.getBroadcast(
+ context, 0, messageIntent, 0);
+ detectFeatures();
+ }
+
+ /**
+ * Releases resources owned by the client.
+ */
+ public void release() {
+ mContext.unregisterReceiver(mActionReceiver);
+ }
+
+ /**
+ * Returns true if the route supports remote playback.
+ * <p>
+ * If the route does not support remote playback, then none of the functionality
+ * offered by the client will be available.
+ * </p><p>
+ * This method returns true if the route supports all of the following
+ * actions: {@link MediaControlIntent#ACTION_PLAY play},
+ * {@link MediaControlIntent#ACTION_SEEK seek},
+ * {@link MediaControlIntent#ACTION_GET_STATUS get status},
+ * {@link MediaControlIntent#ACTION_PAUSE pause},
+ * {@link MediaControlIntent#ACTION_RESUME resume},
+ * {@link MediaControlIntent#ACTION_STOP stop}.
+ * </p>
+ *
+ * @return True if remote playback is supported.
+ */
+ public boolean isRemotePlaybackSupported() {
+ return mRouteSupportsRemotePlayback;
+ }
+
+ /**
+ * Returns true if the route supports queuing features.
+ * <p>
+ * If the route does not support queuing, then at most one media item can be played
+ * at a time and the {@link #enqueue} method will not be available.
+ * </p><p>
+ * This method returns true if the route supports all of the basic remote playback
+ * actions and all of the following actions:
+ * {@link MediaControlIntent#ACTION_ENQUEUE enqueue},
+ * {@link MediaControlIntent#ACTION_REMOVE remove}.
+ * </p>
+ *
+ * @return True if queuing is supported. Implies {@link #isRemotePlaybackSupported}
+ * is also true.
+ *
+ * @see #isRemotePlaybackSupported
+ */
+ public boolean isQueuingSupported() {
+ return mRouteSupportsQueuing;
+ }
+
+ /**
+ * Returns true if the route supports session management features.
+ * <p>
+ * If the route does not support session management, then the session will
+ * not be created until the first media item is played.
+ * </p><p>
+ * This method returns true if the route supports all of the basic remote playback
+ * actions and all of the following actions:
+ * {@link MediaControlIntent#ACTION_START_SESSION start session},
+ * {@link MediaControlIntent#ACTION_GET_SESSION_STATUS get session status},
+ * {@link MediaControlIntent#ACTION_END_SESSION end session}.
+ * </p>
+ *
+ * @return True if session management is supported.
+ * Implies {@link #isRemotePlaybackSupported} is also true.
+ *
+ * @see #isRemotePlaybackSupported
+ */
+ public boolean isSessionManagementSupported() {
+ return mRouteSupportsSessionManagement;
+ }
+
+ /**
+ * Returns true if the route supports messages.
+ * <p>
+ * This method returns true if the route supports all of the basic remote playback
+ * actions and all of the following actions:
+ * {@link MediaControlIntent#ACTION_START_SESSION start session},
+ * {@link MediaControlIntent#ACTION_SEND_MESSAGE send message},
+ * {@link MediaControlIntent#ACTION_END_SESSION end session}.
+ * </p>
+ *
+ * @return True if session management is supported.
+ * Implies {@link #isRemotePlaybackSupported} is also true.
+ *
+ * @see #isRemotePlaybackSupported
+ */
+ public boolean isMessagingSupported() {
+ return mRouteSupportsMessaging;
+ }
+
+ /**
+ * Gets the current session id if there is one.
+ *
+ * @return The current session id, or null if none.
+ */
+ public String getSessionId() {
+ return mSessionId;
+ }
+
+ /**
+ * Sets the current session id.
+ * <p>
+ * It is usually not necessary to set the session id explicitly since
+ * it is created as a side-effect of other requests such as
+ * {@link #play}, {@link #enqueue}, and {@link #startSession}.
+ * </p>
+ *
+ * @param sessionId The new session id, or null if none.
+ */
+ public void setSessionId(String sessionId) {
+ if (!ObjectsCompat.equals(mSessionId, sessionId)) {
+ if (DEBUG) {
+ Log.d(TAG, "Session id is now: " + sessionId);
+ }
+ mSessionId = sessionId;
+ if (mStatusCallback != null) {
+ mStatusCallback.onSessionChanged(sessionId);
+ }
+ }
+ }
+
+ /**
+ * Returns true if the client currently has a session.
+ * <p>
+ * Equivalent to checking whether {@link #getSessionId} returns a non-null result.
+ * </p>
+ *
+ * @return True if there is a current session.
+ */
+ public boolean hasSession() {
+ return mSessionId != null;
+ }
+
+ /**
+ * Sets a callback that should receive status updates when the state of
+ * media sessions or media items created by this instance of the remote
+ * playback client changes.
+ * <p>
+ * The callback should be set before the session is created or any play
+ * commands are issued.
+ * </p>
+ *
+ * @param callback The callback to set. May be null to remove the previous callback.
+ */
+ public void setStatusCallback(StatusCallback callback) {
+ mStatusCallback = callback;
+ }
+
+ /**
+ * Sets a callback that should receive messages when a message is sent from
+ * media sessions created by this instance of the remote playback client changes.
+ * <p>
+ * The callback should be set before the session is created.
+ * </p>
+ *
+ * @param listener The callback to set. May be null to remove the previous callback.
+ */
+ public void setOnMessageReceivedListener(OnMessageReceivedListener listener) {
+ mOnMessageReceivedListener = listener;
+ }
+
+ /**
+ * Sends a request to play a media item.
+ * <p>
+ * Clears the queue and starts playing the new item immediately. If the queue
+ * was previously paused, then it is resumed as a side-effect of this request.
+ * </p><p>
+ * The request is issued in the current session. If no session is available, then
+ * one is created implicitly.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_PLAY ACTION_PLAY} for
+ * more information about the semantics of this request.
+ * </p>
+ *
+ * @param contentUri The content Uri to play.
+ * @param mimeType The mime type of the content, or null if unknown.
+ * @param positionMillis The initial content position for the item in milliseconds,
+ * or <code>0</code> to start at the beginning.
+ * @param metadata The media item metadata bundle, or null if none.
+ * @param extras A bundle of extra arguments to be added to the
+ * {@link MediaControlIntent#ACTION_PLAY} intent, or null if none.
+ * @param callback A callback to invoke when the request has been
+ * processed, or null if none.
+ *
+ * @throws UnsupportedOperationException if the route does not support remote playback.
+ *
+ * @see MediaControlIntent#ACTION_PLAY
+ * @see #isRemotePlaybackSupported
+ */
+ public void play(Uri contentUri, String mimeType, Bundle metadata,
+ long positionMillis, Bundle extras, ItemActionCallback callback) {
+ playOrEnqueue(contentUri, mimeType, metadata, positionMillis,
+ extras, callback, MediaControlIntent.ACTION_PLAY);
+ }
+
+ /**
+ * Sends a request to enqueue a media item.
+ * <p>
+ * Enqueues a new item to play. If the queue was previously paused, then will
+ * remain paused.
+ * </p><p>
+ * The request is issued in the current session. If no session is available, then
+ * one is created implicitly.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_ENQUEUE ACTION_ENQUEUE} for
+ * more information about the semantics of this request.
+ * </p>
+ *
+ * @param contentUri The content Uri to enqueue.
+ * @param mimeType The mime type of the content, or null if unknown.
+ * @param positionMillis The initial content position for the item in milliseconds,
+ * or <code>0</code> to start at the beginning.
+ * @param metadata The media item metadata bundle, or null if none.
+ * @param extras A bundle of extra arguments to be added to the
+ * {@link MediaControlIntent#ACTION_ENQUEUE} intent, or null if none.
+ * @param callback A callback to invoke when the request has been
+ * processed, or null if none.
+ *
+ * @throws UnsupportedOperationException if the route does not support queuing.
+ *
+ * @see MediaControlIntent#ACTION_ENQUEUE
+ * @see #isRemotePlaybackSupported
+ * @see #isQueuingSupported
+ */
+ public void enqueue(Uri contentUri, String mimeType, Bundle metadata,
+ long positionMillis, Bundle extras, ItemActionCallback callback) {
+ playOrEnqueue(contentUri, mimeType, metadata, positionMillis,
+ extras, callback, MediaControlIntent.ACTION_ENQUEUE);
+ }
+
+ private void playOrEnqueue(Uri contentUri, String mimeType, Bundle metadata,
+ long positionMillis, Bundle extras,
+ final ItemActionCallback callback, String action) {
+ if (contentUri == null) {
+ throw new IllegalArgumentException("contentUri must not be null");
+ }
+ throwIfRemotePlaybackNotSupported();
+ if (action.equals(MediaControlIntent.ACTION_ENQUEUE)) {
+ throwIfQueuingNotSupported();
+ }
+
+ Intent intent = new Intent(action);
+ intent.setDataAndType(contentUri, mimeType);
+ intent.putExtra(MediaControlIntent.EXTRA_ITEM_STATUS_UPDATE_RECEIVER,
+ mItemStatusPendingIntent);
+ if (metadata != null) {
+ intent.putExtra(MediaControlIntent.EXTRA_ITEM_METADATA, metadata);
+ }
+ if (positionMillis != 0) {
+ intent.putExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, positionMillis);
+ }
+ performItemAction(intent, mSessionId, null, extras, callback);
+ }
+
+ /**
+ * Sends a request to seek to a new position in a media item.
+ * <p>
+ * Seeks to a new position. If the queue was previously paused then it
+ * remains paused but the item's new position is still remembered.
+ * </p><p>
+ * The request is issued in the current session.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_SEEK ACTION_SEEK} for
+ * more information about the semantics of this request.
+ * </p>
+ *
+ * @param itemId The item id.
+ * @param positionMillis The new content position for the item in milliseconds,
+ * or <code>0</code> to start at the beginning.
+ * @param extras A bundle of extra arguments to be added to the
+ * {@link MediaControlIntent#ACTION_SEEK} intent, or null if none.
+ * @param callback A callback to invoke when the request has been
+ * processed, or null if none.
+ *
+ * @throws IllegalStateException if there is no current session.
+ *
+ * @see MediaControlIntent#ACTION_SEEK
+ * @see #isRemotePlaybackSupported
+ */
+ public void seek(String itemId, long positionMillis, Bundle extras,
+ ItemActionCallback callback) {
+ if (itemId == null) {
+ throw new IllegalArgumentException("itemId must not be null");
+ }
+ throwIfNoCurrentSession();
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_SEEK);
+ intent.putExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, positionMillis);
+ performItemAction(intent, mSessionId, itemId, extras, callback);
+ }
+
+ /**
+ * Sends a request to get the status of a media item.
+ * <p>
+ * The request is issued in the current session.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_GET_STATUS ACTION_GET_STATUS} for
+ * more information about the semantics of this request.
+ * </p>
+ *
+ * @param itemId The item id.
+ * @param extras A bundle of extra arguments to be added to the
+ * {@link MediaControlIntent#ACTION_GET_STATUS} intent, or null if none.
+ * @param callback A callback to invoke when the request has been
+ * processed, or null if none.
+ *
+ * @throws IllegalStateException if there is no current session.
+ *
+ * @see MediaControlIntent#ACTION_GET_STATUS
+ * @see #isRemotePlaybackSupported
+ */
+ public void getStatus(String itemId, Bundle extras, ItemActionCallback callback) {
+ if (itemId == null) {
+ throw new IllegalArgumentException("itemId must not be null");
+ }
+ throwIfNoCurrentSession();
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_GET_STATUS);
+ performItemAction(intent, mSessionId, itemId, extras, callback);
+ }
+
+ /**
+ * Sends a request to remove a media item from the queue.
+ * <p>
+ * The request is issued in the current session.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_REMOVE ACTION_REMOVE} for
+ * more information about the semantics of this request.
+ * </p>
+ *
+ * @param itemId The item id.
+ * @param extras A bundle of extra arguments to be added to the
+ * {@link MediaControlIntent#ACTION_REMOVE} intent, or null if none.
+ * @param callback A callback to invoke when the request has been
+ * processed, or null if none.
+ *
+ * @throws IllegalStateException if there is no current session.
+ * @throws UnsupportedOperationException if the route does not support queuing.
+ *
+ * @see MediaControlIntent#ACTION_REMOVE
+ * @see #isRemotePlaybackSupported
+ * @see #isQueuingSupported
+ */
+ public void remove(String itemId, Bundle extras, ItemActionCallback callback) {
+ if (itemId == null) {
+ throw new IllegalArgumentException("itemId must not be null");
+ }
+ throwIfQueuingNotSupported();
+ throwIfNoCurrentSession();
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_REMOVE);
+ performItemAction(intent, mSessionId, itemId, extras, callback);
+ }
+
+ /**
+ * Sends a request to pause media playback.
+ * <p>
+ * The request is issued in the current session. If playback is already paused
+ * then the request has no effect.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_PAUSE ACTION_PAUSE} for
+ * more information about the semantics of this request.
+ * </p>
+ *
+ * @param extras A bundle of extra arguments to be added to the
+ * {@link MediaControlIntent#ACTION_PAUSE} intent, or null if none.
+ * @param callback A callback to invoke when the request has been
+ * processed, or null if none.
+ *
+ * @throws IllegalStateException if there is no current session.
+ *
+ * @see MediaControlIntent#ACTION_PAUSE
+ * @see #isRemotePlaybackSupported
+ */
+ public void pause(Bundle extras, SessionActionCallback callback) {
+ throwIfNoCurrentSession();
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_PAUSE);
+ performSessionAction(intent, mSessionId, extras, callback);
+ }
+
+ /**
+ * Sends a request to resume (unpause) media playback.
+ * <p>
+ * The request is issued in the current session. If playback is not paused
+ * then the request has no effect.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_RESUME ACTION_RESUME} for
+ * more information about the semantics of this request.
+ * </p>
+ *
+ * @param extras A bundle of extra arguments to be added to the
+ * {@link MediaControlIntent#ACTION_RESUME} intent, or null if none.
+ * @param callback A callback to invoke when the request has been
+ * processed, or null if none.
+ *
+ * @throws IllegalStateException if there is no current session.
+ *
+ * @see MediaControlIntent#ACTION_RESUME
+ * @see #isRemotePlaybackSupported
+ */
+ public void resume(Bundle extras, SessionActionCallback callback) {
+ throwIfNoCurrentSession();
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_RESUME);
+ performSessionAction(intent, mSessionId, extras, callback);
+ }
+
+ /**
+ * Sends a request to stop media playback and clear the media playback queue.
+ * <p>
+ * The request is issued in the current session. If the queue is already
+ * empty then the request has no effect.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_STOP ACTION_STOP} for
+ * more information about the semantics of this request.
+ * </p>
+ *
+ * @param extras A bundle of extra arguments to be added to the
+ * {@link MediaControlIntent#ACTION_STOP} intent, or null if none.
+ * @param callback A callback to invoke when the request has been
+ * processed, or null if none.
+ *
+ * @throws IllegalStateException if there is no current session.
+ *
+ * @see MediaControlIntent#ACTION_STOP
+ * @see #isRemotePlaybackSupported
+ */
+ public void stop(Bundle extras, SessionActionCallback callback) {
+ throwIfNoCurrentSession();
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_STOP);
+ performSessionAction(intent, mSessionId, extras, callback);
+ }
+
+ /**
+ * Sends a request to start a new media playback session.
+ * <p>
+ * The application must wait for the callback to indicate that this request
+ * is complete before issuing other requests that affect the session. If this
+ * request is successful then the previous session will be invalidated.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_START_SESSION ACTION_START_SESSION}
+ * for more information about the semantics of this request.
+ * </p>
+ *
+ * @param extras A bundle of extra arguments to be added to the
+ * {@link MediaControlIntent#ACTION_START_SESSION} intent, or null if none.
+ * @param callback A callback to invoke when the request has been
+ * processed, or null if none.
+ *
+ * @throws UnsupportedOperationException if the route does not support session management.
+ *
+ * @see MediaControlIntent#ACTION_START_SESSION
+ * @see #isRemotePlaybackSupported
+ * @see #isSessionManagementSupported
+ */
+ public void startSession(Bundle extras, SessionActionCallback callback) {
+ throwIfSessionManagementNotSupported();
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_START_SESSION);
+ intent.putExtra(MediaControlIntent.EXTRA_SESSION_STATUS_UPDATE_RECEIVER,
+ mSessionStatusPendingIntent);
+ if (mRouteSupportsMessaging) {
+ intent.putExtra(MediaControlIntent.EXTRA_MESSAGE_RECEIVER, mMessagePendingIntent);
+ }
+ performSessionAction(intent, null, extras, callback);
+ }
+
+ /**
+ * Sends a message.
+ * <p>
+ * The request is issued in the current session.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_SEND_MESSAGE} for
+ * more information about the semantics of this request.
+ * </p>
+ *
+ * @param message A bundle message denoting {@link MediaControlIntent#EXTRA_MESSAGE}.
+ * @param callback A callback to invoke when the request has been processed, or null if none.
+ *
+ * @throws IllegalStateException if there is no current session.
+ * @throws UnsupportedOperationException if the route does not support messages.
+ *
+ * @see MediaControlIntent#ACTION_SEND_MESSAGE
+ * @see #isMessagingSupported
+ */
+ public void sendMessage(Bundle message, SessionActionCallback callback) {
+ throwIfNoCurrentSession();
+ throwIfMessageNotSupported();
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_SEND_MESSAGE);
+ performSessionAction(intent, mSessionId, message, callback);
+ }
+
+ /**
+ * Sends a request to get the status of the media playback session.
+ * <p>
+ * The request is issued in the current session.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_GET_SESSION_STATUS
+ * ACTION_GET_SESSION_STATUS} for more information about the semantics of this request.
+ * </p>
+ *
+ * @param extras A bundle of extra arguments to be added to the
+ * {@link MediaControlIntent#ACTION_GET_SESSION_STATUS} intent, or null if none.
+ * @param callback A callback to invoke when the request has been
+ * processed, or null if none.
+ *
+ * @throws IllegalStateException if there is no current session.
+ * @throws UnsupportedOperationException if the route does not support session management.
+ *
+ * @see MediaControlIntent#ACTION_GET_SESSION_STATUS
+ * @see #isRemotePlaybackSupported
+ * @see #isSessionManagementSupported
+ */
+ public void getSessionStatus(Bundle extras, SessionActionCallback callback) {
+ throwIfSessionManagementNotSupported();
+ throwIfNoCurrentSession();
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_GET_SESSION_STATUS);
+ performSessionAction(intent, mSessionId, extras, callback);
+ }
+
+ /**
+ * Sends a request to end the media playback session.
+ * <p>
+ * The request is issued in the current session. If this request is successful,
+ * the {@link #getSessionId session id property} will be set to null after
+ * the callback is invoked.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_END_SESSION ACTION_END_SESSION}
+ * for more information about the semantics of this request.
+ * </p>
+ *
+ * @param extras A bundle of extra arguments to be added to the
+ * {@link MediaControlIntent#ACTION_END_SESSION} intent, or null if none.
+ * @param callback A callback to invoke when the request has been
+ * processed, or null if none.
+ *
+ * @throws IllegalStateException if there is no current session.
+ * @throws UnsupportedOperationException if the route does not support session management.
+ *
+ * @see MediaControlIntent#ACTION_END_SESSION
+ * @see #isRemotePlaybackSupported
+ * @see #isSessionManagementSupported
+ */
+ public void endSession(Bundle extras, SessionActionCallback callback) {
+ throwIfSessionManagementNotSupported();
+ throwIfNoCurrentSession();
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_END_SESSION);
+ performSessionAction(intent, mSessionId, extras, callback);
+ }
+
+ private void performItemAction(final Intent intent,
+ final String sessionId, final String itemId,
+ Bundle extras, final ItemActionCallback callback) {
+ intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ if (sessionId != null) {
+ intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, sessionId);
+ }
+ if (itemId != null) {
+ intent.putExtra(MediaControlIntent.EXTRA_ITEM_ID, itemId);
+ }
+ if (extras != null) {
+ intent.putExtras(extras);
+ }
+ logRequest(intent);
+ mRoute.sendControlRequest(intent, new MediaRouter.ControlRequestCallback() {
+ @Override
+ public void onResult(Bundle data) {
+ if (data != null) {
+ String sessionIdResult = inferMissingResult(sessionId,
+ data.getString(MediaControlIntent.EXTRA_SESSION_ID));
+ MediaSessionStatus sessionStatus = MediaSessionStatus.fromBundle(
+ data.getBundle(MediaControlIntent.EXTRA_SESSION_STATUS));
+ String itemIdResult = inferMissingResult(itemId,
+ data.getString(MediaControlIntent.EXTRA_ITEM_ID));
+ MediaItemStatus itemStatus = MediaItemStatus.fromBundle(
+ data.getBundle(MediaControlIntent.EXTRA_ITEM_STATUS));
+ adoptSession(sessionIdResult);
+ if (sessionIdResult != null && itemIdResult != null && itemStatus != null) {
+ if (DEBUG) {
+ Log.d(TAG, "Received result from " + intent.getAction()
+ + ": data=" + bundleToString(data)
+ + ", sessionId=" + sessionIdResult
+ + ", sessionStatus=" + sessionStatus
+ + ", itemId=" + itemIdResult
+ + ", itemStatus=" + itemStatus);
+ }
+ callback.onResult(data, sessionIdResult, sessionStatus,
+ itemIdResult, itemStatus);
+ return;
+ }
+ }
+ handleInvalidResult(intent, callback, data);
+ }
+
+ @Override
+ public void onError(String error, Bundle data) {
+ handleError(intent, callback, error, data);
+ }
+ });
+ }
+
+ private void performSessionAction(final Intent intent, final String sessionId,
+ Bundle extras, final SessionActionCallback callback) {
+ intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ if (sessionId != null) {
+ intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, sessionId);
+ }
+ if (extras != null) {
+ intent.putExtras(extras);
+ }
+ logRequest(intent);
+ mRoute.sendControlRequest(intent, new MediaRouter.ControlRequestCallback() {
+ @Override
+ public void onResult(Bundle data) {
+ if (data != null) {
+ String sessionIdResult = inferMissingResult(sessionId,
+ data.getString(MediaControlIntent.EXTRA_SESSION_ID));
+ MediaSessionStatus sessionStatus = MediaSessionStatus.fromBundle(
+ data.getBundle(MediaControlIntent.EXTRA_SESSION_STATUS));
+ adoptSession(sessionIdResult);
+ if (sessionIdResult != null) {
+ if (DEBUG) {
+ Log.d(TAG, "Received result from " + intent.getAction()
+ + ": data=" + bundleToString(data)
+ + ", sessionId=" + sessionIdResult
+ + ", sessionStatus=" + sessionStatus);
+ }
+ try {
+ callback.onResult(data, sessionIdResult, sessionStatus);
+ } finally {
+ if (intent.getAction().equals(MediaControlIntent.ACTION_END_SESSION)
+ && sessionIdResult.equals(mSessionId)) {
+ setSessionId(null);
+ }
+ }
+ return;
+ }
+ }
+ handleInvalidResult(intent, callback, data);
+ }
+
+ @Override
+ public void onError(String error, Bundle data) {
+ handleError(intent, callback, error, data);
+ }
+ });
+ }
+
+ void adoptSession(String sessionId) {
+ if (sessionId != null) {
+ setSessionId(sessionId);
+ }
+ }
+
+ void handleInvalidResult(Intent intent, ActionCallback callback,
+ Bundle data) {
+ Log.w(TAG, "Received invalid result data from " + intent.getAction()
+ + ": data=" + bundleToString(data));
+ callback.onError(null, MediaControlIntent.ERROR_UNKNOWN, data);
+ }
+
+ void handleError(Intent intent, ActionCallback callback,
+ String error, Bundle data) {
+ final int code;
+ if (data != null) {
+ code = data.getInt(MediaControlIntent.EXTRA_ERROR_CODE,
+ MediaControlIntent.ERROR_UNKNOWN);
+ } else {
+ code = MediaControlIntent.ERROR_UNKNOWN;
+ }
+ if (DEBUG) {
+ Log.w(TAG, "Received error from " + intent.getAction()
+ + ": error=" + error
+ + ", code=" + code
+ + ", data=" + bundleToString(data));
+ }
+ callback.onError(error, code, data);
+ }
+
+ private void detectFeatures() {
+ mRouteSupportsRemotePlayback = routeSupportsAction(MediaControlIntent.ACTION_PLAY)
+ && routeSupportsAction(MediaControlIntent.ACTION_SEEK)
+ && routeSupportsAction(MediaControlIntent.ACTION_GET_STATUS)
+ && routeSupportsAction(MediaControlIntent.ACTION_PAUSE)
+ && routeSupportsAction(MediaControlIntent.ACTION_RESUME)
+ && routeSupportsAction(MediaControlIntent.ACTION_STOP);
+ mRouteSupportsQueuing = mRouteSupportsRemotePlayback
+ && routeSupportsAction(MediaControlIntent.ACTION_ENQUEUE)
+ && routeSupportsAction(MediaControlIntent.ACTION_REMOVE);
+ mRouteSupportsSessionManagement = mRouteSupportsRemotePlayback
+ && routeSupportsAction(MediaControlIntent.ACTION_START_SESSION)
+ && routeSupportsAction(MediaControlIntent.ACTION_GET_SESSION_STATUS)
+ && routeSupportsAction(MediaControlIntent.ACTION_END_SESSION);
+ mRouteSupportsMessaging = doesRouteSupportMessaging();
+ }
+
+ private boolean routeSupportsAction(String action) {
+ return mRoute.supportsControlAction(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK, action);
+ }
+
+ private boolean doesRouteSupportMessaging() {
+ for (IntentFilter filter : mRoute.getControlFilters()) {
+ if (filter.hasAction(MediaControlIntent.ACTION_SEND_MESSAGE)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void throwIfRemotePlaybackNotSupported() {
+ if (!mRouteSupportsRemotePlayback) {
+ throw new UnsupportedOperationException("The route does not support remote playback.");
+ }
+ }
+
+ private void throwIfQueuingNotSupported() {
+ if (!mRouteSupportsQueuing) {
+ throw new UnsupportedOperationException("The route does not support queuing.");
+ }
+ }
+
+ private void throwIfSessionManagementNotSupported() {
+ if (!mRouteSupportsSessionManagement) {
+ throw new UnsupportedOperationException("The route does not support "
+ + "session management.");
+ }
+ }
+
+ private void throwIfMessageNotSupported() {
+ if (!mRouteSupportsMessaging) {
+ throw new UnsupportedOperationException("The route does not support message.");
+ }
+ }
+
+ private void throwIfNoCurrentSession() {
+ if (mSessionId == null) {
+ throw new IllegalStateException("There is no current session.");
+ }
+ }
+
+ static String inferMissingResult(String request, String result) {
+ if (result == null) {
+ // Result is missing.
+ return request;
+ }
+ if (request == null || request.equals(result)) {
+ // Request didn't specify a value or result matches request.
+ return result;
+ }
+ // Result conflicts with request.
+ return null;
+ }
+
+ private static void logRequest(Intent intent) {
+ if (DEBUG) {
+ Log.d(TAG, "Sending request: " + intent);
+ }
+ }
+
+ static String bundleToString(Bundle bundle) {
+ if (bundle != null) {
+ bundle.size(); // force bundle to be unparcelled
+ return bundle.toString();
+ }
+ return "null";
+ }
+
+ private final class ActionReceiver extends BroadcastReceiver {
+ public static final String ACTION_ITEM_STATUS_CHANGED =
+ "android.support.v7.media.actions.ACTION_ITEM_STATUS_CHANGED";
+ public static final String ACTION_SESSION_STATUS_CHANGED =
+ "android.support.v7.media.actions.ACTION_SESSION_STATUS_CHANGED";
+ public static final String ACTION_MESSAGE_RECEIVED =
+ "android.support.v7.media.actions.ACTION_MESSAGE_RECEIVED";
+
+ ActionReceiver() {
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String sessionId = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+ if (sessionId == null || !sessionId.equals(mSessionId)) {
+ Log.w(TAG, "Discarding spurious status callback "
+ + "with missing or invalid session id: sessionId=" + sessionId);
+ return;
+ }
+
+ MediaSessionStatus sessionStatus = MediaSessionStatus.fromBundle(
+ intent.getBundleExtra(MediaControlIntent.EXTRA_SESSION_STATUS));
+ String action = intent.getAction();
+ if (action.equals(ACTION_ITEM_STATUS_CHANGED)) {
+ String itemId = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
+ if (itemId == null) {
+ Log.w(TAG, "Discarding spurious status callback with missing item id.");
+ return;
+ }
+
+ MediaItemStatus itemStatus = MediaItemStatus.fromBundle(
+ intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_STATUS));
+ if (itemStatus == null) {
+ Log.w(TAG, "Discarding spurious status callback with missing item status.");
+ return;
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "Received item status callback: sessionId=" + sessionId
+ + ", sessionStatus=" + sessionStatus
+ + ", itemId=" + itemId
+ + ", itemStatus=" + itemStatus);
+ }
+
+ if (mStatusCallback != null) {
+ mStatusCallback.onItemStatusChanged(intent.getExtras(),
+ sessionId, sessionStatus, itemId, itemStatus);
+ }
+ } else if (action.equals(ACTION_SESSION_STATUS_CHANGED)) {
+ if (sessionStatus == null) {
+ Log.w(TAG, "Discarding spurious media status callback with "
+ +"missing session status.");
+ return;
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "Received session status callback: sessionId=" + sessionId
+ + ", sessionStatus=" + sessionStatus);
+ }
+
+ if (mStatusCallback != null) {
+ mStatusCallback.onSessionStatusChanged(intent.getExtras(),
+ sessionId, sessionStatus);
+ }
+ } else if (action.equals(ACTION_MESSAGE_RECEIVED)) {
+ if (DEBUG) {
+ Log.d(TAG, "Received message callback: sessionId=" + sessionId);
+ }
+
+ if (mOnMessageReceivedListener != null) {
+ mOnMessageReceivedListener.onMessageReceived(sessionId,
+ intent.getBundleExtra(MediaControlIntent.EXTRA_MESSAGE));
+ }
+ }
+ }
+ }
+
+ /**
+ * A callback that will receive media status updates.
+ */
+ public static abstract class StatusCallback {
+ /**
+ * Called when the status of a media item changes.
+ *
+ * @param data The result data bundle.
+ * @param sessionId The session id.
+ * @param sessionStatus The session status, or null if unknown.
+ * @param itemId The item id.
+ * @param itemStatus The item status.
+ */
+ public void onItemStatusChanged(Bundle data,
+ String sessionId, MediaSessionStatus sessionStatus,
+ String itemId, MediaItemStatus itemStatus) {
+ }
+
+ /**
+ * Called when the status of a media session changes.
+ *
+ * @param data The result data bundle.
+ * @param sessionId The session id.
+ * @param sessionStatus The session status, or null if unknown.
+ */
+ public void onSessionStatusChanged(Bundle data,
+ String sessionId, MediaSessionStatus sessionStatus) {
+ }
+
+ /**
+ * Called when the session of the remote playback client changes.
+ *
+ * @param sessionId The new session id.
+ */
+ public void onSessionChanged(String sessionId) {
+ }
+ }
+
+ /**
+ * Base callback type for remote playback requests.
+ */
+ public static abstract class ActionCallback {
+ /**
+ * Called when a media control request fails.
+ *
+ * @param error A localized error message which may be shown to the user, or null
+ * if the cause of the error is unclear.
+ * @param code The error code, or {@link MediaControlIntent#ERROR_UNKNOWN} if unknown.
+ * @param data The error data bundle, or null if none.
+ */
+ public void onError(String error, int code, Bundle data) {
+ }
+ }
+
+ /**
+ * Callback for remote playback requests that operate on items.
+ */
+ public static abstract class ItemActionCallback extends ActionCallback {
+ /**
+ * Called when the request succeeds.
+ *
+ * @param data The result data bundle.
+ * @param sessionId The session id.
+ * @param sessionStatus The session status, or null if unknown.
+ * @param itemId The item id.
+ * @param itemStatus The item status.
+ */
+ public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
+ String itemId, MediaItemStatus itemStatus) {
+ }
+ }
+
+ /**
+ * Callback for remote playback requests that operate on sessions.
+ */
+ public static abstract class SessionActionCallback extends ActionCallback {
+ /**
+ * Called when the request succeeds.
+ *
+ * @param data The result data bundle.
+ * @param sessionId The session id.
+ * @param sessionStatus The session status, or null if unknown.
+ */
+ public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
+ }
+ }
+
+ /**
+ * A callback that will receive messages from media sessions.
+ */
+ public interface OnMessageReceivedListener {
+ /**
+ * Called when a message received.
+ *
+ * @param sessionId The session id.
+ * @param message A bundle message denoting {@link MediaControlIntent#EXTRA_MESSAGE}.
+ */
+ void onMessageReceived(String sessionId, Bundle message);
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/media/SystemMediaRouteProvider.java b/packages/MediaComponents/src/com/android/support/mediarouter/media/SystemMediaRouteProvider.java
new file mode 100644
index 0000000..f5e1e61
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/media/SystemMediaRouteProvider.java
@@ -0,0 +1,882 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.mediarouter.media;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.media.AudioManager;
+import android.os.Build;
+import android.support.annotation.RequiresApi;
+import android.view.Display;
+
+import com.android.media.update.R;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Provides routes for built-in system destinations such as the local display
+ * and speaker. On Jellybean and newer platform releases, queries the framework
+ * MediaRouter for framework-provided routes and registers non-framework-provided
+ * routes as user routes.
+ */
+abstract class SystemMediaRouteProvider extends MediaRouteProvider {
+ private static final String TAG = "SystemMediaRouteProvider";
+
+ public static final String PACKAGE_NAME = "android";
+ public static final String DEFAULT_ROUTE_ID = "DEFAULT_ROUTE";
+
+ protected SystemMediaRouteProvider(Context context) {
+ super(context, new ProviderMetadata(new ComponentName(PACKAGE_NAME,
+ SystemMediaRouteProvider.class.getName())));
+ }
+
+ public static SystemMediaRouteProvider obtain(Context context, SyncCallback syncCallback) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ return new Api24Impl(context, syncCallback);
+ }
+ if (Build.VERSION.SDK_INT >= 18) {
+ return new JellybeanMr2Impl(context, syncCallback);
+ }
+ if (Build.VERSION.SDK_INT >= 17) {
+ return new JellybeanMr1Impl(context, syncCallback);
+ }
+ if (Build.VERSION.SDK_INT >= 16) {
+ return new JellybeanImpl(context, syncCallback);
+ }
+ return new LegacyImpl(context);
+ }
+
+ /**
+ * Called by the media router when a route is added to synchronize state with
+ * the framework media router.
+ */
+ public void onSyncRouteAdded(MediaRouter.RouteInfo route) {
+ }
+
+ /**
+ * Called by the media router when a route is removed to synchronize state with
+ * the framework media router.
+ */
+ public void onSyncRouteRemoved(MediaRouter.RouteInfo route) {
+ }
+
+ /**
+ * Called by the media router when a route is changed to synchronize state with
+ * the framework media router.
+ */
+ public void onSyncRouteChanged(MediaRouter.RouteInfo route) {
+ }
+
+ /**
+ * Called by the media router when a route is selected to synchronize state with
+ * the framework media router.
+ */
+ public void onSyncRouteSelected(MediaRouter.RouteInfo route) {
+ }
+
+ /**
+ * Callbacks into the media router to synchronize state with the framework media router.
+ */
+ public interface SyncCallback {
+ void onSystemRouteSelectedByDescriptorId(String id);
+ }
+
+ protected Object getDefaultRoute() {
+ return null;
+ }
+
+ protected Object getSystemRoute(MediaRouter.RouteInfo route) {
+ return null;
+ }
+
+ /**
+ * Legacy implementation for platform versions prior to Jellybean.
+ */
+ static class LegacyImpl extends SystemMediaRouteProvider {
+ static final int PLAYBACK_STREAM = AudioManager.STREAM_MUSIC;
+
+ private static final ArrayList<IntentFilter> CONTROL_FILTERS;
+ static {
+ IntentFilter f = new IntentFilter();
+ f.addCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
+ f.addCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
+
+ CONTROL_FILTERS = new ArrayList<IntentFilter>();
+ CONTROL_FILTERS.add(f);
+ }
+
+ final AudioManager mAudioManager;
+ private final VolumeChangeReceiver mVolumeChangeReceiver;
+ int mLastReportedVolume = -1;
+
+ public LegacyImpl(Context context) {
+ super(context);
+ mAudioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
+ mVolumeChangeReceiver = new VolumeChangeReceiver();
+
+ context.registerReceiver(mVolumeChangeReceiver,
+ new IntentFilter(VolumeChangeReceiver.VOLUME_CHANGED_ACTION));
+ publishRoutes();
+ }
+
+ void publishRoutes() {
+ Resources r = getContext().getResources();
+ int maxVolume = mAudioManager.getStreamMaxVolume(PLAYBACK_STREAM);
+ mLastReportedVolume = mAudioManager.getStreamVolume(PLAYBACK_STREAM);
+ MediaRouteDescriptor defaultRoute = new MediaRouteDescriptor.Builder(
+ DEFAULT_ROUTE_ID, r.getString(R.string.mr_system_route_name))
+ .addControlFilters(CONTROL_FILTERS)
+ .setPlaybackStream(PLAYBACK_STREAM)
+ .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_LOCAL)
+ .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE)
+ .setVolumeMax(maxVolume)
+ .setVolume(mLastReportedVolume)
+ .build();
+
+ MediaRouteProviderDescriptor providerDescriptor =
+ new MediaRouteProviderDescriptor.Builder()
+ .addRoute(defaultRoute)
+ .build();
+ setDescriptor(providerDescriptor);
+ }
+
+ @Override
+ public RouteController onCreateRouteController(String routeId) {
+ if (routeId.equals(DEFAULT_ROUTE_ID)) {
+ return new DefaultRouteController();
+ }
+ return null;
+ }
+
+ final class DefaultRouteController extends RouteController {
+ @Override
+ public void onSetVolume(int volume) {
+ mAudioManager.setStreamVolume(PLAYBACK_STREAM, volume, 0);
+ publishRoutes();
+ }
+
+ @Override
+ public void onUpdateVolume(int delta) {
+ int volume = mAudioManager.getStreamVolume(PLAYBACK_STREAM);
+ int maxVolume = mAudioManager.getStreamMaxVolume(PLAYBACK_STREAM);
+ int newVolume = Math.min(maxVolume, Math.max(0, volume + delta));
+ if (newVolume != volume) {
+ mAudioManager.setStreamVolume(PLAYBACK_STREAM, volume, 0);
+ }
+ publishRoutes();
+ }
+ }
+
+ final class VolumeChangeReceiver extends BroadcastReceiver {
+ // These constants come from AudioManager.
+ public static final String VOLUME_CHANGED_ACTION =
+ "android.media.VOLUME_CHANGED_ACTION";
+ public static final String EXTRA_VOLUME_STREAM_TYPE =
+ "android.media.EXTRA_VOLUME_STREAM_TYPE";
+ public static final String EXTRA_VOLUME_STREAM_VALUE =
+ "android.media.EXTRA_VOLUME_STREAM_VALUE";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(VOLUME_CHANGED_ACTION)) {
+ final int streamType = intent.getIntExtra(EXTRA_VOLUME_STREAM_TYPE, -1);
+ if (streamType == PLAYBACK_STREAM) {
+ final int volume = intent.getIntExtra(EXTRA_VOLUME_STREAM_VALUE, -1);
+ if (volume >= 0 && volume != mLastReportedVolume) {
+ publishRoutes();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Jellybean implementation.
+ */
+ // @@RequiresApi(16)
+ static class JellybeanImpl extends SystemMediaRouteProvider
+ implements MediaRouterJellybean.Callback, MediaRouterJellybean.VolumeCallback {
+ private static final ArrayList<IntentFilter> LIVE_AUDIO_CONTROL_FILTERS;
+ static {
+ IntentFilter f = new IntentFilter();
+ f.addCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
+
+ LIVE_AUDIO_CONTROL_FILTERS = new ArrayList<IntentFilter>();
+ LIVE_AUDIO_CONTROL_FILTERS.add(f);
+ }
+
+ private static final ArrayList<IntentFilter> LIVE_VIDEO_CONTROL_FILTERS;
+ static {
+ IntentFilter f = new IntentFilter();
+ f.addCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
+
+ LIVE_VIDEO_CONTROL_FILTERS = new ArrayList<IntentFilter>();
+ LIVE_VIDEO_CONTROL_FILTERS.add(f);
+ }
+
+ private final SyncCallback mSyncCallback;
+
+ protected final Object mRouterObj;
+ protected final Object mCallbackObj;
+ protected final Object mVolumeCallbackObj;
+ protected final Object mUserRouteCategoryObj;
+ protected int mRouteTypes;
+ protected boolean mActiveScan;
+ protected boolean mCallbackRegistered;
+
+ // Maintains an association from framework routes to support library routes.
+ // Note that we cannot use the tag field for this because an application may
+ // have published its own user routes to the framework media router and already
+ // used the tag for its own purposes.
+ protected final ArrayList<SystemRouteRecord> mSystemRouteRecords =
+ new ArrayList<SystemRouteRecord>();
+
+ // Maintains an association from support library routes to framework routes.
+ protected final ArrayList<UserRouteRecord> mUserRouteRecords =
+ new ArrayList<UserRouteRecord>();
+
+ private MediaRouterJellybean.SelectRouteWorkaround mSelectRouteWorkaround;
+ private MediaRouterJellybean.GetDefaultRouteWorkaround mGetDefaultRouteWorkaround;
+
+ public JellybeanImpl(Context context, SyncCallback syncCallback) {
+ super(context);
+ mSyncCallback = syncCallback;
+ mRouterObj = MediaRouterJellybean.getMediaRouter(context);
+ mCallbackObj = createCallbackObj();
+ mVolumeCallbackObj = createVolumeCallbackObj();
+
+ Resources r = context.getResources();
+ mUserRouteCategoryObj = MediaRouterJellybean.createRouteCategory(
+ mRouterObj, r.getString(R.string.mr_user_route_category_name), false);
+
+ updateSystemRoutes();
+ }
+
+ @Override
+ public RouteController onCreateRouteController(String routeId) {
+ int index = findSystemRouteRecordByDescriptorId(routeId);
+ if (index >= 0) {
+ SystemRouteRecord record = mSystemRouteRecords.get(index);
+ return new SystemRouteController(record.mRouteObj);
+ }
+ return null;
+ }
+
+ @Override
+ public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) {
+ int newRouteTypes = 0;
+ boolean newActiveScan = false;
+ if (request != null) {
+ final MediaRouteSelector selector = request.getSelector();
+ final List<String> categories = selector.getControlCategories();
+ final int count = categories.size();
+ for (int i = 0; i < count; i++) {
+ String category = categories.get(i);
+ if (category.equals(MediaControlIntent.CATEGORY_LIVE_AUDIO)) {
+ newRouteTypes |= MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO;
+ } else if (category.equals(MediaControlIntent.CATEGORY_LIVE_VIDEO)) {
+ newRouteTypes |= MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO;
+ } else {
+ newRouteTypes |= MediaRouterJellybean.ROUTE_TYPE_USER;
+ }
+ }
+ newActiveScan = request.isActiveScan();
+ }
+
+ if (mRouteTypes != newRouteTypes || mActiveScan != newActiveScan) {
+ mRouteTypes = newRouteTypes;
+ mActiveScan = newActiveScan;
+ updateSystemRoutes();
+ }
+ }
+
+ @Override
+ public void onRouteAdded(Object routeObj) {
+ if (addSystemRouteNoPublish(routeObj)) {
+ publishRoutes();
+ }
+ }
+
+ private void updateSystemRoutes() {
+ updateCallback();
+ boolean changed = false;
+ for (Object routeObj : MediaRouterJellybean.getRoutes(mRouterObj)) {
+ changed |= addSystemRouteNoPublish(routeObj);
+ }
+ if (changed) {
+ publishRoutes();
+ }
+ }
+
+ private boolean addSystemRouteNoPublish(Object routeObj) {
+ if (getUserRouteRecord(routeObj) == null
+ && findSystemRouteRecord(routeObj) < 0) {
+ String id = assignRouteId(routeObj);
+ SystemRouteRecord record = new SystemRouteRecord(routeObj, id);
+ updateSystemRouteDescriptor(record);
+ mSystemRouteRecords.add(record);
+ return true;
+ }
+ return false;
+ }
+
+ private String assignRouteId(Object routeObj) {
+ // TODO: The framework media router should supply a unique route id that
+ // we can use here. For now we use a hash of the route name and take care
+ // to dedupe it.
+ boolean isDefault = (getDefaultRoute() == routeObj);
+ String id = isDefault ? DEFAULT_ROUTE_ID :
+ String.format(Locale.US, "ROUTE_%08x", getRouteName(routeObj).hashCode());
+ if (findSystemRouteRecordByDescriptorId(id) < 0) {
+ return id;
+ }
+ for (int i = 2; ; i++) {
+ String newId = String.format(Locale.US, "%s_%d", id, i);
+ if (findSystemRouteRecordByDescriptorId(newId) < 0) {
+ return newId;
+ }
+ }
+ }
+
+ @Override
+ public void onRouteRemoved(Object routeObj) {
+ if (getUserRouteRecord(routeObj) == null) {
+ int index = findSystemRouteRecord(routeObj);
+ if (index >= 0) {
+ mSystemRouteRecords.remove(index);
+ publishRoutes();
+ }
+ }
+ }
+
+ @Override
+ public void onRouteChanged(Object routeObj) {
+ if (getUserRouteRecord(routeObj) == null) {
+ int index = findSystemRouteRecord(routeObj);
+ if (index >= 0) {
+ SystemRouteRecord record = mSystemRouteRecords.get(index);
+ updateSystemRouteDescriptor(record);
+ publishRoutes();
+ }
+ }
+ }
+
+ @Override
+ public void onRouteVolumeChanged(Object routeObj) {
+ if (getUserRouteRecord(routeObj) == null) {
+ int index = findSystemRouteRecord(routeObj);
+ if (index >= 0) {
+ SystemRouteRecord record = mSystemRouteRecords.get(index);
+ int newVolume = MediaRouterJellybean.RouteInfo.getVolume(routeObj);
+ if (newVolume != record.mRouteDescriptor.getVolume()) {
+ record.mRouteDescriptor =
+ new MediaRouteDescriptor.Builder(record.mRouteDescriptor)
+ .setVolume(newVolume)
+ .build();
+ publishRoutes();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onRouteSelected(int type, Object routeObj) {
+ if (routeObj != MediaRouterJellybean.getSelectedRoute(mRouterObj,
+ MediaRouterJellybean.ALL_ROUTE_TYPES)) {
+ // The currently selected route has already changed so this callback
+ // is stale. Drop it to prevent getting into sync loops.
+ return;
+ }
+
+ UserRouteRecord userRouteRecord = getUserRouteRecord(routeObj);
+ if (userRouteRecord != null) {
+ userRouteRecord.mRoute.select();
+ } else {
+ // Select the route if it already exists in the compat media router.
+ // If not, we will select it instead when the route is added.
+ int index = findSystemRouteRecord(routeObj);
+ if (index >= 0) {
+ SystemRouteRecord record = mSystemRouteRecords.get(index);
+ mSyncCallback.onSystemRouteSelectedByDescriptorId(record.mRouteDescriptorId);
+ }
+ }
+ }
+
+ @Override
+ public void onRouteUnselected(int type, Object routeObj) {
+ // Nothing to do when a route is unselected.
+ // We only need to handle when a route is selected.
+ }
+
+ @Override
+ public void onRouteGrouped(Object routeObj, Object groupObj, int index) {
+ // Route grouping is deprecated and no longer supported.
+ }
+
+ @Override
+ public void onRouteUngrouped(Object routeObj, Object groupObj) {
+ // Route grouping is deprecated and no longer supported.
+ }
+
+ @Override
+ public void onVolumeSetRequest(Object routeObj, int volume) {
+ UserRouteRecord record = getUserRouteRecord(routeObj);
+ if (record != null) {
+ record.mRoute.requestSetVolume(volume);
+ }
+ }
+
+ @Override
+ public void onVolumeUpdateRequest(Object routeObj, int direction) {
+ UserRouteRecord record = getUserRouteRecord(routeObj);
+ if (record != null) {
+ record.mRoute.requestUpdateVolume(direction);
+ }
+ }
+
+ @Override
+ public void onSyncRouteAdded(MediaRouter.RouteInfo route) {
+ if (route.getProviderInstance() != this) {
+ Object routeObj = MediaRouterJellybean.createUserRoute(
+ mRouterObj, mUserRouteCategoryObj);
+ UserRouteRecord record = new UserRouteRecord(route, routeObj);
+ MediaRouterJellybean.RouteInfo.setTag(routeObj, record);
+ MediaRouterJellybean.UserRouteInfo.setVolumeCallback(routeObj, mVolumeCallbackObj);
+ updateUserRouteProperties(record);
+ mUserRouteRecords.add(record);
+ MediaRouterJellybean.addUserRoute(mRouterObj, routeObj);
+ } else {
+ // If the newly added route is the counterpart of the currently selected
+ // route in the framework media router then ensure it is selected in
+ // the compat media router.
+ Object routeObj = MediaRouterJellybean.getSelectedRoute(
+ mRouterObj, MediaRouterJellybean.ALL_ROUTE_TYPES);
+ int index = findSystemRouteRecord(routeObj);
+ if (index >= 0) {
+ SystemRouteRecord record = mSystemRouteRecords.get(index);
+ if (record.mRouteDescriptorId.equals(route.getDescriptorId())) {
+ route.select();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onSyncRouteRemoved(MediaRouter.RouteInfo route) {
+ if (route.getProviderInstance() != this) {
+ int index = findUserRouteRecord(route);
+ if (index >= 0) {
+ UserRouteRecord record = mUserRouteRecords.remove(index);
+ MediaRouterJellybean.RouteInfo.setTag(record.mRouteObj, null);
+ MediaRouterJellybean.UserRouteInfo.setVolumeCallback(record.mRouteObj, null);
+ MediaRouterJellybean.removeUserRoute(mRouterObj, record.mRouteObj);
+ }
+ }
+ }
+
+ @Override
+ public void onSyncRouteChanged(MediaRouter.RouteInfo route) {
+ if (route.getProviderInstance() != this) {
+ int index = findUserRouteRecord(route);
+ if (index >= 0) {
+ UserRouteRecord record = mUserRouteRecords.get(index);
+ updateUserRouteProperties(record);
+ }
+ }
+ }
+
+ @Override
+ public void onSyncRouteSelected(MediaRouter.RouteInfo route) {
+ if (!route.isSelected()) {
+ // The currently selected route has already changed so this callback
+ // is stale. Drop it to prevent getting into sync loops.
+ return;
+ }
+
+ if (route.getProviderInstance() != this) {
+ int index = findUserRouteRecord(route);
+ if (index >= 0) {
+ UserRouteRecord record = mUserRouteRecords.get(index);
+ selectRoute(record.mRouteObj);
+ }
+ } else {
+ int index = findSystemRouteRecordByDescriptorId(route.getDescriptorId());
+ if (index >= 0) {
+ SystemRouteRecord record = mSystemRouteRecords.get(index);
+ selectRoute(record.mRouteObj);
+ }
+ }
+ }
+
+ protected void publishRoutes() {
+ MediaRouteProviderDescriptor.Builder builder =
+ new MediaRouteProviderDescriptor.Builder();
+ int count = mSystemRouteRecords.size();
+ for (int i = 0; i < count; i++) {
+ builder.addRoute(mSystemRouteRecords.get(i).mRouteDescriptor);
+ }
+
+ setDescriptor(builder.build());
+ }
+
+ protected int findSystemRouteRecord(Object routeObj) {
+ final int count = mSystemRouteRecords.size();
+ for (int i = 0; i < count; i++) {
+ if (mSystemRouteRecords.get(i).mRouteObj == routeObj) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ protected int findSystemRouteRecordByDescriptorId(String id) {
+ final int count = mSystemRouteRecords.size();
+ for (int i = 0; i < count; i++) {
+ if (mSystemRouteRecords.get(i).mRouteDescriptorId.equals(id)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ protected int findUserRouteRecord(MediaRouter.RouteInfo route) {
+ final int count = mUserRouteRecords.size();
+ for (int i = 0; i < count; i++) {
+ if (mUserRouteRecords.get(i).mRoute == route) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ protected UserRouteRecord getUserRouteRecord(Object routeObj) {
+ Object tag = MediaRouterJellybean.RouteInfo.getTag(routeObj);
+ return tag instanceof UserRouteRecord ? (UserRouteRecord)tag : null;
+ }
+
+ protected void updateSystemRouteDescriptor(SystemRouteRecord record) {
+ // We must always recreate the route descriptor when making any changes
+ // because they are intended to be immutable once published.
+ MediaRouteDescriptor.Builder builder = new MediaRouteDescriptor.Builder(
+ record.mRouteDescriptorId, getRouteName(record.mRouteObj));
+ onBuildSystemRouteDescriptor(record, builder);
+ record.mRouteDescriptor = builder.build();
+ }
+
+ protected String getRouteName(Object routeObj) {
+ // Routes should not have null names but it may happen for badly configured
+ // user routes. We tolerate this by using an empty name string here but
+ // such unnamed routes will be discarded by the media router upstream
+ // (with a log message so we can track down the problem).
+ CharSequence name = MediaRouterJellybean.RouteInfo.getName(routeObj, getContext());
+ return name != null ? name.toString() : "";
+ }
+
+ protected void onBuildSystemRouteDescriptor(SystemRouteRecord record,
+ MediaRouteDescriptor.Builder builder) {
+ int supportedTypes = MediaRouterJellybean.RouteInfo.getSupportedTypes(
+ record.mRouteObj);
+ if ((supportedTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO) != 0) {
+ builder.addControlFilters(LIVE_AUDIO_CONTROL_FILTERS);
+ }
+ if ((supportedTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO) != 0) {
+ builder.addControlFilters(LIVE_VIDEO_CONTROL_FILTERS);
+ }
+
+ builder.setPlaybackType(
+ MediaRouterJellybean.RouteInfo.getPlaybackType(record.mRouteObj));
+ builder.setPlaybackStream(
+ MediaRouterJellybean.RouteInfo.getPlaybackStream(record.mRouteObj));
+ builder.setVolume(
+ MediaRouterJellybean.RouteInfo.getVolume(record.mRouteObj));
+ builder.setVolumeMax(
+ MediaRouterJellybean.RouteInfo.getVolumeMax(record.mRouteObj));
+ builder.setVolumeHandling(
+ MediaRouterJellybean.RouteInfo.getVolumeHandling(record.mRouteObj));
+ }
+
+ protected void updateUserRouteProperties(UserRouteRecord record) {
+ MediaRouterJellybean.UserRouteInfo.setName(
+ record.mRouteObj, record.mRoute.getName());
+ MediaRouterJellybean.UserRouteInfo.setPlaybackType(
+ record.mRouteObj, record.mRoute.getPlaybackType());
+ MediaRouterJellybean.UserRouteInfo.setPlaybackStream(
+ record.mRouteObj, record.mRoute.getPlaybackStream());
+ MediaRouterJellybean.UserRouteInfo.setVolume(
+ record.mRouteObj, record.mRoute.getVolume());
+ MediaRouterJellybean.UserRouteInfo.setVolumeMax(
+ record.mRouteObj, record.mRoute.getVolumeMax());
+ MediaRouterJellybean.UserRouteInfo.setVolumeHandling(
+ record.mRouteObj, record.mRoute.getVolumeHandling());
+ }
+
+ protected void updateCallback() {
+ if (mCallbackRegistered) {
+ mCallbackRegistered = false;
+ MediaRouterJellybean.removeCallback(mRouterObj, mCallbackObj);
+ }
+
+ if (mRouteTypes != 0) {
+ mCallbackRegistered = true;
+ MediaRouterJellybean.addCallback(mRouterObj, mRouteTypes, mCallbackObj);
+ }
+ }
+
+ protected Object createCallbackObj() {
+ return MediaRouterJellybean.createCallback(this);
+ }
+
+ protected Object createVolumeCallbackObj() {
+ return MediaRouterJellybean.createVolumeCallback(this);
+ }
+
+ protected void selectRoute(Object routeObj) {
+ if (mSelectRouteWorkaround == null) {
+ mSelectRouteWorkaround = new MediaRouterJellybean.SelectRouteWorkaround();
+ }
+ mSelectRouteWorkaround.selectRoute(mRouterObj,
+ MediaRouterJellybean.ALL_ROUTE_TYPES, routeObj);
+ }
+
+ @Override
+ protected Object getDefaultRoute() {
+ if (mGetDefaultRouteWorkaround == null) {
+ mGetDefaultRouteWorkaround = new MediaRouterJellybean.GetDefaultRouteWorkaround();
+ }
+ return mGetDefaultRouteWorkaround.getDefaultRoute(mRouterObj);
+ }
+
+ @Override
+ protected Object getSystemRoute(MediaRouter.RouteInfo route) {
+ if (route == null) {
+ return null;
+ }
+ int index = findSystemRouteRecordByDescriptorId(route.getDescriptorId());
+ if (index >= 0) {
+ return mSystemRouteRecords.get(index).mRouteObj;
+ }
+ return null;
+ }
+
+ /**
+ * Represents a route that is provided by the framework media router
+ * and published by this route provider to the support library media router.
+ */
+ protected static final class SystemRouteRecord {
+ public final Object mRouteObj;
+ public final String mRouteDescriptorId;
+ public MediaRouteDescriptor mRouteDescriptor; // assigned immediately after creation
+
+ public SystemRouteRecord(Object routeObj, String id) {
+ mRouteObj = routeObj;
+ mRouteDescriptorId = id;
+ }
+ }
+
+ /**
+ * Represents a route that is provided by the support library media router
+ * and published by this route provider to the framework media router.
+ */
+ protected static final class UserRouteRecord {
+ public final MediaRouter.RouteInfo mRoute;
+ public final Object mRouteObj;
+
+ public UserRouteRecord(MediaRouter.RouteInfo route, Object routeObj) {
+ mRoute = route;
+ mRouteObj = routeObj;
+ }
+ }
+
+ protected static final class SystemRouteController extends RouteController {
+ private final Object mRouteObj;
+
+ public SystemRouteController(Object routeObj) {
+ mRouteObj = routeObj;
+ }
+
+ @Override
+ public void onSetVolume(int volume) {
+ MediaRouterJellybean.RouteInfo.requestSetVolume(mRouteObj, volume);
+ }
+
+ @Override
+ public void onUpdateVolume(int delta) {
+ MediaRouterJellybean.RouteInfo.requestUpdateVolume(mRouteObj, delta);
+ }
+ }
+ }
+
+ /**
+ * Jellybean MR1 implementation.
+ */
+ // @@RequiresApi(17)
+ private static class JellybeanMr1Impl extends JellybeanImpl
+ implements MediaRouterJellybeanMr1.Callback {
+ private MediaRouterJellybeanMr1.ActiveScanWorkaround mActiveScanWorkaround;
+ private MediaRouterJellybeanMr1.IsConnectingWorkaround mIsConnectingWorkaround;
+
+ public JellybeanMr1Impl(Context context, SyncCallback syncCallback) {
+ super(context, syncCallback);
+ }
+
+ @Override
+ public void onRoutePresentationDisplayChanged(Object routeObj) {
+ int index = findSystemRouteRecord(routeObj);
+ if (index >= 0) {
+ SystemRouteRecord record = mSystemRouteRecords.get(index);
+ Display newPresentationDisplay =
+ MediaRouterJellybeanMr1.RouteInfo.getPresentationDisplay(routeObj);
+ int newPresentationDisplayId = (newPresentationDisplay != null
+ ? newPresentationDisplay.getDisplayId() : -1);
+ if (newPresentationDisplayId
+ != record.mRouteDescriptor.getPresentationDisplayId()) {
+ record.mRouteDescriptor =
+ new MediaRouteDescriptor.Builder(record.mRouteDescriptor)
+ .setPresentationDisplayId(newPresentationDisplayId)
+ .build();
+ publishRoutes();
+ }
+ }
+ }
+
+ @Override
+ protected void onBuildSystemRouteDescriptor(SystemRouteRecord record,
+ MediaRouteDescriptor.Builder builder) {
+ super.onBuildSystemRouteDescriptor(record, builder);
+
+ if (!MediaRouterJellybeanMr1.RouteInfo.isEnabled(record.mRouteObj)) {
+ builder.setEnabled(false);
+ }
+
+ if (isConnecting(record)) {
+ builder.setConnecting(true);
+ }
+
+ Display presentationDisplay =
+ MediaRouterJellybeanMr1.RouteInfo.getPresentationDisplay(record.mRouteObj);
+ if (presentationDisplay != null) {
+ builder.setPresentationDisplayId(presentationDisplay.getDisplayId());
+ }
+ }
+
+ @Override
+ protected void updateCallback() {
+ super.updateCallback();
+
+ if (mActiveScanWorkaround == null) {
+ mActiveScanWorkaround = new MediaRouterJellybeanMr1.ActiveScanWorkaround(
+ getContext(), getHandler());
+ }
+ mActiveScanWorkaround.setActiveScanRouteTypes(mActiveScan ? mRouteTypes : 0);
+ }
+
+ @Override
+ protected Object createCallbackObj() {
+ return MediaRouterJellybeanMr1.createCallback(this);
+ }
+
+ protected boolean isConnecting(SystemRouteRecord record) {
+ if (mIsConnectingWorkaround == null) {
+ mIsConnectingWorkaround = new MediaRouterJellybeanMr1.IsConnectingWorkaround();
+ }
+ return mIsConnectingWorkaround.isConnecting(record.mRouteObj);
+ }
+ }
+
+ /**
+ * Jellybean MR2 implementation.
+ */
+ // @@RequiresApi(18)
+ private static class JellybeanMr2Impl extends JellybeanMr1Impl {
+ public JellybeanMr2Impl(Context context, SyncCallback syncCallback) {
+ super(context, syncCallback);
+ }
+
+ @Override
+ protected void onBuildSystemRouteDescriptor(SystemRouteRecord record,
+ MediaRouteDescriptor.Builder builder) {
+ super.onBuildSystemRouteDescriptor(record, builder);
+
+ CharSequence description =
+ MediaRouterJellybeanMr2.RouteInfo.getDescription(record.mRouteObj);
+ if (description != null) {
+ builder.setDescription(description.toString());
+ }
+ }
+
+ @Override
+ protected void selectRoute(Object routeObj) {
+ MediaRouterJellybean.selectRoute(mRouterObj,
+ MediaRouterJellybean.ALL_ROUTE_TYPES, routeObj);
+ }
+
+ @Override
+ protected Object getDefaultRoute() {
+ return MediaRouterJellybeanMr2.getDefaultRoute(mRouterObj);
+ }
+
+ @Override
+ protected void updateUserRouteProperties(UserRouteRecord record) {
+ super.updateUserRouteProperties(record);
+
+ MediaRouterJellybeanMr2.UserRouteInfo.setDescription(
+ record.mRouteObj, record.mRoute.getDescription());
+ }
+
+ @Override
+ protected void updateCallback() {
+ if (mCallbackRegistered) {
+ MediaRouterJellybean.removeCallback(mRouterObj, mCallbackObj);
+ }
+
+ mCallbackRegistered = true;
+ MediaRouterJellybeanMr2.addCallback(mRouterObj, mRouteTypes, mCallbackObj,
+ MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS
+ | (mActiveScan ? MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN : 0));
+ }
+
+ @Override
+ protected boolean isConnecting(SystemRouteRecord record) {
+ return MediaRouterJellybeanMr2.RouteInfo.isConnecting(record.mRouteObj);
+ }
+ }
+
+ /**
+ * Api24 implementation.
+ */
+ // @@RequiresApi(24)
+ private static class Api24Impl extends JellybeanMr2Impl {
+ public Api24Impl(Context context, SyncCallback syncCallback) {
+ super(context, syncCallback);
+ }
+
+ @Override
+ protected void onBuildSystemRouteDescriptor(SystemRouteRecord record,
+ MediaRouteDescriptor.Builder builder) {
+ super.onBuildSystemRouteDescriptor(record, builder);
+
+ builder.setDeviceType(MediaRouterApi24.RouteInfo.getDeviceType(record.mRouteObj));
+ }
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/support/mediarouter/media/package.html b/packages/MediaComponents/src/com/android/support/mediarouter/media/package.html
new file mode 100644
index 0000000..be2aaf2
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/support/mediarouter/media/package.html
@@ -0,0 +1,9 @@
+<html>
+
+<body>
+
+<p>Contains APIs that control the routing of media channels and streams from the current device
+ to external speakers and destination devices.</p>
+
+</body>
+</html>
diff --git a/packages/MediaComponents/src/com/android/widget/MediaControlView2Impl.java b/packages/MediaComponents/src/com/android/widget/MediaControlView2Impl.java
new file mode 100644
index 0000000..317a45f
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/widget/MediaControlView2Impl.java
@@ -0,0 +1,679 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.widget;
+
+import android.content.res.Resources;
+import android.media.MediaMetadata;
+import android.media.session.MediaController;
+import android.media.session.PlaybackState;
+import android.media.update.MediaControlView2Provider;
+import android.media.update.ViewProvider;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.ImageButton;
+import android.widget.MediaControlView2;
+import android.widget.ProgressBar;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+
+import com.android.media.update.ApiHelper;
+import com.android.media.update.R;
+
+import java.util.Formatter;
+import java.util.Locale;
+
+public class MediaControlView2Impl implements MediaControlView2Provider {
+ private static final String TAG = "MediaControlView2";
+
+ private final MediaControlView2 mInstance;
+ private final ViewProvider mSuperProvider;
+
+ static final String ACTION_SHOW_SUBTITLE = "showSubtitle";
+ static final String ACTION_HIDE_SUBTITLE = "hideSubtitle";
+
+ private static final int MAX_PROGRESS = 1000;
+ private static final int DEFAULT_PROGRESS_UPDATE_TIME_MS = 1000;
+ private static final int DEFAULT_TIMEOUT_MS = 2000;
+
+ private static final int REWIND_TIME_MS = 10000;
+ private static final int FORWARD_TIME_MS = 30000;
+
+ private final boolean mUseClosedCaption;
+ private final AccessibilityManager mAccessibilityManager;
+
+ private MediaController mController;
+ private MediaController.TransportControls mControls;
+ private PlaybackState mPlaybackState;
+ private MediaMetadata mMetadata;
+ private ProgressBar mProgress;
+ private TextView mEndTime, mCurrentTime;
+ private int mDuration;
+ private int mPrevState;
+ private boolean mShowing;
+ private boolean mDragging;
+ private boolean mListenersSet;
+ private boolean mCCIsEnabled;
+ private boolean mIsStopped;
+ private View.OnClickListener mNextListener, mPrevListener;
+ private ImageButton mPlayPauseButton;
+ private ImageButton mFfwdButton;
+ private ImageButton mRewButton;
+ private ImageButton mNextButton;
+ private ImageButton mPrevButton;
+ private ImageButton mCCButton;
+ private CharSequence mPlayDescription;
+ private CharSequence mPauseDescription;
+ private CharSequence mReplayDescription;
+
+ private StringBuilder mFormatBuilder;
+ private Formatter mFormatter;
+
+ public MediaControlView2Impl(MediaControlView2 instance, ViewProvider superProvider) {
+ mInstance = instance;
+ mSuperProvider = superProvider;
+ mUseClosedCaption = true;
+ mAccessibilityManager = AccessibilityManager.getInstance(mInstance.getContext());
+
+ // Inflate MediaControlView2 from XML
+ View root = makeControllerView();
+ mInstance.addView(root);
+ }
+
+ @Override
+ public void setController_impl(MediaController controller) {
+ mController = controller;
+ if (controller != null) {
+ mControls = controller.getTransportControls();
+ // Set mMetadata and mPlaybackState to existing MediaSession variables since they may
+ // be called before the callback is called
+ mPlaybackState = mController.getPlaybackState();
+ mMetadata = mController.getMetadata();
+ updateDuration();
+
+ mController.registerCallback(new MediaControllerCallback());
+ }
+ }
+
+ @Override
+ public void show_impl() {
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ }
+
+ @Override
+ public void show_impl(int timeout) {
+ if (!mShowing) {
+ setProgress();
+ if (mPlayPauseButton != null) {
+ mPlayPauseButton.requestFocus();
+ }
+ disableUnsupportedButtons();
+ mInstance.setVisibility(View.VISIBLE);
+ mShowing = true;
+ }
+ // cause the progress bar to be updated even if mShowing
+ // was already true. This happens, for example, if we're
+ // paused with the progress bar showing the user hits play.
+ mInstance.post(mShowProgress);
+
+ if (timeout != 0 && !mAccessibilityManager.isTouchExplorationEnabled()) {
+ mInstance.removeCallbacks(mFadeOut);
+ mInstance.postDelayed(mFadeOut, timeout);
+ }
+ }
+
+ @Override
+ public boolean isShowing_impl() {
+ return mShowing;
+ }
+
+ @Override
+ public void hide_impl() {
+ if (mShowing) {
+ try {
+ mInstance.removeCallbacks(mShowProgress);
+ // Remove existing call to mFadeOut to avoid from being called later.
+ mInstance.removeCallbacks(mFadeOut);
+ mInstance.setVisibility(View.GONE);
+ } catch (IllegalArgumentException ex) {
+ Log.w(TAG, "already removed");
+ }
+ mShowing = false;
+ }
+ }
+
+ @Override
+ public void showCCButton_impl() {
+ if (mCCButton != null) {
+ mCCButton.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ public boolean isPlaying_impl() {
+ if (mPlaybackState != null) {
+ return mPlaybackState.getState() == PlaybackState.STATE_PLAYING;
+ }
+ return false;
+ }
+
+ @Override
+ public int getCurrentPosition_impl() {
+ mPlaybackState = mController.getPlaybackState();
+ if (mPlaybackState != null) {
+ return (int) mPlaybackState.getPosition();
+ }
+ return 0;
+ }
+
+ @Override
+ public int getBufferPercentage_impl() {
+ if (mDuration == 0) {
+ return 0;
+ }
+ mPlaybackState = mController.getPlaybackState();
+ if (mPlaybackState != null) {
+ return (int) (mPlaybackState.getBufferedPosition() * 100) / mDuration;
+ }
+ return 0;
+ }
+
+ @Override
+ public boolean canPause_impl() {
+ if (mPlaybackState != null) {
+ return (mPlaybackState.getActions() & PlaybackState.ACTION_PAUSE) != 0;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean canSeekBackward_impl() {
+ if (mPlaybackState != null) {
+ return (mPlaybackState.getActions() & PlaybackState.ACTION_REWIND) != 0;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean canSeekForward_impl() {
+ if (mPlaybackState != null) {
+ return (mPlaybackState.getActions() & PlaybackState.ACTION_FAST_FORWARD) != 0;
+ }
+ return true;
+ }
+
+ @Override
+ public void showSubtitle_impl() {
+ mController.sendCommand(ACTION_SHOW_SUBTITLE, null, null);
+ }
+
+ @Override
+ public void hideSubtitle_impl() {
+ mController.sendCommand(ACTION_HIDE_SUBTITLE, null, null);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName_impl() {
+ return MediaControlView2.class.getName();
+ }
+
+ @Override
+ public boolean onTouchEvent_impl(MotionEvent ev) {
+ return false;
+ }
+
+ // TODO: Should this function be removed?
+ @Override
+ public boolean onTrackballEvent_impl(MotionEvent ev) {
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ return false;
+ }
+
+ @Override
+ public boolean onKeyDown_impl(int keyCode, KeyEvent event) {
+ return mSuperProvider.onKeyDown_impl(keyCode, event);
+ }
+
+ @Override
+ public void onFinishInflate_impl() {
+ mSuperProvider.onFinishInflate_impl();
+ }
+
+ @Override
+ public boolean dispatchKeyEvent_impl(KeyEvent event) {
+ int keyCode = event.getKeyCode();
+ final boolean uniqueDown = event.getRepeatCount() == 0
+ && event.getAction() == KeyEvent.ACTION_DOWN;
+ if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
+ || keyCode == KeyEvent.KEYCODE_SPACE) {
+ if (uniqueDown) {
+ togglePausePlayState();
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ if (mPlayPauseButton != null) {
+ mPlayPauseButton.requestFocus();
+ }
+ }
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
+ if (uniqueDown && !mInstance.isPlaying()) {
+ togglePausePlayState();
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ }
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
+ if (uniqueDown && mInstance.isPlaying()) {
+ togglePausePlayState();
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ }
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
+ || keyCode == KeyEvent.KEYCODE_VOLUME_UP
+ || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE
+ || keyCode == KeyEvent.KEYCODE_CAMERA) {
+ // don't show the controls for volume adjustment
+ return mSuperProvider.dispatchKeyEvent_impl(event);
+ } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) {
+ if (uniqueDown) {
+ mInstance.hide();
+ }
+ return true;
+ }
+
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ return mSuperProvider.dispatchKeyEvent_impl(event);
+ }
+
+ @Override
+ public void setEnabled_impl(boolean enabled) {
+ if (mPlayPauseButton != null) {
+ mPlayPauseButton.setEnabled(enabled);
+ }
+ if (mFfwdButton != null) {
+ mFfwdButton.setEnabled(enabled);
+ }
+ if (mRewButton != null) {
+ mRewButton.setEnabled(enabled);
+ }
+ if (mNextButton != null) {
+ mNextButton.setEnabled(enabled && mNextListener != null);
+ }
+ if (mPrevButton != null) {
+ mPrevButton.setEnabled(enabled && mPrevListener != null);
+ }
+ if (mProgress != null) {
+ mProgress.setEnabled(enabled);
+ }
+ disableUnsupportedButtons();
+ mSuperProvider.setEnabled_impl(enabled);
+ }
+
+ ///////////////////////////////////////////////////
+ // Protected or private methods
+ ///////////////////////////////////////////////////
+
+ /**
+ * Create the view that holds the widgets that control playback.
+ * Derived classes can override this to create their own.
+ *
+ * @return The controller view.
+ * @hide This doesn't work as advertised
+ */
+ protected View makeControllerView() {
+ View root = LayoutInflater.from(mInstance.getContext()).inflate(
+ R.layout.media_controller, null);
+
+ initControllerView(root);
+
+ return root;
+ }
+
+ private void initControllerView(View v) {
+ Resources res = ApiHelper.getLibResources();
+ mPlayDescription = res.getText(R.string.lockscreen_play_button_content_description);
+ mPauseDescription = res.getText(R.string.lockscreen_pause_button_content_description);
+ mReplayDescription = res.getText(R.string.lockscreen_replay_button_content_description);
+ mPlayPauseButton = v.findViewById(R.id.pause);
+ if (mPlayPauseButton != null) {
+ mPlayPauseButton.requestFocus();
+ mPlayPauseButton.setOnClickListener(mPlayPauseListener);
+ }
+
+ // TODO: make the following buttons visible based upon whether they are supported for
+ // individual media files
+ mFfwdButton = v.findViewById(R.id.ffwd);
+ if (mFfwdButton != null) {
+ mFfwdButton.setOnClickListener(mFfwdListener);
+ mFfwdButton.setVisibility(View.GONE);
+ }
+ mRewButton = v.findViewById(R.id.rew);
+ if (mRewButton != null) {
+ mRewButton.setOnClickListener(mRewListener);
+ mRewButton.setVisibility(View.GONE);
+ }
+ mNextButton = v.findViewById(R.id.next);
+ if (mNextButton != null && !mListenersSet) {
+ mNextButton.setVisibility(View.GONE);
+ }
+ mPrevButton = v.findViewById(R.id.prev);
+ if (mPrevButton != null && !mListenersSet) {
+ mPrevButton.setVisibility(View.GONE);
+ }
+
+ // TODO: make CC button visible if the media file has a subtitle track
+ mCCButton = v.findViewById(R.id.cc);
+ if (mCCButton != null) {
+ mCCButton.setOnClickListener(mCCListener);
+ mCCButton.setVisibility(mUseClosedCaption ? View.VISIBLE : View.GONE);
+ }
+
+ mProgress =
+ v.findViewById(R.id.mediacontroller_progress);
+ if (mProgress != null) {
+ if (mProgress instanceof SeekBar) {
+ SeekBar seeker = (SeekBar) mProgress;
+ seeker.setOnSeekBarChangeListener(mSeekListener);
+ }
+ mProgress.setMax(MAX_PROGRESS);
+ }
+
+ mEndTime = v.findViewById(R.id.time);
+ mCurrentTime = v.findViewById(R.id.time_current);
+ mFormatBuilder = new StringBuilder();
+ mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());
+
+ installPrevNextListeners();
+ }
+
+ /**
+ * Disable pause or seek buttons if the stream cannot be paused or seeked.
+ * This requires the control interface to be a MediaPlayerControlExt
+ */
+ private void disableUnsupportedButtons() {
+ try {
+ if (mPlayPauseButton != null && !mInstance.canPause()) {
+ mPlayPauseButton.setEnabled(false);
+ }
+ if (mRewButton != null && !mInstance.canSeekBackward()) {
+ mRewButton.setEnabled(false);
+ }
+ if (mFfwdButton != null && !mInstance.canSeekForward()) {
+ mFfwdButton.setEnabled(false);
+ }
+ // TODO What we really should do is add a canSeek to the MediaPlayerControl interface;
+ // this scheme can break the case when applications want to allow seek through the
+ // progress bar but disable forward/backward buttons.
+ //
+ // However, currently the flags SEEK_BACKWARD_AVAILABLE, SEEK_FORWARD_AVAILABLE,
+ // and SEEK_AVAILABLE are all (un)set together; as such the aforementioned issue
+ // shouldn't arise in existing applications.
+ if (mProgress != null && !mInstance.canSeekBackward() && !mInstance.canSeekForward()) {
+ mProgress.setEnabled(false);
+ }
+ } catch (IncompatibleClassChangeError ex) {
+ // We were given an old version of the interface, that doesn't have
+ // the canPause/canSeekXYZ methods. This is OK, it just means we
+ // assume the media can be paused and seeked, and so we don't disable
+ // the buttons.
+ }
+ }
+
+ private final Runnable mFadeOut = new Runnable() {
+ @Override
+ public void run() {
+ mInstance.hide();
+ }
+ };
+
+ private final Runnable mShowProgress = new Runnable() {
+ @Override
+ public void run() {
+ int pos = setProgress();
+ if (!mDragging && mShowing && mInstance.isPlaying()) {
+ mInstance.postDelayed(mShowProgress,
+ DEFAULT_PROGRESS_UPDATE_TIME_MS - (pos % DEFAULT_PROGRESS_UPDATE_TIME_MS));
+ }
+ }
+ };
+
+ private String stringForTime(int timeMs) {
+ int totalSeconds = timeMs / 1000;
+
+ int seconds = totalSeconds % 60;
+ int minutes = (totalSeconds / 60) % 60;
+ int hours = totalSeconds / 3600;
+
+ mFormatBuilder.setLength(0);
+ if (hours > 0) {
+ return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString();
+ } else {
+ return mFormatter.format("%02d:%02d", minutes, seconds).toString();
+ }
+ }
+
+ private int setProgress() {
+ if (mController == null || mDragging) {
+ return 0;
+ }
+ int positionOnProgressBar = 0;
+ int currentPosition = mInstance.getCurrentPosition();
+ if (mDuration > 0) {
+ positionOnProgressBar = (int) (MAX_PROGRESS * (long) currentPosition / mDuration);
+ }
+ if (mProgress != null && currentPosition != mDuration) {
+ mProgress.setProgress(positionOnProgressBar);
+ mProgress.setSecondaryProgress(mInstance.getBufferPercentage() * 10);
+ }
+
+ if (mEndTime != null) {
+ mEndTime.setText(stringForTime(mDuration));
+
+ }
+ if (mCurrentTime != null) {
+ mCurrentTime.setText(stringForTime(currentPosition));
+ }
+
+ return currentPosition;
+ }
+
+ private void togglePausePlayState() {
+ if (mInstance.isPlaying()) {
+ mControls.pause();
+ mPlayPauseButton.setImageResource(R.drawable.ic_play_circle_filled);
+ mPlayPauseButton.setContentDescription(mPlayDescription);
+ } else {
+ mControls.play();
+ mPlayPauseButton.setImageResource(R.drawable.ic_pause_circle_filled);
+ mPlayPauseButton.setContentDescription(mPauseDescription);
+ }
+ }
+
+ // There are two scenarios that can trigger the seekbar listener to trigger:
+ //
+ // The first is the user using the touchpad to adjust the posititon of the
+ // seekbar's thumb. In this case onStartTrackingTouch is called followed by
+ // a number of onProgressChanged notifications, concluded by onStopTrackingTouch.
+ // We're setting the field "mDragging" to true for the duration of the dragging
+ // session to avoid jumps in the position in case of ongoing playback.
+ //
+ // The second scenario involves the user operating the scroll ball, in this
+ // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications,
+ // we will simply apply the updated position without suspending regular updates.
+ private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
+ @Override
+ public void onStartTrackingTouch(SeekBar bar) {
+ mInstance.show(3600000);
+
+ mDragging = true;
+
+ // By removing these pending progress messages we make sure
+ // that a) we won't update the progress while the user adjusts
+ // the seekbar and b) once the user is done dragging the thumb
+ // we will post one of these messages to the queue again and
+ // this ensures that there will be exactly one message queued up.
+ mInstance.removeCallbacks(mShowProgress);
+
+ // Check if playback is currently stopped. In this case, update the pause button to show
+ // the play image instead of the replay image.
+ if (mIsStopped) {
+ mPlayPauseButton.setImageResource(R.drawable.ic_play_circle_filled);
+ mPlayPauseButton.setContentDescription(mPlayDescription);
+ mIsStopped = false;
+ }
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar bar, int progress, boolean fromUser) {
+ if (!fromUser) {
+ // We're not interested in programmatically generated changes to
+ // the progress bar's position.
+ return;
+ }
+ if (mDuration > 0) {
+ int newPosition = (int) (((long) mDuration * progress) / MAX_PROGRESS);
+ mControls.seekTo(newPosition);
+
+ if (mCurrentTime != null) {
+ mCurrentTime.setText(stringForTime(newPosition));
+ }
+ }
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar bar) {
+ mDragging = false;
+
+ setProgress();
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+
+ // Ensure that progress is properly updated in the future,
+ // the call to show() does not guarantee this because it is a
+ // no-op if we are already showing.
+ mInstance.post(mShowProgress);
+ }
+ };
+
+ private final View.OnClickListener mPlayPauseListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ togglePausePlayState();
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ }
+ };
+
+ private final View.OnClickListener mRewListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int pos = mInstance.getCurrentPosition() - REWIND_TIME_MS;
+ mControls.seekTo(pos);
+ setProgress();
+
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ }
+ };
+
+ private final View.OnClickListener mFfwdListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int pos = mInstance.getCurrentPosition() + FORWARD_TIME_MS;
+ mControls.seekTo(pos);
+ setProgress();
+
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ }
+ };
+
+ private final View.OnClickListener mCCListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!mCCIsEnabled) {
+ mCCButton.setImageResource(R.drawable.ic_media_cc_enabled);
+ mCCIsEnabled = true;
+ mInstance.showSubtitle();
+ } else {
+ mCCButton.setImageResource(R.drawable.ic_media_cc_disabled);
+ mCCIsEnabled = false;
+ mInstance.hideSubtitle();
+ }
+ }
+ };
+
+ private void installPrevNextListeners() {
+ if (mNextButton != null) {
+ mNextButton.setOnClickListener(mNextListener);
+ mNextButton.setEnabled(mNextListener != null);
+ }
+
+ if (mPrevButton != null) {
+ mPrevButton.setOnClickListener(mPrevListener);
+ mPrevButton.setEnabled(mPrevListener != null);
+ }
+ }
+
+ private void updateDuration() {
+ if (mMetadata != null) {
+ if (mMetadata.containsKey(MediaMetadata.METADATA_KEY_DURATION)) {
+ mDuration = (int) mMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
+ // update progress bar
+ setProgress();
+ }
+ }
+ }
+
+ private class MediaControllerCallback extends MediaController.Callback {
+ @Override
+ public void onPlaybackStateChanged(PlaybackState state) {
+ mPlaybackState = state;
+
+ // Update pause button depending on playback state for the following two reasons:
+ // 1) Need to handle case where app customizes playback state behavior when app
+ // activity is resumed.
+ // 2) Need to handle case where the media file reaches end of duration.
+ if (mPlaybackState.getState() != mPrevState) {
+ switch (mPlaybackState.getState()) {
+ case PlaybackState.STATE_PLAYING:
+ mPlayPauseButton.setImageResource(R.drawable.ic_pause_circle_filled);
+ mPlayPauseButton.setContentDescription(mPauseDescription);
+ break;
+ case PlaybackState.STATE_PAUSED:
+ mPlayPauseButton.setImageResource(R.drawable.ic_play_circle_filled);
+ mPlayPauseButton.setContentDescription(mPlayDescription);
+ break;
+ case PlaybackState.STATE_STOPPED:
+ mPlayPauseButton.setImageResource(R.drawable.ic_replay);
+ mPlayPauseButton.setContentDescription(mReplayDescription);
+ mIsStopped = true;
+ break;
+ default:
+ break;
+ }
+ mPrevState = mPlaybackState.getState();
+ }
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadata metadata) {
+ mMetadata = metadata;
+ updateDuration();
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/MediaComponents/src/com/android/widget/MediaController2Impl.java b/packages/MediaComponents/src/com/android/widget/MediaController2Impl.java
deleted file mode 100644
index d322a20..0000000
--- a/packages/MediaComponents/src/com/android/widget/MediaController2Impl.java
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.widget;
-
-import android.graphics.Canvas;
-import android.media.session.MediaController;
-import android.media.update.MediaController2Provider;
-import android.media.update.ViewProvider;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.MediaController2;
-
-public class MediaController2Impl implements MediaController2Provider {
- private final MediaController2 mInstance;
- private final ViewProvider mSuperProvider;
-
- public MediaController2Impl(MediaController2 instance, ViewProvider superProvider) {
- mInstance = instance;
- mSuperProvider = superProvider;
-
- // TODO: Implement
- }
-
- @Override
- public void setController_impl(MediaController controller) {
- // TODO: Implement
- }
-
- @Override
- public void setAnchorView_impl(View view) {
- // TODO: Implement
- }
-
- @Override
- public void show_impl() {
- // TODO: Implement
- }
-
- @Override
- public void show_impl(int timeout) {
- // TODO: Implement
- }
-
- @Override
- public boolean isShowing_impl() {
- // TODO: Implement
- return false;
- }
-
- @Override
- public void hide_impl() {
- // TODO: Implement
- }
-
- @Override
- public void setPrevNextListeners_impl(OnClickListener next, OnClickListener prev) {
- // TODO: Implement
- }
-
- @Override
- public void showCCButton_impl() {
- // TODO: Implement
- }
-
- @Override
- public boolean isPlaying_impl() {
- // TODO: Implement
- return false;
- }
-
- @Override
- public int getCurrentPosition_impl() {
- // TODO: Implement
- return 0;
- }
-
- @Override
- public int getBufferPercentage_impl() {
- // TODO: Implement
- return 0;
- }
-
- @Override
- public boolean canPause_impl() {
- // TODO: Implement
- return false;
- }
-
- @Override
- public boolean canSeekBackward_impl() {
- // TODO: Implement
- return false;
- }
-
- @Override
- public boolean canSeekForward_impl() {
- // TODO: Implement
- return false;
- }
-
- @Override
- public void showSubtitle_impl() {
- // TODO: Implement
- }
-
- @Override
- public void hideSubtitle_impl() {
- // TODO: Implement
- }
-
- @Override
- public void onAttachedToWindow_impl() {
- mSuperProvider.onAttachedToWindow_impl();
- // TODO: Implement
- }
-
- @Override
- public void onDetachedFromWindow_impl() {
- mSuperProvider.onDetachedFromWindow_impl();
- // TODO: Implement
- }
-
- @Override
- public void onLayout_impl(boolean changed, int left, int top, int right, int bottom) {
- mSuperProvider.onLayout_impl(changed, left, top, right, bottom);
- // TODO: Implement
- }
-
- @Override
- public void draw_impl(Canvas canvas) {
- mSuperProvider.draw_impl(canvas);
- // TODO: Implement
- }
-
- @Override
- public CharSequence getAccessibilityClassName_impl() {
- // TODO: Implement
- return MediaController2.class.getName();
- }
-
- @Override
- public boolean onTouchEvent_impl(MotionEvent ev) {
- // TODO: Implement
- return mSuperProvider.onTouchEvent_impl(ev);
- }
-
- @Override
- public boolean onTrackballEvent_impl(MotionEvent ev) {
- // TODO: Implement
- return mSuperProvider.onTrackballEvent_impl(ev);
- }
-
- @Override
- public boolean onKeyDown_impl(int keyCode, KeyEvent event) {
- // TODO: Implement
- return mSuperProvider.onKeyDown_impl(keyCode, event);
- }
-
- @Override
- public void onFinishInflate_impl() {
- mSuperProvider.onFinishInflate_impl();
- // TODO: Implement
- }
-
- @Override
- public boolean dispatchKeyEvent_impl(KeyEvent event) {
- // TODO: Implement
- return mSuperProvider.dispatchKeyEvent_impl(event);
- }
-
- @Override
- public void setEnabled_impl(boolean enabled) {
- mSuperProvider.setEnabled_impl(enabled);
- // TODO: Implement
- }
-}
diff --git a/packages/MediaComponents/src/com/android/widget/SubtitleView.java b/packages/MediaComponents/src/com/android/widget/SubtitleView.java
new file mode 100644
index 0000000..9071967
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/widget/SubtitleView.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.media.SubtitleController.Anchor;
+import android.media.SubtitleTrack.RenderingWidget;
+import android.os.Looper;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+class SubtitleView extends FrameLayout implements Anchor {
+ private static final String TAG = "SubtitleView";
+
+ private RenderingWidget mSubtitleWidget;
+ private RenderingWidget.OnChangedListener mSubtitlesChangedListener;
+
+ public SubtitleView(Context context) {
+ this(context, null);
+ }
+
+ public SubtitleView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public SubtitleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public SubtitleView(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public void setSubtitleWidget(RenderingWidget subtitleWidget) {
+ if (mSubtitleWidget == subtitleWidget) {
+ return;
+ }
+
+ final boolean attachedToWindow = isAttachedToWindow();
+ if (mSubtitleWidget != null) {
+ if (attachedToWindow) {
+ mSubtitleWidget.onDetachedFromWindow();
+ }
+
+ mSubtitleWidget.setOnChangedListener(null);
+ }
+ mSubtitleWidget = subtitleWidget;
+
+ if (subtitleWidget != null) {
+ if (mSubtitlesChangedListener == null) {
+ mSubtitlesChangedListener = new RenderingWidget.OnChangedListener() {
+ @Override
+ public void onChanged(RenderingWidget renderingWidget) {
+ invalidate();
+ }
+ };
+ }
+
+ setWillNotDraw(false);
+ subtitleWidget.setOnChangedListener(mSubtitlesChangedListener);
+
+ if (attachedToWindow) {
+ subtitleWidget.onAttachedToWindow();
+ requestLayout();
+ }
+ } else {
+ setWillNotDraw(true);
+ }
+
+ invalidate();
+ }
+
+ @Override
+ public Looper getSubtitleLooper() {
+ return Looper.getMainLooper();
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ if (mSubtitleWidget != null) {
+ mSubtitleWidget.onAttachedToWindow();
+ }
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ if (mSubtitleWidget != null) {
+ mSubtitleWidget.onDetachedFromWindow();
+ }
+ }
+
+ @Override
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ if (mSubtitleWidget != null) {
+ final int width = getWidth() - getPaddingLeft() - getPaddingRight();
+ final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+
+ mSubtitleWidget.setSize(width, height);
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+
+ if (mSubtitleWidget != null) {
+ final int saveCount = canvas.save();
+ canvas.translate(getPaddingLeft(), getPaddingTop());
+ mSubtitleWidget.draw(canvas);
+ canvas.restoreToCount(saveCount);
+ }
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return SubtitleView.class.getName();
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/widget/VideoSurfaceView.java b/packages/MediaComponents/src/com/android/widget/VideoSurfaceView.java
new file mode 100644
index 0000000..800be8e
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/widget/VideoSurfaceView.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.widget;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.media.MediaPlayer;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+
+import static android.widget.VideoView2.VIEW_TYPE_SURFACEVIEW;
+
+class VideoSurfaceView extends SurfaceView implements VideoViewInterface, SurfaceHolder.Callback {
+ private static final String TAG = "VideoSurfaceView";
+ private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
+ private SurfaceHolder mSurfaceHolder = null;
+ private SurfaceListener mSurfaceListener = null;
+ private MediaPlayer mMediaPlayer;
+ // A flag to indicate taking over other view should be proceed.
+ private boolean mIsTakingOverOldView;
+ private VideoViewInterface mOldView;
+
+
+ public VideoSurfaceView(Context context) {
+ this(context, null);
+ }
+
+ public VideoSurfaceView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ getHolder().addCallback(this);
+ }
+
+ ////////////////////////////////////////////////////
+ // implements VideoViewInterface
+ ////////////////////////////////////////////////////
+
+ @Override
+ public boolean assignSurfaceToMediaPlayer(MediaPlayer mp) {
+ Log.d(TAG, "assignSurfaceToMediaPlayer(): mSurfaceHolder: " + mSurfaceHolder);
+ if (mp == null || !hasAvailableSurface()) {
+ return false;
+ }
+ mp.setDisplay(mSurfaceHolder);
+ return true;
+ }
+
+ @Override
+ public void setSurfaceListener(SurfaceListener l) {
+ mSurfaceListener = l;
+ }
+
+ @Override
+ public int getViewType() {
+ return VIEW_TYPE_SURFACEVIEW;
+ }
+
+ @Override
+ public void setMediaPlayer(MediaPlayer mp) {
+ mMediaPlayer = mp;
+ if (mIsTakingOverOldView) {
+ takeOver(mOldView);
+ }
+ }
+
+ @Override
+ public void takeOver(@NonNull VideoViewInterface oldView) {
+ if (assignSurfaceToMediaPlayer(mMediaPlayer)) {
+ ((View) oldView).setVisibility(GONE);
+ mIsTakingOverOldView = false;
+ mOldView = null;
+ if (mSurfaceListener != null) {
+ mSurfaceListener.onSurfaceTakeOverDone(this);
+ }
+ } else {
+ mIsTakingOverOldView = true;
+ mOldView = oldView;
+ }
+ }
+
+ @Override
+ public boolean hasAvailableSurface() {
+ return (mSurfaceHolder != null && mSurfaceHolder.getSurface() != null);
+ }
+
+ ////////////////////////////////////////////////////
+ // implements SurfaceHolder.Callback
+ ////////////////////////////////////////////////////
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ Log.d(TAG, "surfaceCreated: mSurfaceHolder: " + mSurfaceHolder + ", new holder: " + holder);
+ mSurfaceHolder = holder;
+ if (mIsTakingOverOldView) {
+ takeOver(mOldView);
+ } else {
+ assignSurfaceToMediaPlayer(mMediaPlayer);
+ }
+
+ if (mSurfaceListener != null) {
+ Rect rect = mSurfaceHolder.getSurfaceFrame();
+ mSurfaceListener.onSurfaceCreated(this, rect.width(), rect.height());
+ }
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ if (mSurfaceListener != null) {
+ mSurfaceListener.onSurfaceChanged(this, width, height);
+ }
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ // After we return from this we can't use the surface any more
+ mSurfaceHolder = null;
+ if (mSurfaceListener != null) {
+ mSurfaceListener.onSurfaceDestroyed(this);
+ }
+ }
+
+ // TODO: Investigate the way to move onMeasure() code into FrameLayout.
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int videoWidth = (mMediaPlayer == null) ? 0 : mMediaPlayer.getVideoWidth();
+ int videoHeight = (mMediaPlayer == null) ? 0 : mMediaPlayer.getVideoHeight();
+ if (DEBUG) {
+ Log.d(TAG, "onMeasure(" + MeasureSpec.toString(widthMeasureSpec) + ", "
+ + MeasureSpec.toString(heightMeasureSpec) + ")");
+ Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/" + getMeasuredHeight());
+ Log.i(TAG, " viewSize: " + getWidth() + "/" + getHeight());
+ Log.i(TAG, " mVideoWidth/height: " + videoWidth + ", " + videoHeight);
+ }
+
+ int width = getDefaultSize(videoWidth, widthMeasureSpec);
+ int height = getDefaultSize(videoHeight, heightMeasureSpec);
+
+ if (videoWidth > 0 && videoHeight > 0) {
+ int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
+ int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
+ int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) {
+ // the size is fixed
+ width = widthSpecSize;
+ height = heightSpecSize;
+
+ // for compatibility, we adjust size based on aspect ratio
+ if (videoWidth * height < width * videoHeight) {
+ if (DEBUG) {
+ Log.d(TAG, "image too wide, correcting");
+ }
+ width = height * videoWidth / videoHeight;
+ } else if (videoWidth * height > width * videoHeight) {
+ if (DEBUG) {
+ Log.d(TAG, "image too tall, correcting");
+ }
+ height = width * videoHeight / videoWidth;
+ }
+ } else if (widthSpecMode == MeasureSpec.EXACTLY) {
+ // only the width is fixed, adjust the height to match aspect ratio if possible
+ width = widthSpecSize;
+ height = width * videoHeight / videoWidth;
+ if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) {
+ // couldn't match aspect ratio within the constraints
+ height = heightSpecSize;
+ }
+ } else if (heightSpecMode == MeasureSpec.EXACTLY) {
+ // only the height is fixed, adjust the width to match aspect ratio if possible
+ height = heightSpecSize;
+ width = height * videoWidth / videoHeight;
+ if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) {
+ // couldn't match aspect ratio within the constraints
+ width = widthSpecSize;
+ }
+ } else {
+ // neither the width nor the height are fixed, try to use actual video size
+ width = videoWidth;
+ height = videoHeight;
+ if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) {
+ // too tall, decrease both width and height
+ height = heightSpecSize;
+ width = height * videoWidth / videoHeight;
+ }
+ if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) {
+ // too wide, decrease both width and height
+ width = widthSpecSize;
+ height = width * videoHeight / videoWidth;
+ }
+ }
+ } else {
+ // no size yet, just adopt the given spec sizes
+ }
+ setMeasuredDimension(width, height);
+ if (DEBUG) {
+ Log.i(TAG, "end of onMeasure()");
+ Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/" + getMeasuredHeight());
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "ViewType: SurfaceView / Visibility: " + getVisibility()
+ + " / surfaceHolder: " + mSurfaceHolder;
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/widget/VideoTextureView.java b/packages/MediaComponents/src/com/android/widget/VideoTextureView.java
new file mode 100644
index 0000000..f240301
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/widget/VideoTextureView.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.widget;
+
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.media.MediaPlayer;
+import android.support.annotation.NonNull;
+import android.support.annotation.RequiresApi;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Surface;
+import android.view.TextureView;
+import android.view.View;
+
+import static android.widget.VideoView2.VIEW_TYPE_TEXTUREVIEW;
+
+@RequiresApi(26)
+class VideoTextureView extends TextureView
+ implements VideoViewInterface, TextureView.SurfaceTextureListener {
+ private static final String TAG = "VideoTextureView";
+ private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
+
+ private SurfaceTexture mSurfaceTexture;
+ private Surface mSurface;
+ private SurfaceListener mSurfaceListener;
+ private MediaPlayer mMediaPlayer;
+ // A flag to indicate taking over other view should be proceed.
+ private boolean mIsTakingOverOldView;
+ private VideoViewInterface mOldView;
+
+ public VideoTextureView(Context context) {
+ this(context, null);
+ }
+
+ public VideoTextureView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public VideoTextureView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public VideoTextureView(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ setSurfaceTextureListener(this);
+ }
+
+ ////////////////////////////////////////////////////
+ // implements VideoViewInterface
+ ////////////////////////////////////////////////////
+
+ @Override
+ public boolean assignSurfaceToMediaPlayer(MediaPlayer mp) {
+ Log.d(TAG, "assignSurfaceToMediaPlayer(): mSurfaceTexture: " + mSurfaceTexture);
+ if (mp == null || !hasAvailableSurface()) {
+ // Surface is not ready.
+ return false;
+ }
+ mp.setSurface(mSurface);
+ return true;
+ }
+
+ @Override
+ public void setSurfaceListener(SurfaceListener l) {
+ mSurfaceListener = l;
+ }
+
+ @Override
+ public int getViewType() {
+ return VIEW_TYPE_TEXTUREVIEW;
+ }
+
+ @Override
+ public void setMediaPlayer(MediaPlayer mp) {
+ mMediaPlayer = mp;
+ if (mIsTakingOverOldView) {
+ takeOver(mOldView);
+ }
+ }
+
+ @Override
+ public void takeOver(@NonNull VideoViewInterface oldView) {
+ if (assignSurfaceToMediaPlayer(mMediaPlayer)) {
+ ((View) oldView).setVisibility(GONE);
+ mIsTakingOverOldView = false;
+ mOldView = null;
+ if (mSurfaceListener != null) {
+ mSurfaceListener.onSurfaceTakeOverDone(this);
+ }
+ } else {
+ mIsTakingOverOldView = true;
+ mOldView = oldView;
+ }
+ }
+
+ @Override
+ public boolean hasAvailableSurface() {
+ return (mSurfaceTexture != null && !mSurfaceTexture.isReleased() && mSurface != null);
+ }
+
+ ////////////////////////////////////////////////////
+ // implements TextureView.SurfaceTextureListener
+ ////////////////////////////////////////////////////
+
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
+ Log.d(TAG, "onSurfaceTextureAvailable: mSurfaceTexture: " + mSurfaceTexture
+ + ", new surface: " + surfaceTexture);
+ mSurfaceTexture = surfaceTexture;
+ mSurface = new Surface(mSurfaceTexture);
+ if (mIsTakingOverOldView) {
+ takeOver(mOldView);
+ } else {
+ assignSurfaceToMediaPlayer(mMediaPlayer);
+ }
+ if (mSurfaceListener != null) {
+ mSurfaceListener.onSurfaceCreated(this, width, height);
+ }
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {
+ if (mSurfaceListener != null) {
+ mSurfaceListener.onSurfaceChanged(this, width, height);
+ }
+ // requestLayout(); // TODO: figure out if it should be called here?
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+ // no-op
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
+ if (mSurfaceListener != null) {
+ mSurfaceListener.onSurfaceDestroyed(this);
+ }
+ mSurfaceTexture = null;
+ mSurface = null;
+ return true;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int videoWidth = (mMediaPlayer == null) ? 0 : mMediaPlayer.getVideoWidth();
+ int videoHeight = (mMediaPlayer == null) ? 0 : mMediaPlayer.getVideoHeight();
+ if (DEBUG) {
+ Log.d(TAG, "onMeasure(" + MeasureSpec.toString(widthMeasureSpec) + ", "
+ + MeasureSpec.toString(heightMeasureSpec) + ")");
+ Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/" + getMeasuredHeight());
+ Log.i(TAG, " viewSize: " + getWidth() + "/" + getHeight());
+ Log.i(TAG, " mVideoWidth/height: " + videoWidth + ", " + videoHeight);
+ }
+
+ int width = getDefaultSize(videoWidth, widthMeasureSpec);
+ int height = getDefaultSize(videoHeight, heightMeasureSpec);
+
+ if (videoWidth > 0 && videoHeight > 0) {
+ int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
+ int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
+ int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) {
+ // the size is fixed
+ width = widthSpecSize;
+ height = heightSpecSize;
+
+ // for compatibility, we adjust size based on aspect ratio
+ if (videoWidth * height < width * videoHeight) {
+ if (DEBUG) {
+ Log.d(TAG, "image too wide, correcting");
+ }
+ width = height * videoWidth / videoHeight;
+ } else if (videoWidth * height > width * videoHeight) {
+ if (DEBUG) {
+ Log.d(TAG, "image too tall, correcting");
+ }
+ height = width * videoHeight / videoWidth;
+ }
+ } else if (widthSpecMode == MeasureSpec.EXACTLY) {
+ // only the width is fixed, adjust the height to match aspect ratio if possible
+ width = widthSpecSize;
+ height = width * videoHeight / videoWidth;
+ if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) {
+ // couldn't match aspect ratio within the constraints
+ height = heightSpecSize;
+ }
+ } else if (heightSpecMode == MeasureSpec.EXACTLY) {
+ // only the height is fixed, adjust the width to match aspect ratio if possible
+ height = heightSpecSize;
+ width = height * videoWidth / videoHeight;
+ if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) {
+ // couldn't match aspect ratio within the constraints
+ width = widthSpecSize;
+ }
+ } else {
+ // neither the width nor the height are fixed, try to use actual video size
+ width = videoWidth;
+ height = videoHeight;
+ if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) {
+ // too tall, decrease both width and height
+ height = heightSpecSize;
+ width = height * videoWidth / videoHeight;
+ }
+ if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) {
+ // too wide, decrease both width and height
+ width = widthSpecSize;
+ height = width * videoHeight / videoWidth;
+ }
+ }
+ } else {
+ // no size yet, just adopt the given spec sizes
+ }
+ setMeasuredDimension(width, height);
+ if (DEBUG) {
+ Log.i(TAG, "end of onMeasure()");
+ Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/" + getMeasuredHeight());
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "ViewType: TextureView / Visibility: " + getVisibility()
+ + " / surfaceTexture: " + mSurfaceTexture;
+
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/widget/VideoView2Impl.java b/packages/MediaComponents/src/com/android/widget/VideoView2Impl.java
index 66b5ed5..19a41de 100644
--- a/packages/MediaComponents/src/com/android/widget/VideoView2Impl.java
+++ b/packages/MediaComponents/src/com/android/widget/VideoView2Impl.java
@@ -16,220 +16,486 @@
package com.android.widget;
-import android.graphics.Canvas;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
+import android.media.AudioManager;
+import android.media.MediaMetadata;
import android.media.MediaPlayer;
+import android.media.Cea708CaptionRenderer;
+import android.media.ClosedCaptionRenderer;
+import android.media.Metadata;
+import android.media.PlaybackParams;
+import android.media.SubtitleController;
+import android.media.session.MediaSession;
+import android.media.session.PlaybackState;
+import android.media.TtmlRenderer;
+import android.media.WebVttRenderer;
import android.media.update.VideoView2Provider;
import android.media.update.ViewProvider;
import android.net.Uri;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
-import android.widget.MediaController2;
+import android.widget.FrameLayout.LayoutParams;
+import android.widget.MediaControlView2;
import android.widget.VideoView2;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
+import com.android.media.update.ApiHelper;
+import com.android.media.update.R;
+
public class VideoView2Impl implements VideoView2Provider, VideoViewInterface.SurfaceListener {
+ private static final String TAG = "VideoView2";
+ private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
private final VideoView2 mInstance;
private final ViewProvider mSuperProvider;
- public VideoView2Impl(VideoView2 instance, ViewProvider superProvider) {
+ private static final int STATE_ERROR = -1;
+ private static final int STATE_IDLE = 0;
+ private static final int STATE_PREPARING = 1;
+ private static final int STATE_PREPARED = 2;
+ private static final int STATE_PLAYING = 3;
+ private static final int STATE_PAUSED = 4;
+ private static final int STATE_PLAYBACK_COMPLETED = 5;
+
+ private final AudioManager mAudioManager;
+ private AudioAttributes mAudioAttributes;
+ private int mAudioFocusType = AudioManager.AUDIOFOCUS_GAIN; // legacy focus gain
+ private int mAudioSession;
+
+ private VideoView2.OnPreparedListener mOnPreparedListener;
+ private VideoView2.OnCompletionListener mOnCompletionListener;
+ private VideoView2.OnErrorListener mOnErrorListener;
+ private VideoView2.OnInfoListener mOnInfoListener;
+ private VideoView2.OnViewTypeChangedListener mOnViewTypeChangedListener;
+
+ private VideoViewInterface mCurrentView;
+ private VideoTextureView mTextureView;
+ private VideoSurfaceView mSurfaceView;
+
+ private MediaPlayer mMediaPlayer;
+ private MediaControlView2 mMediaControlView;
+ private MediaSession mMediaSession;
+
+ private PlaybackState.Builder mStateBuilder;
+ private int mTargetState = STATE_IDLE;
+ private int mCurrentState = STATE_IDLE;
+ private int mCurrentBufferPercentage;
+ private int mSeekWhenPrepared; // recording the seek position while preparing
+
+ private int mSurfaceWidth;
+ private int mSurfaceHeight;
+ private int mVideoWidth;
+ private int mVideoHeight;
+
+ private boolean mCCEnabled;
+ private int mSelectedTrackIndex;
+
+ private SubtitleView mSubtitleView;
+ private float mSpeed;
+ // TODO: Remove mFallbackSpeed when integration with MediaPlayer2's new setPlaybackParams().
+ // Refer: https://docs.google.com/document/d/1nzAfns6i2hJ3RkaUre3QMT6wsDedJ5ONLiA_OOBFFX8/edit
+ private float mFallbackSpeed; // keep the original speed before 'pause' is called.
+
+ public VideoView2Impl(
+ VideoView2 instance, ViewProvider superProvider,
+ @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
mInstance = instance;
mSuperProvider = superProvider;
- // TODO: Implement
+ mVideoWidth = 0;
+ mVideoHeight = 0;
+ mSurfaceWidth = 0;
+ mSurfaceHeight = 0;
+ mSpeed = 1.0f;
+ mFallbackSpeed = mSpeed;
+
+ mAudioManager = (AudioManager) mInstance.getContext()
+ .getSystemService(Context.AUDIO_SERVICE);
+ mAudioAttributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA)
+ .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE).build();
+ mInstance.setFocusable(true);
+ mInstance.setFocusableInTouchMode(true);
+ mInstance.requestFocus();
+
+ // TODO: try to keep a single child at a time rather than always having both.
+ mTextureView = new VideoTextureView(mInstance.getContext());
+ mSurfaceView = new VideoSurfaceView(mInstance.getContext());
+ LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
+ LayoutParams.WRAP_CONTENT);
+ mTextureView.setLayoutParams(params);
+ mSurfaceView.setLayoutParams(params);
+ mTextureView.setSurfaceListener(this);
+ mSurfaceView.setSurfaceListener(this);
+
+ // TODO: Choose TextureView when SurfaceView cannot be created.
+ // Choose surface view by default
+ mTextureView.setVisibility(View.GONE);
+ mSurfaceView.setVisibility(View.VISIBLE);
+ mInstance.addView(mTextureView);
+ mInstance.addView(mSurfaceView);
+ mCurrentView = mSurfaceView;
+
+ LayoutParams subtitleParams = new LayoutParams(LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT);
+ mSubtitleView = new SubtitleView(mInstance.getContext());
+ mSubtitleView.setLayoutParams(subtitleParams);
+ mSubtitleView.setBackgroundColor(0);
+ mInstance.addView(mSubtitleView);
+
+ // Create MediaSession
+ mMediaSession = new MediaSession(mInstance.getContext(), "VideoView2MediaSession");
+ mMediaSession.setCallback(new MediaSessionCallback());
+
+ // TODO: Need a common namespace for attributes those are defined in updatable library.
+ boolean enableControlView = (attrs == null) || attrs.getAttributeBooleanValue(
+ "http://schemas.android.com/apk/com.android.media.api_provider",
+ "enableControlView", true);
+ if (enableControlView) {
+ setMediaControlView2_impl(new MediaControlView2(mInstance.getContext()));
+ }
+ }
+
+ @Override
+ public void setMediaControlView2_impl(MediaControlView2 mediaControlView) {
+ mMediaControlView = mediaControlView;
+
+ // TODO: change this so that the CC button appears only where there is a subtitle track.
+ mMediaControlView.showCCButton();
+
+ // Get MediaController from MediaSession and set it inside MediaControlView2
+ mMediaControlView.setController(mMediaSession.getController());
+
+ LayoutParams params =
+ new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ mInstance.addView(mMediaControlView, params);
+ }
+
+ @Override
+ public MediaControlView2 getMediaControlView2_impl() {
+ return mMediaControlView;
}
@Override
public void start_impl() {
- // TODO: Implement
+ if (isInPlaybackState() && mCurrentView.hasAvailableSurface()) {
+ applySpeed();
+ mMediaPlayer.start();
+ mCurrentState = STATE_PLAYING;
+ updatePlaybackState();
+ }
+ mTargetState = STATE_PLAYING;
+ if (DEBUG) {
+ Log.d(TAG, "start(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
}
@Override
public void pause_impl() {
- // TODO: Implement
+ if (isInPlaybackState()) {
+ if (mMediaPlayer.isPlaying()) {
+ mMediaPlayer.pause();
+ mCurrentState = STATE_PAUSED;
+ updatePlaybackState();
+ }
+ }
+ mTargetState = STATE_PAUSED;
+ if (DEBUG) {
+ Log.d(TAG, "pause(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
}
@Override
public int getDuration_impl() {
- // TODO: Implement
+ if (isInPlaybackState()) {
+ return mMediaPlayer.getDuration();
+ }
return -1;
}
@Override
public int getCurrentPosition_impl() {
- // TODO: Implement
+ if (isInPlaybackState()) {
+ return mMediaPlayer.getCurrentPosition();
+ }
return 0;
}
@Override
public void seekTo_impl(int msec) {
- // TODO: Implement
+ if (isInPlaybackState()) {
+ mMediaPlayer.seekTo(msec);
+ mSeekWhenPrepared = 0;
+ updatePlaybackState();
+ } else {
+ mSeekWhenPrepared = msec;
+ }
}
@Override
public boolean isPlaying_impl() {
- // TODO: Implement
- return false;
+ return (isInPlaybackState()) && mMediaPlayer.isPlaying();
}
@Override
public int getBufferPercentage_impl() {
- return -1;
+ return mCurrentBufferPercentage;
}
@Override
public int getAudioSessionId_impl() {
- // TODO: Implement
- return 0;
+ if (mAudioSession == 0) {
+ MediaPlayer foo = new MediaPlayer();
+ mAudioSession = foo.getAudioSessionId();
+ foo.release();
+ }
+ return mAudioSession;
}
@Override
public void showSubtitle_impl() {
- // TODO: Implement
+ // Retrieve all tracks that belong to the current video.
+ MediaPlayer.TrackInfo[] trackInfos = mMediaPlayer.getTrackInfo();
+
+ List<Integer> subtitleTrackIndices = new ArrayList<>();
+ for (int i = 0; i < trackInfos.length; ++i) {
+ int trackType = trackInfos[i].getTrackType();
+ if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) {
+ subtitleTrackIndices.add(i);
+ }
+ }
+ if (subtitleTrackIndices.size() > 0) {
+ // Select first subtitle track
+ mCCEnabled = true;
+ mSelectedTrackIndex = subtitleTrackIndices.get(0);
+ mMediaPlayer.selectTrack(mSelectedTrackIndex);
+ }
}
@Override
public void hideSubtitle_impl() {
- // TODO: Implement
+ if (mCCEnabled) {
+ mMediaPlayer.deselectTrack(mSelectedTrackIndex);
+ mCCEnabled = false;
+ }
+ }
+
+ @Override
+ public void setSpeed_impl(float speed) {
+ if (speed <= 0.0f) {
+ Log.e(TAG, "Unsupported speed (" + speed + ") is ignored.");
+ return;
+ }
+ mSpeed = speed;
+ if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
+ applySpeed();
+ }
+ }
+
+ @Override
+ public float getSpeed_impl() {
+ if (DEBUG) {
+ if (mMediaPlayer != null) {
+ float speed = mMediaPlayer.getPlaybackParams().getSpeed();
+ if (speed != mSpeed) {
+ Log.w(TAG, "VideoView2's speed : " + mSpeed + " is different from "
+ + "MediaPlayer's speed : " + speed);
+ }
+ }
+ }
+ return mSpeed;
}
@Override
public void setAudioFocusRequest_impl(int focusGain) {
- // TODO: Implement
+ if (focusGain != AudioManager.AUDIOFOCUS_NONE
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) {
+ throw new IllegalArgumentException("Illegal audio focus type " + focusGain);
+ }
+ mAudioFocusType = focusGain;
}
@Override
public void setAudioAttributes_impl(AudioAttributes attributes) {
- // TODO: Implement
+ if (attributes == null) {
+ throw new IllegalArgumentException("Illegal null AudioAttributes");
+ }
+ mAudioAttributes = attributes;
}
@Override
public void setVideoPath_impl(String path) {
- // TODO: Implement
+ mInstance.setVideoURI(Uri.parse(path));
}
@Override
public void setVideoURI_impl(Uri uri) {
- // TODO: Implement
+ mInstance.setVideoURI(uri, null);
}
@Override
public void setVideoURI_impl(Uri uri, Map<String, String> headers) {
- // TODO: Implement
- }
-
- @Override
- public void setMediaController2_impl(MediaController2 controllerView) {
- // TODO: Implement
+ mSeekWhenPrepared = 0;
+ openVideo(uri, headers);
}
@Override
public void setViewType_impl(int viewType) {
- // TODO: Implement
+ if (viewType == mCurrentView.getViewType()) {
+ return;
+ }
+ VideoViewInterface targetView;
+ if (viewType == VideoView2.VIEW_TYPE_TEXTUREVIEW) {
+ Log.d(TAG, "switching to TextureView");
+ targetView = mTextureView;
+ } else if (viewType == VideoView2.VIEW_TYPE_SURFACEVIEW) {
+ Log.d(TAG, "switching to SurfaceView");
+ targetView = mSurfaceView;
+ } else {
+ throw new IllegalArgumentException("Unknown view type: " + viewType);
+ }
+ ((View) targetView).setVisibility(View.VISIBLE);
+ targetView.takeOver(mCurrentView);
+ mInstance.requestLayout();
}
@Override
public int getViewType_impl() {
- // TODO: Implement
- return -1;
+ return mCurrentView.getViewType();
}
@Override
public void stopPlayback_impl() {
- // TODO: Implement
+ resetPlayer();
}
@Override
- public void setOnPreparedListener_impl(MediaPlayer.OnPreparedListener l) {
- // TODO: Implement
+ public void setOnPreparedListener_impl(VideoView2.OnPreparedListener l) {
+ mOnPreparedListener = l;
}
@Override
- public void setOnCompletionListener_impl(MediaPlayer.OnCompletionListener l) {
- // TODO: Implement
+ public void setOnCompletionListener_impl(VideoView2.OnCompletionListener l) {
+ mOnCompletionListener = l;
}
@Override
- public void setOnErrorListener_impl(MediaPlayer.OnErrorListener l) {
- // TODO: Implement
+ public void setOnErrorListener_impl(VideoView2.OnErrorListener l) {
+ mOnErrorListener = l;
}
@Override
- public void setOnInfoListener_impl(MediaPlayer.OnInfoListener l) {
- // TODO: Implement
+ public void setOnInfoListener_impl(VideoView2.OnInfoListener l) {
+ mOnInfoListener = l;
}
@Override
public void setOnViewTypeChangedListener_impl(VideoView2.OnViewTypeChangedListener l) {
- // TODO: Implement
- }
-
- @Override
- public void onAttachedToWindow_impl() {
- mSuperProvider.onAttachedToWindow_impl();
- // TODO: Implement
- }
-
- @Override
- public void onDetachedFromWindow_impl() {
- mSuperProvider.onDetachedFromWindow_impl();
- // TODO: Implement
- }
-
- @Override
- public void onLayout_impl(boolean changed, int left, int top, int right, int bottom) {
- mSuperProvider.onLayout_impl(changed, left, top, right, bottom);
- // TODO: Implement
- }
-
- @Override
- public void draw_impl(Canvas canvas) {
- mSuperProvider.draw_impl(canvas);
- // TODO: Implement
+ mOnViewTypeChangedListener = l;
}
@Override
public CharSequence getAccessibilityClassName_impl() {
- // TODO: Implement
- return null;
+ return VideoView2.class.getName();
}
@Override
public boolean onTouchEvent_impl(MotionEvent ev) {
- // TODO: Implement
- return false;
+ if (DEBUG) {
+ Log.d(TAG, "onTouchEvent(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ if (ev.getAction() == MotionEvent.ACTION_UP
+ && isInPlaybackState() && mMediaControlView != null) {
+ toggleMediaControlViewVisibility();
+ }
+ return mSuperProvider.onTouchEvent_impl(ev);
}
@Override
public boolean onTrackballEvent_impl(MotionEvent ev) {
- // TODO: Implement
- return false;
+ if (ev.getAction() == MotionEvent.ACTION_UP
+ && isInPlaybackState() && mMediaControlView != null) {
+ toggleMediaControlViewVisibility();
+ }
+ return mSuperProvider.onTrackballEvent_impl(ev);
}
@Override
public boolean onKeyDown_impl(int keyCode, KeyEvent event) {
- // TODO: Implement
- return false;
+ Log.v(TAG, "onKeyDown_impl: " + keyCode);
+ boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK
+ && keyCode != KeyEvent.KEYCODE_VOLUME_UP
+ && keyCode != KeyEvent.KEYCODE_VOLUME_DOWN
+ && keyCode != KeyEvent.KEYCODE_VOLUME_MUTE
+ && keyCode != KeyEvent.KEYCODE_MENU
+ && keyCode != KeyEvent.KEYCODE_CALL
+ && keyCode != KeyEvent.KEYCODE_ENDCALL;
+ if (isInPlaybackState() && isKeyCodeSupported && mMediaControlView != null) {
+ if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
+ if (mMediaPlayer.isPlaying()) {
+ mInstance.pause();
+ mMediaControlView.show();
+ } else {
+ mInstance.start();
+ mMediaControlView.hide();
+ }
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
+ if (!mMediaPlayer.isPlaying()) {
+ mInstance.start();
+ mMediaControlView.hide();
+ }
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
+ if (mMediaPlayer.isPlaying()) {
+ mInstance.pause();
+ mMediaControlView.show();
+ }
+ return true;
+ } else {
+ toggleMediaControlViewVisibility();
+ }
+ }
+
+ return mSuperProvider.onKeyDown_impl(keyCode, event);
}
@Override
public void onFinishInflate_impl() {
- // TODO: Implement
+ mSuperProvider.onFinishInflate_impl();
}
@Override
public boolean dispatchKeyEvent_impl(KeyEvent event) {
- // TODO: Implement
- return false;
+ return mSuperProvider.dispatchKeyEvent_impl(event);
}
@Override
public void setEnabled_impl(boolean enabled) {
- // TODO: Implement
+ mSuperProvider.setEnabled_impl(enabled);
}
///////////////////////////////////////////////////
@@ -238,21 +504,484 @@
@Override
public void onSurfaceCreated(View view, int width, int height) {
- // TODO: Implement
+ if (DEBUG) {
+ Log.d(TAG, "onSurfaceCreated(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState + ", width/height: " + width + "/" + height
+ + ", " + view.toString());
+ }
+ mSurfaceWidth = width;
+ mSurfaceHeight = height;
+
+ if (needToStart()) {
+ mInstance.start();
+ }
}
@Override
public void onSurfaceDestroyed(View view) {
- // TODO: Implement
+ if (DEBUG) {
+ Log.d(TAG, "onSurfaceDestroyed(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState + ", " + view.toString());
+ }
+ if (mMediaControlView != null) {
+ mMediaControlView.hide();
+ }
}
@Override
public void onSurfaceChanged(View view, int width, int height) {
- // TODO: Implement
+ // TODO: Do we need to call requestLayout here?
+ if (DEBUG) {
+ Log.d(TAG, "onSurfaceChanged(). width/height: " + width + "/" + height
+ + ", " + view.toString());
+ }
+ mSurfaceWidth = width;
+ mSurfaceHeight = height;
}
@Override
public void onSurfaceTakeOverDone(VideoViewInterface view) {
- // TODO: Implement
+ if (DEBUG) {
+ Log.d(TAG, "onSurfaceTakeOverDone(). Now current view is: " + view);
+ }
+ mCurrentView = view;
+ if (mOnViewTypeChangedListener != null) {
+ mOnViewTypeChangedListener.onViewTypeChanged(view.getViewType());
+ }
+ if (needToStart()) {
+ mInstance.start();
+ }
+ }
+
+ ///////////////////////////////////////////////////
+ // Protected or private methods
+ ///////////////////////////////////////////////////
+
+ private boolean isInPlaybackState() {
+ return (mMediaPlayer != null
+ && mCurrentState != STATE_ERROR
+ && mCurrentState != STATE_IDLE
+ && mCurrentState != STATE_PREPARING);
+ }
+
+ private boolean needToStart() {
+ return (mMediaPlayer != null
+ && mCurrentState != STATE_PLAYING
+ && mTargetState == STATE_PLAYING);
+ }
+
+ // Creates a MediaPlayer instance and prepare playback.
+ private void openVideo(Uri uri, Map<String, String> headers) {
+ resetPlayer();
+ if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+ // TODO this should have a focus listener
+ AudioFocusRequest focusRequest;
+ focusRequest = new AudioFocusRequest.Builder(mAudioFocusType)
+ .setAudioAttributes(mAudioAttributes)
+ .build();
+ mAudioManager.requestAudioFocus(focusRequest);
+ }
+
+ try {
+ Log.d(TAG, "openVideo(): creating new MediaPlayer instance.");
+ mMediaPlayer = new MediaPlayer();
+ mSurfaceView.setMediaPlayer(mMediaPlayer);
+ mTextureView.setMediaPlayer(mMediaPlayer);
+ mCurrentView.assignSurfaceToMediaPlayer(mMediaPlayer);
+
+ // TODO: create SubtitleController in MediaPlayer, but we need
+ // a context for the subtitle renderers
+ final Context context = mInstance.getContext();
+ final SubtitleController controller = new SubtitleController(
+ context, mMediaPlayer.getMediaTimeProvider(), mMediaPlayer);
+ controller.registerRenderer(new WebVttRenderer(context));
+ controller.registerRenderer(new TtmlRenderer(context));
+ controller.registerRenderer(new Cea708CaptionRenderer(context));
+ controller.registerRenderer(new ClosedCaptionRenderer(context));
+ mMediaPlayer.setSubtitleAnchor(controller, (SubtitleController.Anchor) mSubtitleView);
+
+ if (mAudioSession != 0) {
+ mMediaPlayer.setAudioSessionId(mAudioSession);
+ } else {
+ mAudioSession = mMediaPlayer.getAudioSessionId();
+ }
+ mMediaPlayer.setOnPreparedListener(mPreparedListener);
+ mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
+ mMediaPlayer.setOnCompletionListener(mCompletionListener);
+ mMediaPlayer.setOnErrorListener(mErrorListener);
+ mMediaPlayer.setOnInfoListener(mInfoListener);
+ mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
+ mCurrentBufferPercentage = 0;
+ mMediaPlayer.setDataSource(mInstance.getContext(), uri, headers);
+ mMediaPlayer.setAudioAttributes(mAudioAttributes);
+ // we don't set the target state here either, but preserve the
+ // target state that was there before.
+ mCurrentState = STATE_PREPARING;
+ mMediaPlayer.prepareAsync();
+
+ if (DEBUG) {
+ Log.d(TAG, "openVideo(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ /*
+ for (Pair<InputStream, MediaFormat> pending: mPendingSubtitleTracks) {
+ try {
+ mMediaPlayer.addSubtitleSource(pending.first, pending.second);
+ } catch (IllegalStateException e) {
+ mInfoListener.onInfo(
+ mMediaPlayer, MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, 0);
+ }
+ }
+ */
+ } catch (IOException | IllegalArgumentException ex) {
+ Log.w(TAG, "Unable to open content: " + uri, ex);
+ mCurrentState = STATE_ERROR;
+ mTargetState = STATE_ERROR;
+ mErrorListener.onError(mMediaPlayer,
+ MediaPlayer.MEDIA_ERROR_UNKNOWN, MediaPlayer.MEDIA_ERROR_IO);
+ } finally {
+ //mPendingSubtitleTracks.clear();
+ }
+ }
+
+ /*
+ * Reset the media player in any state
+ */
+ // TODO: Figure out if the legacy code's boolean parameter: cleartargetstate is necessary.
+ private void resetPlayer() {
+ if (mMediaPlayer != null) {
+ mMediaPlayer.reset();
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ //mPendingSubtitleTracks.clear();
+ mCurrentState = STATE_IDLE;
+ mTargetState = STATE_IDLE;
+ if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+ mAudioManager.abandonAudioFocus(null);
+ }
+ }
+ mSurfaceWidth = 0;
+ mSurfaceHeight = 0;
+ mVideoWidth = 0;
+ mVideoHeight = 0;
+ }
+
+ private void updatePlaybackState() {
+ if (mStateBuilder == null) {
+ // Get the capabilities of the player for this stream
+ Metadata data = mMediaPlayer.getMetadata(MediaPlayer.METADATA_ALL,
+ MediaPlayer.BYPASS_METADATA_FILTER);
+
+ // Add Play action as default
+ long playbackActions = PlaybackState.ACTION_PLAY;
+ if (data != null) {
+ if (!data.has(Metadata.PAUSE_AVAILABLE)
+ || data.getBoolean(Metadata.PAUSE_AVAILABLE)) {
+ playbackActions |= PlaybackState.ACTION_PAUSE;
+ }
+ if (!data.has(Metadata.SEEK_BACKWARD_AVAILABLE)
+ || data.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE)) {
+ playbackActions |= PlaybackState.ACTION_REWIND;
+ }
+ if (!data.has(Metadata.SEEK_FORWARD_AVAILABLE)
+ || data.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE)) {
+ playbackActions |= PlaybackState.ACTION_FAST_FORWARD;
+ }
+ if (!data.has(Metadata.SEEK_AVAILABLE)
+ || data.getBoolean(Metadata.SEEK_AVAILABLE)) {
+ playbackActions |= PlaybackState.ACTION_SEEK_TO;
+ }
+ } else {
+ playbackActions |= (PlaybackState.ACTION_PAUSE |
+ PlaybackState.ACTION_REWIND | PlaybackState.ACTION_FAST_FORWARD |
+ PlaybackState.ACTION_SEEK_TO);
+ }
+ mStateBuilder = new PlaybackState.Builder();
+ mStateBuilder.setActions(playbackActions);
+ mStateBuilder.addCustomAction(MediaControlView2Impl.ACTION_SHOW_SUBTITLE, null, -1);
+ mStateBuilder.addCustomAction(MediaControlView2Impl.ACTION_HIDE_SUBTITLE, null, -1);
+ }
+ mStateBuilder.setState(getCorrespondingPlaybackState(),
+ mInstance.getCurrentPosition(), 1.0f);
+ mStateBuilder.setBufferedPosition(
+ (long) (mCurrentBufferPercentage / 100.0) * mInstance.getDuration());
+
+ // Set PlaybackState for MediaSession
+ if (mMediaSession != null) {
+ PlaybackState state = mStateBuilder.build();
+ mMediaSession.setPlaybackState(state);
+ }
+ }
+
+ private int getCorrespondingPlaybackState() {
+ switch (mCurrentState) {
+ case STATE_ERROR:
+ return PlaybackState.STATE_ERROR;
+ case STATE_IDLE:
+ return PlaybackState.STATE_NONE;
+ case STATE_PREPARING:
+ return PlaybackState.STATE_CONNECTING;
+ case STATE_PREPARED:
+ return PlaybackState.STATE_STOPPED;
+ case STATE_PLAYING:
+ return PlaybackState.STATE_PLAYING;
+ case STATE_PAUSED:
+ return PlaybackState.STATE_PAUSED;
+ case STATE_PLAYBACK_COMPLETED:
+ return PlaybackState.STATE_STOPPED;
+ default:
+ return -1;
+ }
+ }
+
+ private void toggleMediaControlViewVisibility() {
+ if (mMediaControlView.isShowing()) {
+ mMediaControlView.hide();
+ } else {
+ mMediaControlView.show();
+ }
+ }
+
+ private void applySpeed() {
+ PlaybackParams params = mMediaPlayer.getPlaybackParams().allowDefaults();
+ if (mSpeed != params.getSpeed()) {
+ try {
+ params.setSpeed(mSpeed);
+ mMediaPlayer.setPlaybackParams(params);
+ mFallbackSpeed = mSpeed;
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "PlaybackParams has unsupported value: " + e);
+ // TODO: should revise this part after integrating with MP2.
+ // If mSpeed had an illegal value for speed rate, system will determine best
+ // handling (see PlaybackParams.AUDIO_FALLBACK_MODE_DEFAULT).
+ // Note: The pre-MP2 returns 0.0f when it is paused. In this case, VideoView2 will
+ // use mFallbackSpeed instead.
+ float fallbackSpeed = mMediaPlayer.getPlaybackParams().allowDefaults().getSpeed();
+ if (fallbackSpeed > 0.0f) {
+ mFallbackSpeed = fallbackSpeed;
+ }
+ mSpeed = mFallbackSpeed;
+ }
+ }
+ }
+
+ MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener =
+ new MediaPlayer.OnVideoSizeChangedListener() {
+ public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
+ if (DEBUG) {
+ Log.d(TAG, "OnVideoSizeChanged(): size: " + width + "/" + height);
+ }
+ mVideoWidth = mp.getVideoWidth();
+ mVideoHeight = mp.getVideoHeight();
+ if (DEBUG) {
+ Log.d(TAG, "OnVideoSizeChanged(): mVideoSize:" + mVideoWidth + "/"
+ + mVideoHeight);
+ }
+
+ if (mVideoWidth != 0 && mVideoHeight != 0) {
+ mInstance.requestLayout();
+ }
+ }
+ };
+
+ MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() {
+ public void onPrepared(MediaPlayer mp) {
+ if (DEBUG) {
+ Log.d(TAG, "OnPreparedListener(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ mCurrentState = STATE_PREPARED;
+ if (mOnPreparedListener != null) {
+ mOnPreparedListener.onPrepared();
+ }
+ if (mMediaControlView != null) {
+ mMediaControlView.setEnabled(true);
+ }
+ mVideoWidth = mp.getVideoWidth();
+ mVideoHeight = mp.getVideoHeight();
+
+ // mSeekWhenPrepared may be changed after seekTo() call
+ int seekToPosition = mSeekWhenPrepared;
+ if (seekToPosition != 0) {
+ mInstance.seekTo(seekToPosition);
+ }
+
+ // Create and set playback state for MediaControlView2
+ updatePlaybackState();
+
+ // Get and set duration value as MediaMetadata for MediaControlView2
+ MediaMetadata.Builder builder = new MediaMetadata.Builder();
+ builder.putLong(MediaMetadata.METADATA_KEY_DURATION, mInstance.getDuration());
+ if (mMediaSession != null) {
+ mMediaSession.setMetadata(builder.build());
+ }
+
+ if (mVideoWidth != 0 && mVideoHeight != 0) {
+ if (mVideoWidth != mSurfaceWidth || mVideoHeight != mSurfaceHeight) {
+ if (DEBUG) {
+ Log.i(TAG, "OnPreparedListener() : ");
+ Log.i(TAG, " video size: " + mVideoWidth + "/" + mVideoHeight);
+ Log.i(TAG, " surface size: " + mSurfaceWidth + "/" + mSurfaceHeight);
+ Log.i(TAG, " measuredSize: " + mInstance.getMeasuredWidth() + "/"
+ + mInstance.getMeasuredHeight());
+ Log.i(TAG, " viewSize: " + mInstance.getWidth() + "/"
+ + mInstance.getHeight());
+ }
+
+ // TODO: It seems like that overriding onMeasure() is needed like legacy code.
+ mSurfaceWidth = mVideoWidth;
+ mSurfaceHeight = mVideoHeight;
+ mInstance.requestLayout();
+ }
+ if (mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) {
+ // We didn't actually change the size (it was already at the size
+ // we need), so we won't get a "surface changed" callback, so
+ // start the video here instead of in the callback.
+ if (needToStart()) {
+ mInstance.start();
+ if (mMediaControlView != null) {
+ mMediaControlView.show();
+ }
+ } else if (!mInstance.isPlaying() && (seekToPosition != 0
+ || mInstance.getCurrentPosition() > 0)) {
+ if (mMediaControlView != null) {
+ // Show the media controls when we're paused into a video and
+ // make them stick.
+ mMediaControlView.show(0);
+ }
+ }
+ }
+ } else {
+ // We don't know the video size yet, but should start anyway.
+ // The video size might be reported to us later.
+ if (needToStart()) {
+ mInstance.start();
+ }
+ }
+ }
+ };
+
+ private MediaPlayer.OnCompletionListener mCompletionListener =
+ new MediaPlayer.OnCompletionListener() {
+ public void onCompletion(MediaPlayer mp) {
+ mCurrentState = STATE_PLAYBACK_COMPLETED;
+ mTargetState = STATE_PLAYBACK_COMPLETED;
+ updatePlaybackState();
+
+ if (mMediaControlView != null) {
+ mMediaControlView.hide();
+ }
+ if (mOnCompletionListener != null) {
+ mOnCompletionListener.onCompletion();
+ }
+ if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+ mAudioManager.abandonAudioFocus(null);
+ }
+ }
+ };
+
+ private MediaPlayer.OnInfoListener mInfoListener =
+ new MediaPlayer.OnInfoListener() {
+ public boolean onInfo(MediaPlayer mp, int what, int extra) {
+ if (mOnInfoListener != null) {
+ mOnInfoListener.onInfo(what, extra);
+ }
+ return true;
+ }
+ };
+
+ private MediaPlayer.OnErrorListener mErrorListener =
+ new MediaPlayer.OnErrorListener() {
+ public boolean onError(MediaPlayer mp, int frameworkErr, int implErr) {
+ if (DEBUG) {
+ Log.d(TAG, "Error: " + frameworkErr + "," + implErr);
+ }
+ mCurrentState = STATE_ERROR;
+ mTargetState = STATE_ERROR;
+ updatePlaybackState();
+
+ if (mMediaControlView != null) {
+ mMediaControlView.hide();
+ }
+
+ /* If an error handler has been supplied, use it and finish. */
+ if (mOnErrorListener != null) {
+ if (mOnErrorListener.onError(frameworkErr, implErr)) {
+ return true;
+ }
+ }
+
+ /* Otherwise, pop up an error dialog so the user knows that
+ * something bad has happened. Only try and pop up the dialog
+ * if we're attached to a window. When we're going away and no
+ * longer have a window, don't bother showing the user an error.
+ */
+ if (mInstance.getWindowToken() != null) {
+ int messageId;
+
+ if (frameworkErr
+ == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
+ messageId = R.string.VideoView2_error_text_invalid_progressive_playback;
+ } else {
+ messageId = R.string.VideoView2_error_text_unknown;
+ }
+
+ Resources res = ApiHelper.getLibResources();
+ new AlertDialog.Builder(mInstance.getContext())
+ .setMessage(res.getString(messageId))
+ .setPositiveButton(res.getString(R.string.VideoView2_error_button),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int whichButton) {
+ /* If we get here, there is no onError listener, so
+ * at least inform them that the video is over.
+ */
+ if (mOnCompletionListener != null) {
+ mOnCompletionListener.onCompletion();
+ }
+ }
+ })
+ .setCancelable(false)
+ .show();
+ }
+ return true;
+ }
+ };
+
+ private MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener =
+ new MediaPlayer.OnBufferingUpdateListener() {
+ public void onBufferingUpdate(MediaPlayer mp, int percent) {
+ mCurrentBufferPercentage = percent;
+ updatePlaybackState();
+ }
+ };
+
+ private class MediaSessionCallback extends MediaSession.Callback {
+ @Override
+ public void onCommand(String command, Bundle args, ResultReceiver receiver) {
+ switch (command) {
+ case MediaControlView2Impl.ACTION_SHOW_SUBTITLE:
+ mInstance.showSubtitle();
+ break;
+ case MediaControlView2Impl.ACTION_HIDE_SUBTITLE:
+ mInstance.hideSubtitle();
+ break;
+ }
+ }
+
+ @Override
+ public void onPlay() {
+ mInstance.start();
+ }
+
+ @Override
+ public void onPause() {
+ mInstance.pause();
+ }
+
+ @Override
+ public void onSeekTo(long pos) {
+ mInstance.seekTo((int) pos);
+ }
}
}
diff --git a/services/audioflinger/AudioFlinger.cpp b/services/audioflinger/AudioFlinger.cpp
index 4d5e094..a9d0054 100644
--- a/services/audioflinger/AudioFlinger.cpp
+++ b/services/audioflinger/AudioFlinger.cpp
@@ -262,6 +262,7 @@
audio_config_base_t *config,
const AudioClient& client,
audio_port_handle_t *deviceId,
+ audio_session_t *sessionId,
const sp<MmapStreamCallback>& callback,
sp<MmapStreamInterface>& interface,
audio_port_handle_t *handle)
@@ -274,7 +275,8 @@
status_t ret = NO_INIT;
if (af != 0) {
ret = af->openMmapStream(
- direction, attr, config, client, deviceId, callback, interface, handle);
+ direction, attr, config, client, deviceId,
+ sessionId, callback, interface, handle);
}
return ret;
}
@@ -284,6 +286,7 @@
audio_config_base_t *config,
const AudioClient& client,
audio_port_handle_t *deviceId,
+ audio_session_t *sessionId,
const sp<MmapStreamCallback>& callback,
sp<MmapStreamInterface>& interface,
audio_port_handle_t *handle)
@@ -292,8 +295,10 @@
if (ret != NO_ERROR) {
return ret;
}
-
- audio_session_t sessionId = (audio_session_t) newAudioUniqueId(AUDIO_UNIQUE_ID_USE_SESSION);
+ audio_session_t actualSessionId = *sessionId;
+ if (actualSessionId == AUDIO_SESSION_ALLOCATE) {
+ actualSessionId = (audio_session_t) newAudioUniqueId(AUDIO_UNIQUE_ID_USE_SESSION);
+ }
audio_stream_type_t streamType = AUDIO_STREAM_DEFAULT;
audio_io_handle_t io = AUDIO_IO_HANDLE_NONE;
audio_port_handle_t portId = AUDIO_PORT_HANDLE_NONE;
@@ -303,7 +308,7 @@
fullConfig.channel_mask = config->channel_mask;
fullConfig.format = config->format;
ret = AudioSystem::getOutputForAttr(attr, &io,
- sessionId,
+ actualSessionId,
&streamType, client.clientUid,
&fullConfig,
(audio_output_flags_t)(AUDIO_OUTPUT_FLAG_MMAP_NOIRQ |
@@ -311,7 +316,7 @@
deviceId, &portId);
} else {
ret = AudioSystem::getInputForAttr(attr, &io,
- sessionId,
+ actualSessionId,
client.clientPid,
client.clientUid,
config,
@@ -326,13 +331,14 @@
sp<MmapThread> thread = mMmapThreads.valueFor(io);
if (thread != 0) {
interface = new MmapThreadHandle(thread);
- thread->configure(attr, streamType, sessionId, callback, *deviceId, portId);
+ thread->configure(attr, streamType, actualSessionId, callback, *deviceId, portId);
*handle = portId;
+ *sessionId = actualSessionId;
} else {
if (direction == MmapStreamInterface::DIRECTION_OUTPUT) {
- AudioSystem::releaseOutput(io, streamType, sessionId);
+ AudioSystem::releaseOutput(io, streamType, actualSessionId);
} else {
- AudioSystem::releaseInput(io, sessionId);
+ AudioSystem::releaseInput(io, actualSessionId);
}
ret = NO_INIT;
}
@@ -987,6 +993,21 @@
return mute;
}
+void AudioFlinger::setRecordSilenced(uid_t uid, bool silenced)
+{
+ ALOGV("AudioFlinger::setRecordSilenced(uid:%d, silenced:%d)", uid, silenced);
+
+ // TODO: Notify MmapThreads
+
+ AutoMutex lock(mLock);
+ for (size_t i = 0; i < mRecordThreads.size(); i++) {
+ sp<RecordThread> thread = mRecordThreads.valueAt(i);
+ if (thread != 0) {
+ thread->setRecordSilenced(uid, silenced);
+ }
+ }
+}
+
status_t AudioFlinger::setMasterMute(bool muted)
{
status_t ret = initCheck();
diff --git a/services/audioflinger/AudioFlinger.h b/services/audioflinger/AudioFlinger.h
index bc73ffd..a1c3f36 100644
--- a/services/audioflinger/AudioFlinger.h
+++ b/services/audioflinger/AudioFlinger.h
@@ -147,6 +147,8 @@
virtual status_t setMicMute(bool state);
virtual bool getMicMute() const;
+ virtual void setRecordSilenced(uid_t uid, bool silenced);
+
virtual status_t setParameters(audio_io_handle_t ioHandle, const String8& keyValuePairs);
virtual String8 getParameters(audio_io_handle_t ioHandle, const String8& keys) const;
@@ -271,6 +273,7 @@
audio_config_base_t *config,
const AudioClient& client,
audio_port_handle_t *deviceId,
+ audio_session_t *sessionId,
const sp<MmapStreamCallback>& callback,
sp<MmapStreamInterface>& interface,
audio_port_handle_t *handle);
diff --git a/services/audioflinger/Configuration.h b/services/audioflinger/Configuration.h
index 6e0f2b6..ede8e3f 100644
--- a/services/audioflinger/Configuration.h
+++ b/services/audioflinger/Configuration.h
@@ -47,6 +47,9 @@
#ifdef FLOAT_EFFECT_CHAIN
// define FLOAT_AUX to process aux effect buffers in float (FLOAT_EFFECT_CHAIN must be defined)
#define FLOAT_AUX
+
+// define MULTICHANNEL_EFFECT_CHAIN to allow multichannel effects (FLOAT_EFFECT_CHAIN defined)
+#define MULTICHANNEL_EFFECT_CHAIN
#endif
#endif // ANDROID_AUDIOFLINGER_CONFIGURATION_H
diff --git a/services/audioflinger/Effects.cpp b/services/audioflinger/Effects.cpp
index b4ff0d6..b13e551 100644
--- a/services/audioflinger/Effects.cpp
+++ b/services/audioflinger/Effects.cpp
@@ -26,6 +26,7 @@
#include <system/audio_effects/effect_aec.h>
#include <system/audio_effects/effect_ns.h>
#include <system/audio_effects/effect_visualizer.h>
+#include <audio_utils/channels.h>
#include <audio_utils/primitives.h>
#include <media/AudioEffect.h>
#include <media/audiohal/EffectHalInterface.h>
@@ -292,7 +293,6 @@
return;
}
- // TODO: Implement multichannel effects; here outChannelCount == FCC_2 == 2
const uint32_t inChannelCount =
audio_channel_count_from_out_mask(mConfig.inputCfg.channels);
const uint32_t outChannelCount =
@@ -342,6 +342,7 @@
if (isProcessImplemented()) {
if (auxType) {
// We overwrite the aux input buffer here and clear after processing.
+ // aux input is always mono.
#ifdef FLOAT_EFFECT_CHAIN
if (mSupportsFloat) {
#ifndef FLOAT_AUX
@@ -371,6 +372,28 @@
}
}
#ifdef FLOAT_EFFECT_CHAIN
+ sp<EffectBufferHalInterface> inBuffer = mInBuffer;
+ sp<EffectBufferHalInterface> outBuffer = mOutBuffer;
+
+ if (!auxType && mInChannelCountRequested != inChannelCount) {
+ adjust_channels(
+ inBuffer->audioBuffer()->f32, mInChannelCountRequested,
+ mInConversionBuffer->audioBuffer()->f32, inChannelCount,
+ sizeof(float),
+ sizeof(float)
+ * mInChannelCountRequested * mConfig.inputCfg.buffer.frameCount);
+ inBuffer = mInConversionBuffer;
+ }
+ if (mConfig.outputCfg.accessMode == EFFECT_BUFFER_ACCESS_ACCUMULATE
+ && mOutChannelCountRequested != outChannelCount) {
+ adjust_selected_channels(
+ outBuffer->audioBuffer()->f32, mOutChannelCountRequested,
+ mOutConversionBuffer->audioBuffer()->f32, outChannelCount,
+ sizeof(float),
+ sizeof(float)
+ * mOutChannelCountRequested * mConfig.outputCfg.buffer.frameCount);
+ outBuffer = mOutConversionBuffer;
+ }
if (!mSupportsFloat) { // convert input to int16_t as effect doesn't support float.
if (!auxType) {
if (mInConversionBuffer.get() == nullptr) {
@@ -379,8 +402,9 @@
}
memcpy_to_i16_from_float(
mInConversionBuffer->audioBuffer()->s16,
- mInBuffer->audioBuffer()->f32,
+ inBuffer->audioBuffer()->f32,
inChannelCount * mConfig.inputCfg.buffer.frameCount);
+ inBuffer = mInConversionBuffer;
}
if (mConfig.outputCfg.accessMode == EFFECT_BUFFER_ACCESS_ACCUMULATE) {
if (mOutConversionBuffer.get() == nullptr) {
@@ -389,21 +413,30 @@
}
memcpy_to_i16_from_float(
mOutConversionBuffer->audioBuffer()->s16,
- mOutBuffer->audioBuffer()->f32,
+ outBuffer->audioBuffer()->f32,
outChannelCount * mConfig.outputCfg.buffer.frameCount);
+ outBuffer = mOutConversionBuffer;
}
}
#endif
-
ret = mEffectInterface->process();
-
#ifdef FLOAT_EFFECT_CHAIN
if (!mSupportsFloat) { // convert output int16_t back to float.
+ sp<EffectBufferHalInterface> target =
+ mOutChannelCountRequested != outChannelCount
+ ? mOutConversionBuffer : mOutBuffer;
+
memcpy_to_float_from_i16(
- mOutBuffer->audioBuffer()->f32,
+ target->audioBuffer()->f32,
mOutConversionBuffer->audioBuffer()->s16,
outChannelCount * mConfig.outputCfg.buffer.frameCount);
}
+ if (mOutChannelCountRequested != outChannelCount) {
+ adjust_selected_channels(mOutConversionBuffer->audioBuffer()->f32, outChannelCount,
+ mOutBuffer->audioBuffer()->f32, mOutChannelCountRequested,
+ sizeof(float),
+ sizeof(float) * outChannelCount * mConfig.outputCfg.buffer.frameCount);
+ }
#endif
} else {
#ifdef FLOAT_EFFECT_CHAIN
@@ -476,15 +509,28 @@
}
// TODO: handle configuration of effects replacing track process
+ // TODO: handle configuration of input (record) SW effects above the HAL,
+ // similar to output EFFECT_FLAG_TYPE_INSERT/REPLACE,
+ // in which case input channel masks should be used here.
channelMask = thread->channelMask();
+ mConfig.inputCfg.channels = channelMask;
mConfig.outputCfg.channels = channelMask;
if ((mDescriptor.flags & EFFECT_FLAG_TYPE_MASK) == EFFECT_FLAG_TYPE_AUXILIARY) {
- mConfig.inputCfg.channels = AUDIO_CHANNEL_OUT_MONO;
- mConfig.outputCfg.channels = AUDIO_CHANNEL_OUT_STEREO;
- ALOGV("Overriding auxiliary effect input as MONO and output as STEREO");
+ if (mConfig.inputCfg.channels != AUDIO_CHANNEL_OUT_MONO) {
+ mConfig.inputCfg.channels = AUDIO_CHANNEL_OUT_MONO;
+ ALOGV("Overriding auxiliary effect input channels %#x as MONO",
+ mConfig.inputCfg.channels);
+ }
+#ifndef MULTICHANNEL_EFFECT_CHAIN
+ if (mConfig.outputCfg.channels != AUDIO_CHANNEL_OUT_STEREO) {
+ mConfig.outputCfg.channels = AUDIO_CHANNEL_OUT_STEREO;
+ ALOGV("Overriding auxiliary effect output channels %#x as STEREO",
+ mConfig.outputCfg.channels);
+ }
+#endif
} else {
- mConfig.inputCfg.channels = channelMask;
+#ifndef MULTICHANNEL_EFFECT_CHAIN
// TODO: Update this logic when multichannel effects are implemented.
// For offloaded tracks consider mono output as stereo for proper effect initialization
if (channelMask == AUDIO_CHANNEL_OUT_MONO) {
@@ -492,7 +538,12 @@
mConfig.outputCfg.channels = AUDIO_CHANNEL_OUT_STEREO;
ALOGV("Overriding effect input and output as STEREO");
}
+#endif
}
+ mInChannelCountRequested =
+ audio_channel_count_from_out_mask(mConfig.inputCfg.channels);
+ mOutChannelCountRequested =
+ audio_channel_count_from_out_mask(mConfig.outputCfg.channels);
mConfig.inputCfg.format = EFFECT_BUFFER_FORMAT;
mConfig.outputCfg.format = EFFECT_BUFFER_FORMAT;
@@ -530,28 +581,58 @@
status_t cmdStatus;
size = sizeof(int);
status = mEffectInterface->command(EFFECT_CMD_SET_CONFIG,
- sizeof(effect_config_t),
+ sizeof(mConfig),
&mConfig,
&size,
&cmdStatus);
if (status == NO_ERROR) {
status = cmdStatus;
-#ifdef FLOAT_EFFECT_CHAIN
- mSupportsFloat = true;
-#endif
}
-#ifdef FLOAT_EFFECT_CHAIN
- else {
- ALOGV("EFFECT_CMD_SET_CONFIG failed with float format, retry with int16_t.");
- mConfig.inputCfg.format = AUDIO_FORMAT_PCM_16_BIT;
- mConfig.outputCfg.format = AUDIO_FORMAT_PCM_16_BIT;
+
+#ifdef MULTICHANNEL_EFFECT_CHAIN
+ if (status != NO_ERROR &&
+ (mConfig.inputCfg.channels != AUDIO_CHANNEL_OUT_STEREO
+ || mConfig.outputCfg.channels != AUDIO_CHANNEL_OUT_STEREO)) {
+ // Older effects may require exact STEREO position mask.
+ if (mConfig.inputCfg.channels != AUDIO_CHANNEL_OUT_STEREO) {
+ ALOGV("Overriding effect input channels %#x as STEREO", mConfig.inputCfg.channels);
+ mConfig.inputCfg.channels = AUDIO_CHANNEL_OUT_STEREO;
+ }
+ if (mConfig.outputCfg.channels != AUDIO_CHANNEL_OUT_STEREO) {
+ ALOGV("Overriding effect output channels %#x as STEREO", mConfig.outputCfg.channels);
+ mConfig.outputCfg.channels = AUDIO_CHANNEL_OUT_STEREO;
+ }
+ size = sizeof(int);
status = mEffectInterface->command(EFFECT_CMD_SET_CONFIG,
- sizeof(effect_config_t),
+ sizeof(mConfig),
&mConfig,
&size,
&cmdStatus);
if (status == NO_ERROR) {
status = cmdStatus;
+ }
+ }
+#endif
+
+#ifdef FLOAT_EFFECT_CHAIN
+ if (status == NO_ERROR) {
+ mSupportsFloat = true;
+ }
+
+ if (status != NO_ERROR) {
+ ALOGV("EFFECT_CMD_SET_CONFIG failed with float format, retry with int16_t.");
+ mConfig.inputCfg.format = AUDIO_FORMAT_PCM_16_BIT;
+ mConfig.outputCfg.format = AUDIO_FORMAT_PCM_16_BIT;
+ size = sizeof(int);
+ status = mEffectInterface->command(EFFECT_CMD_SET_CONFIG,
+ sizeof(mConfig),
+ &mConfig,
+ &size,
+ &cmdStatus);
+ if (status == NO_ERROR) {
+ status = cmdStatus;
+ }
+ if (status == NO_ERROR) {
mSupportsFloat = false;
ALOGVV("config worked with 16 bit");
} else {
@@ -929,11 +1010,15 @@
// the original buffer) when the output buffer is identical to the input buffer,
// but we don't optimize for it here.
const bool auxType = (mDescriptor.flags & EFFECT_FLAG_TYPE_MASK) == EFFECT_FLAG_TYPE_AUXILIARY;
- if (!auxType && !mSupportsFloat && mInBuffer.get() != nullptr) {
+ const uint32_t inChannelCount =
+ audio_channel_count_from_out_mask(mConfig.inputCfg.channels);
+ const bool formatMismatch = !mSupportsFloat || mInChannelCountRequested != inChannelCount;
+ if (!auxType && formatMismatch && mInBuffer.get() != nullptr) {
// we need to translate - create hidl shared buffer and intercept
const size_t inFrameCount = mConfig.inputCfg.buffer.frameCount;
- const int inChannels = audio_channel_count_from_out_mask(mConfig.inputCfg.channels);
- const size_t size = inChannels * inFrameCount * sizeof(int16_t);
+ // Use FCC_2 in case mInChannelCountRequested is mono and the effect is stereo.
+ const uint32_t inChannels = std::max((uint32_t)FCC_2, mInChannelCountRequested);
+ const size_t size = inChannels * inFrameCount * std::max(sizeof(int16_t), sizeof(float));
ALOGV("%s: setInBuffer updating for inChannels:%d inFrameCount:%zu total size:%zu",
__func__, inChannels, inFrameCount, size);
@@ -970,10 +1055,14 @@
#ifdef FLOAT_EFFECT_CHAIN
// Note: Any effect that does not accumulate does not need mOutConversionBuffer and
// can do in-place conversion from int16_t to float. We don't optimize here.
- if (!mSupportsFloat && mOutBuffer.get() != nullptr) {
+ const uint32_t outChannelCount =
+ audio_channel_count_from_out_mask(mConfig.outputCfg.channels);
+ const bool formatMismatch = !mSupportsFloat || mOutChannelCountRequested != outChannelCount;
+ if (formatMismatch && mOutBuffer.get() != nullptr) {
const size_t outFrameCount = mConfig.outputCfg.buffer.frameCount;
- const int outChannels = audio_channel_count_from_out_mask(mConfig.outputCfg.channels);
- const size_t size = outChannels * outFrameCount * sizeof(int16_t);
+ // Use FCC_2 in case mOutChannelCountRequested is mono and the effect is stereo.
+ const uint32_t outChannels = std::max((uint32_t)FCC_2, mOutChannelCountRequested);
+ const size_t size = outChannels * outFrameCount * std::max(sizeof(int16_t), sizeof(float));
ALOGV("%s: setOutBuffer updating for outChannels:%d outFrameCount:%zu total size:%zu",
__func__, outChannels, outFrameCount, size);
@@ -1813,14 +1902,8 @@
if (mInBuffer == NULL) {
return;
}
- // TODO: This will change in the future, depending on multichannel
- // and sample format changes for effects.
- // Currently effects processing is only available for stereo, AUDIO_FORMAT_PCM_16_BIT
- // (4 bytes frame size)
-
const size_t frameSize =
- audio_bytes_per_sample(EFFECT_BUFFER_FORMAT)
- * std::min((uint32_t)FCC_2, thread->channelCount());
+ audio_bytes_per_sample(EFFECT_BUFFER_FORMAT) * thread->channelCount();
memset(mInBuffer->audioBuffer()->raw, 0, thread->frameCount() * frameSize);
mInBuffer->commit();
diff --git a/services/audioflinger/Effects.h b/services/audioflinger/Effects.h
index eea3208..2327bb9 100644
--- a/services/audioflinger/Effects.h
+++ b/services/audioflinger/Effects.h
@@ -173,6 +173,8 @@
bool mSupportsFloat; // effect supports float processing
sp<EffectBufferHalInterface> mInConversionBuffer; // Buffers for HAL conversion if needed.
sp<EffectBufferHalInterface> mOutConversionBuffer;
+ uint32_t mInChannelCountRequested;
+ uint32_t mOutChannelCountRequested;
#endif
};
diff --git a/services/audioflinger/RecordTracks.h b/services/audioflinger/RecordTracks.h
index f8da780..63a3d98 100644
--- a/services/audioflinger/RecordTracks.h
+++ b/services/audioflinger/RecordTracks.h
@@ -63,6 +63,9 @@
virtual bool isFastTrack() const { return (mFlags & AUDIO_INPUT_FLAG_FAST) != 0; }
+ void setSilenced(bool silenced) { mSilenced = silenced; }
+ bool isSilenced() const { return mSilenced; }
+
private:
friend class AudioFlinger; // for mState
@@ -91,6 +94,8 @@
// used by the record thread to convert frames to proper destination format
RecordBufferConverter *mRecordBufferConverter;
audio_input_flags_t mFlags;
+
+ bool mSilenced;
};
// playback track, used by PatchPanel
diff --git a/services/audioflinger/Threads.cpp b/services/audioflinger/Threads.cpp
index d5def48..8bf50b1 100644
--- a/services/audioflinger/Threads.cpp
+++ b/services/audioflinger/Threads.cpp
@@ -1176,6 +1176,7 @@
switch (mType) {
case MIXER: {
+#ifndef MULTICHANNEL_EFFECT_CHAIN
// Reject any effect on mixer multichannel sinks.
// TODO: fix both format and multichannel issues with effects.
if (mChannelCount != FCC_2) {
@@ -1183,6 +1184,7 @@
" thread %s", desc->name, mChannelCount, mThreadName);
return BAD_VALUE;
}
+#endif
audio_output_flags_t flags = mOutput->flags;
if (hasFastMixer() || (flags & AUDIO_OUTPUT_FLAG_FAST)) {
if (sessionId == AUDIO_SESSION_OUTPUT_MIX) {
@@ -1229,6 +1231,7 @@
desc->name, mThreadName);
return BAD_VALUE;
case DUPLICATING:
+#ifndef MULTICHANNEL_EFFECT_CHAIN
// Reject any effect on mixer multichannel sinks.
// TODO: fix both format and multichannel issues with effects.
if (mChannelCount != FCC_2) {
@@ -1236,6 +1239,7 @@
" on DUPLICATING thread %s", desc->name, mChannelCount, mThreadName);
return BAD_VALUE;
}
+#endif
if ((sessionId == AUDIO_SESSION_OUTPUT_STAGE) || (sessionId == AUDIO_SESSION_OUTPUT_MIX)) {
ALOGW("checkEffectCompatibility_l(): global effect %s on DUPLICATING"
" thread %s", desc->name, mThreadName);
@@ -6528,6 +6532,7 @@
rear = mRsmpInRear += framesRead;
size = activeTracks.size();
+
// loop over each active track
for (size_t i = 0; i < size; i++) {
activeTrack = activeTracks[i];
@@ -6584,6 +6589,11 @@
if (activeTrack->mFramesToDrop == 0) {
if (framesOut > 0) {
activeTrack->mSink.frameCount = framesOut;
+ // Sanitize before releasing if the track has no access to the source data
+ // An idle UID receives silence from non virtual devices until active
+ if (activeTrack->isSilenced()) {
+ memset(activeTrack->mSink.raw, 0, framesOut * mFrameSize);
+ }
activeTrack->releaseBuffer(&activeTrack->mSink);
}
} else {
@@ -6923,7 +6933,9 @@
status_t status = NO_ERROR;
if (recordTrack->isExternalTrack()) {
mLock.unlock();
- status = AudioSystem::startInput(mId, recordTrack->sessionId());
+ bool silenced;
+ status = AudioSystem::startInput(mId, recordTrack->sessionId(),
+ mInDevice, recordTrack->uid(), &silenced);
mLock.lock();
// FIXME should verify that recordTrack is still in mActiveTracks
if (status != NO_ERROR) {
@@ -6932,6 +6944,7 @@
ALOGV("RecordThread::start error %d", status);
return status;
}
+ recordTrack->setSilenced(silenced);
}
// Catch up with current buffer indices if thread is already running.
// This is what makes a new client discard all buffered data. If the track's mRsmpInFront
@@ -7135,6 +7148,16 @@
write(fd, result.string(), result.size());
}
+void AudioFlinger::RecordThread::setRecordSilenced(uid_t uid, bool silenced)
+{
+ Mutex::Autolock _l(mLock);
+ for (size_t i = 0; i < mTracks.size() ; i++) {
+ sp<RecordTrack> track = mTracks[i];
+ if (track != 0 && track->uid() == uid) {
+ track->setSilenced(silenced);
+ }
+ }
+}
void AudioFlinger::RecordThread::ResamplerBufferProvider::reset()
{
@@ -7827,7 +7850,9 @@
if (isOutput()) {
ret = AudioSystem::startOutput(mId, streamType(), mSessionId);
} else {
- ret = AudioSystem::startInput(mId, mSessionId);
+ // TODO: Block recording for idle UIDs (b/72134552)
+ bool silenced;
+ ret = AudioSystem::startInput(mId, mSessionId, mInDevice, client.clientUid, &silenced);
}
// abort if start is rejected by audio policy manager
diff --git a/services/audioflinger/Threads.h b/services/audioflinger/Threads.h
index 17f26c5..41d87a4 100644
--- a/services/audioflinger/Threads.h
+++ b/services/audioflinger/Threads.h
@@ -1396,6 +1396,9 @@
void checkBtNrec();
+ // Sets the UID records silence
+ void setRecordSilenced(uid_t uid, bool silenced);
+
private:
// Enter standby if not already in standby, and set mStandby flag
void standbyIfNotAlreadyInStandby();
diff --git a/services/audiopolicy/AudioPolicyInterface.h b/services/audiopolicy/AudioPolicyInterface.h
index f2cb25f..2a8f54d 100644
--- a/services/audiopolicy/AudioPolicyInterface.h
+++ b/services/audiopolicy/AudioPolicyInterface.h
@@ -65,15 +65,15 @@
API_INPUT_TELEPHONY_RX, // used for capture from telephony RX path
} input_type_t;
- enum {
+ enum {
API_INPUT_CONCURRENCY_NONE = 0,
API_INPUT_CONCURRENCY_CALL = (1 << 0), // Concurrency with a call
API_INPUT_CONCURRENCY_CAPTURE = (1 << 1), // Concurrency with another capture
API_INPUT_CONCURRENCY_ALL = (API_INPUT_CONCURRENCY_CALL | API_INPUT_CONCURRENCY_CAPTURE),
- };
+ };
- typedef uint32_t concurrency_type__mask_t;
+ typedef uint32_t concurrency_type__mask_t;
public:
virtual ~AudioPolicyInterface() {}
@@ -145,6 +145,7 @@
// indicates to the audio policy manager that the input starts being used.
virtual status_t startInput(audio_io_handle_t input,
audio_session_t session,
+ bool silenced,
concurrency_type__mask_t *concurrency) = 0;
// indicates to the audio policy manager that the input stops being used.
virtual status_t stopInput(audio_io_handle_t input,
@@ -239,6 +240,8 @@
virtual float getStreamVolumeDB(
audio_stream_type_t stream, int index, audio_devices_t device) = 0;
+
+ virtual void setRecordSilenced(uid_t uid, bool silenced);
};
diff --git a/services/audiopolicy/common/managerdefinitions/include/AudioSession.h b/services/audiopolicy/common/managerdefinitions/include/AudioSession.h
index 0d19373..dd5247d 100644
--- a/services/audiopolicy/common/managerdefinitions/include/AudioSession.h
+++ b/services/audiopolicy/common/managerdefinitions/include/AudioSession.h
@@ -55,6 +55,8 @@
void setUid(uid_t uid) { mRecordClientInfo.uid = uid; }
bool matches(const sp<AudioSession> &other) const;
bool isSoundTrigger() const { return mIsSoundTrigger; }
+ void setSilenced(bool silenced) { mSilenced = silenced; }
+ bool isSilenced() const { return mSilenced; }
uint32_t openCount() const { return mOpenCount; } ;
uint32_t activeCount() const { return mActiveCount; } ;
@@ -70,6 +72,7 @@
const struct audio_config_base mConfig;
const audio_input_flags_t mFlags;
bool mIsSoundTrigger;
+ bool mSilenced;
uint32_t mOpenCount;
uint32_t mActiveCount;
AudioMix* mPolicyMix; // non NULL when used by a dynamic policy
diff --git a/services/audiopolicy/managerdefault/AudioPolicyManager.cpp b/services/audiopolicy/managerdefault/AudioPolicyManager.cpp
index 68730a5..78a02b2 100644
--- a/services/audiopolicy/managerdefault/AudioPolicyManager.cpp
+++ b/services/audiopolicy/managerdefault/AudioPolicyManager.cpp
@@ -1801,10 +1801,15 @@
status_t AudioPolicyManager::startInput(audio_io_handle_t input,
audio_session_t session,
+ bool silenced,
concurrency_type__mask_t *concurrency)
{
- ALOGV("startInput() input %d", input);
+
+ ALOGV("AudioPolicyManager::startInput(input:%d, session:%d, silenced:%d, concurrency:%d)",
+ input, session, silenced, *concurrency);
+
*concurrency = API_INPUT_CONCURRENCY_NONE;
+
ssize_t index = mInputs.indexOfKey(input);
if (index < 0) {
ALOGW("startInput() unknown input %d", input);
@@ -1839,12 +1844,33 @@
return INVALID_OPERATION;
}
- Vector< sp<AudioInputDescriptor> > activeInputs = mInputs.getActiveInputs();
- for (const auto& activeDesc : activeInputs) {
- if (is_virtual_input_device(activeDesc->mDevice)) {
- continue;
- }
+ Vector<sp<AudioInputDescriptor>> activeInputs = mInputs.getActiveInputs();
+ // If a UID is idle and records silence and another not silenced recording starts
+ // from another UID (idle or active) we stop the current idle UID recording in
+ // favor of the new one - "There can be only one" TM
+ if (!silenced) {
+ for (const auto& activeDesc : activeInputs) {
+ if ((audioSession->flags() & AUDIO_INPUT_FLAG_MMAP_NOIRQ) != 0 &&
+ activeDesc->getId() == inputDesc->getId()) {
+ continue;
+ }
+
+ AudioSessionCollection activeSessions = activeDesc->getAudioSessions(
+ true /*activeOnly*/);
+ sp<AudioSession> activeSession = activeSessions.valueAt(0);
+ if (activeSession->isSilenced()) {
+ audio_io_handle_t activeInput = activeDesc->mIoHandle;
+ audio_session_t activeSessionId = activeSession->session();
+ stopInput(activeInput, activeSessionId);
+ releaseInput(activeInput, activeSessionId);
+ ALOGV("startInput(%d) stopping silenced input %d", input, activeInput);
+ activeInputs = mInputs.getActiveInputs();
+ }
+ }
+ }
+
+ for (const auto& activeDesc : activeInputs) {
if ((audioSession->flags() & AUDIO_INPUT_FLAG_MMAP_NOIRQ) != 0 &&
activeDesc->getId() == inputDesc->getId()) {
continue;
@@ -1881,10 +1907,6 @@
// if capture is allowed, preempt currently active HOTWORD captures
for (const auto& activeDesc : activeInputs) {
- if (is_virtual_input_device(activeDesc->mDevice)) {
- continue;
- }
-
if (allowConcurrentWithSoundTrigger && activeDesc->isSoundTrigger()) {
continue;
}
@@ -1907,6 +1929,9 @@
}
#endif
+ // Make sure we start with the correct silence state
+ audioSession->setSilenced(silenced);
+
// increment activity count before calling getNewInputDevice() below as only active sessions
// are considered for device selection
audioSession->changeActiveCount(1);
@@ -2039,7 +2064,6 @@
void AudioPolicyManager::releaseInput(audio_io_handle_t input,
audio_session_t session)
{
-
ALOGV("releaseInput() %d", input);
ssize_t index = mInputs.indexOfKey(input);
if (index < 0) {
@@ -2123,7 +2147,10 @@
audio_devices_t device)
{
- if ((index < mVolumeCurves->getVolumeIndexMin(stream)) ||
+ // VOICE_CALL stream has minVolumeIndex > 0 but can be muted directly by an
+ // app that has MODIFY_PHONE_STATE permission.
+ if (((index < mVolumeCurves->getVolumeIndexMin(stream)) &&
+ !(stream == AUDIO_STREAM_VOICE_CALL && index == 0)) ||
(index > mVolumeCurves->getVolumeIndexMax(stream))) {
return BAD_VALUE;
}
@@ -3391,6 +3418,23 @@
return computeVolume(stream, index, device);
}
+void AudioPolicyManager::setRecordSilenced(uid_t uid, bool silenced)
+{
+ ALOGV("AudioPolicyManager:setRecordSilenced(uid:%d, silenced:%d)", uid, silenced);
+
+ Vector<sp<AudioInputDescriptor> > activeInputs = mInputs.getActiveInputs();
+ for (size_t i = 0; i < activeInputs.size(); i++) {
+ sp<AudioInputDescriptor> activeDesc = activeInputs[i];
+ AudioSessionCollection activeSessions = activeDesc->getAudioSessions(true);
+ for (size_t j = 0; j < activeSessions.size(); j++) {
+ sp<AudioSession> activeSession = activeSessions.valueAt(j);
+ if (activeSession->uid() == uid) {
+ activeSession->setSilenced(silenced);
+ }
+ }
+ }
+}
+
status_t AudioPolicyManager::disconnectAudioSource(const sp<AudioSourceDescriptor>& sourceDesc)
{
ALOGV("%s handle %d", __FUNCTION__, sourceDesc->getHandle());
diff --git a/services/audiopolicy/managerdefault/AudioPolicyManager.h b/services/audiopolicy/managerdefault/AudioPolicyManager.h
index 611edec..4fd73e6 100644
--- a/services/audiopolicy/managerdefault/AudioPolicyManager.h
+++ b/services/audiopolicy/managerdefault/AudioPolicyManager.h
@@ -135,6 +135,7 @@
// indicates to the audio policy manager that the input starts being used.
virtual status_t startInput(audio_io_handle_t input,
audio_session_t session,
+ bool silenced,
concurrency_type__mask_t *concurrency);
// indicates to the audio policy manager that the input stops being used.
@@ -235,6 +236,8 @@
// return the strategy corresponding to a given stream type
routing_strategy getStrategy(audio_stream_type_t stream) const;
+ virtual void setRecordSilenced(uid_t uid, bool silenced);
+
protected:
// A constructor that allows more fine-grained control over initialization process,
// used in automatic tests.
diff --git a/services/audiopolicy/service/AudioPolicyInterfaceImpl.cpp b/services/audiopolicy/service/AudioPolicyInterfaceImpl.cpp
index 7dd6d70..dc676d0 100644
--- a/services/audiopolicy/service/AudioPolicyInterfaceImpl.cpp
+++ b/services/audiopolicy/service/AudioPolicyInterfaceImpl.cpp
@@ -362,14 +362,22 @@
}
status_t AudioPolicyService::startInput(audio_io_handle_t input,
- audio_session_t session)
+ audio_session_t session,
+ audio_devices_t device,
+ uid_t uid,
+ bool *silenced)
{
+ // If UID inactive it records silence until becoming active
+ *silenced = !mUidPolicy->isUidActive(uid) && !is_virtual_input_device(device);
+
if (mAudioPolicyManager == NULL) {
return NO_INIT;
}
+
Mutex::Autolock _l(mLock);
- AudioPolicyInterface::concurrency_type__mask_t concurrency;
- status_t status = mAudioPolicyManager->startInput(input, session, &concurrency);
+ AudioPolicyInterface::concurrency_type__mask_t concurrency =
+ AudioPolicyInterface::API_INPUT_CONCURRENCY_NONE;
+ status_t status = mAudioPolicyManager->startInput(input, session, silenced, &concurrency);
if (status == NO_ERROR) {
LOG_ALWAYS_FATAL_IF(concurrency & ~AudioPolicyInterface::API_INPUT_CONCURRENCY_ALL,
diff --git a/services/audiopolicy/service/AudioPolicyService.cpp b/services/audiopolicy/service/AudioPolicyService.cpp
index af0c823..e5aed9a 100644
--- a/services/audiopolicy/service/AudioPolicyService.cpp
+++ b/services/audiopolicy/service/AudioPolicyService.cpp
@@ -28,6 +28,9 @@
#include <utils/Log.h>
#include <cutils/properties.h>
#include <binder/IPCThreadState.h>
+#include <binder/ActivityManager.h>
+#include <binder/PermissionController.h>
+#include <binder/IResultReceiver.h>
#include <utils/String16.h>
#include <utils/threads.h>
#include "AudioPolicyService.h"
@@ -39,6 +42,8 @@
#include <system/audio.h>
#include <system/audio_policy.h>
+#include <private/android_filesystem_config.h>
+
namespace android {
static const char kDeadlockedString[] = "AudioPolicyService may be deadlocked\n";
@@ -49,6 +54,7 @@
static const nsecs_t kAudioCommandTimeoutNs = seconds(3); // 3 seconds
+static const String16 sManageAudioPolicyPermission("android.permission.MANAGE_AUDIO_POLICY");
// ----------------------------------------------------------------------------
@@ -79,6 +85,9 @@
Mutex::Autolock _l(mLock);
mAudioPolicyEffects = audioPolicyEffects;
}
+
+ mUidPolicy = new UidPolicy(this);
+ mUidPolicy->registerSelf();
}
AudioPolicyService::~AudioPolicyService()
@@ -92,6 +101,9 @@
mNotificationClients.clear();
mAudioPolicyEffects.clear();
+
+ mUidPolicy->unregisterSelf();
+ mUidPolicy.clear();
}
// A notification client is always registered by AudioSystem when the client process
@@ -318,6 +330,20 @@
return NO_ERROR;
}
+void AudioPolicyService::setRecordSilenced(uid_t uid, bool silenced)
+{
+ {
+ Mutex::Autolock _l(mLock);
+ if (mAudioPolicyManager) {
+ mAudioPolicyManager->setRecordSilenced(uid, silenced);
+ }
+ }
+ sp<IAudioFlinger> af = AudioSystem::get_audio_flinger();
+ if (af) {
+ af->setRecordSilenced(uid, silenced);
+ }
+}
+
status_t AudioPolicyService::dump(int fd, const Vector<String16>& args __unused)
{
if (!dumpAllowed()) {
@@ -361,11 +387,210 @@
}
status_t AudioPolicyService::onTransact(
- uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
-{
+ uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) {
+ switch (code) {
+ case SHELL_COMMAND_TRANSACTION: {
+ int in = data.readFileDescriptor();
+ int out = data.readFileDescriptor();
+ int err = data.readFileDescriptor();
+ int argc = data.readInt32();
+ Vector<String16> args;
+ for (int i = 0; i < argc && data.dataAvail() > 0; i++) {
+ args.add(data.readString16());
+ }
+ sp<IBinder> unusedCallback;
+ sp<IResultReceiver> resultReceiver;
+ status_t status;
+ if ((status = data.readNullableStrongBinder(&unusedCallback)) != NO_ERROR) {
+ return status;
+ }
+ if ((status = data.readNullableStrongBinder(&resultReceiver)) != NO_ERROR) {
+ return status;
+ }
+ status = shellCommand(in, out, err, args);
+ if (resultReceiver != nullptr) {
+ resultReceiver->send(status);
+ }
+ return NO_ERROR;
+ }
+ }
+
return BnAudioPolicyService::onTransact(code, data, reply, flags);
}
+// ------------------- Shell command implementation -------------------
+
+// NOTE: This is a remote API - make sure all args are validated
+status_t AudioPolicyService::shellCommand(int in, int out, int err, Vector<String16>& args) {
+ if (!checkCallingPermission(sManageAudioPolicyPermission, nullptr, nullptr)) {
+ return PERMISSION_DENIED;
+ }
+ if (in == BAD_TYPE || out == BAD_TYPE || err == BAD_TYPE) {
+ return BAD_VALUE;
+ }
+ if (args.size() == 3 && args[0] == String16("set-uid-state")) {
+ return handleSetUidState(args, err);
+ } else if (args.size() == 2 && args[0] == String16("reset-uid-state")) {
+ return handleResetUidState(args, err);
+ } else if (args.size() == 2 && args[0] == String16("get-uid-state")) {
+ return handleGetUidState(args, out, err);
+ } else if (args.size() == 1 && args[0] == String16("help")) {
+ printHelp(out);
+ return NO_ERROR;
+ }
+ printHelp(err);
+ return BAD_VALUE;
+}
+
+status_t AudioPolicyService::handleSetUidState(Vector<String16>& args, int err) {
+ PermissionController pc;
+ int uid = pc.getPackageUid(args[1], 0);
+ if (uid <= 0) {
+ ALOGE("Unknown package: '%s'", String8(args[1]).string());
+ dprintf(err, "Unknown package: '%s'\n", String8(args[1]).string());
+ return BAD_VALUE;
+ }
+ bool active = false;
+ if (args[2] == String16("active")) {
+ active = true;
+ } else if ((args[2] != String16("idle"))) {
+ ALOGE("Expected active or idle but got: '%s'", String8(args[2]).string());
+ return BAD_VALUE;
+ }
+ mUidPolicy->addOverrideUid(uid, active);
+ return NO_ERROR;
+}
+
+status_t AudioPolicyService::handleResetUidState(Vector<String16>& args, int err) {
+ PermissionController pc;
+ int uid = pc.getPackageUid(args[1], 0);
+ if (uid < 0) {
+ ALOGE("Unknown package: '%s'", String8(args[1]).string());
+ dprintf(err, "Unknown package: '%s'\n", String8(args[1]).string());
+ return BAD_VALUE;
+ }
+ mUidPolicy->removeOverrideUid(uid);
+ return NO_ERROR;
+}
+
+status_t AudioPolicyService::handleGetUidState(Vector<String16>& args, int out, int err) {
+ PermissionController pc;
+ int uid = pc.getPackageUid(args[1], 0);
+ if (uid < 0) {
+ ALOGE("Unknown package: '%s'", String8(args[1]).string());
+ dprintf(err, "Unknown package: '%s'\n", String8(args[1]).string());
+ return BAD_VALUE;
+ }
+ if (mUidPolicy->isUidActive(uid)) {
+ return dprintf(out, "active\n");
+ } else {
+ return dprintf(out, "idle\n");
+ }
+}
+
+status_t AudioPolicyService::printHelp(int out) {
+ return dprintf(out, "Audio policy service commands:\n"
+ " get-uid-state <PACKAGE> gets the uid state\n"
+ " set-uid-state <PACKAGE> <active|idle> overrides the uid state\n"
+ " reset-uid-state <PACKAGE> clears the uid state override\n"
+ " help print this message\n");
+}
+
+// ----------- AudioPolicyService::UidPolicy implementation ----------
+
+void AudioPolicyService::UidPolicy::registerSelf() {
+ ActivityManager am;
+ am.registerUidObserver(this, ActivityManager::UID_OBSERVER_GONE
+ | ActivityManager::UID_OBSERVER_IDLE
+ | ActivityManager::UID_OBSERVER_ACTIVE,
+ ActivityManager::PROCESS_STATE_UNKNOWN,
+ String16("audioserver"));
+}
+
+void AudioPolicyService::UidPolicy::unregisterSelf() {
+ ActivityManager am;
+ am.unregisterUidObserver(this);
+}
+
+void AudioPolicyService::UidPolicy::onUidGone(uid_t uid, __unused bool disabled) {
+ onUidIdle(uid, disabled);
+}
+
+void AudioPolicyService::UidPolicy::onUidActive(uid_t uid) {
+ {
+ Mutex::Autolock _l(mUidLock);
+ mActiveUids.insert(uid);
+ }
+ sp<AudioPolicyService> service = mService.promote();
+ if (service != nullptr) {
+ service->setRecordSilenced(uid, false);
+ }
+}
+
+void AudioPolicyService::UidPolicy::onUidIdle(uid_t uid, __unused bool disabled) {
+ bool deleted = false;
+ {
+ Mutex::Autolock _l(mUidLock);
+ if (mActiveUids.erase(uid) > 0) {
+ deleted = true;
+ }
+ }
+ if (deleted) {
+ sp<AudioPolicyService> service = mService.promote();
+ if (service != nullptr) {
+ service->setRecordSilenced(uid, true);
+ }
+ }
+}
+
+void AudioPolicyService::UidPolicy::addOverrideUid(uid_t uid, bool active) {
+ updateOverrideUid(uid, active, true);
+}
+
+void AudioPolicyService::UidPolicy::removeOverrideUid(uid_t uid) {
+ updateOverrideUid(uid, false, false);
+}
+
+void AudioPolicyService::UidPolicy::updateOverrideUid(uid_t uid, bool active, bool insert) {
+ bool wasActive = false;
+ bool isActive = false;
+ {
+ Mutex::Autolock _l(mUidLock);
+ wasActive = isUidActiveLocked(uid);
+ mOverrideUids.erase(uid);
+ if (insert) {
+ mOverrideUids.insert(std::pair<uid_t, bool>(uid, active));
+ }
+ isActive = isUidActiveLocked(uid);
+ }
+ if (wasActive != isActive) {
+ sp<AudioPolicyService> service = mService.promote();
+ if (service != nullptr) {
+ service->setRecordSilenced(uid, !isActive);
+ }
+ }
+}
+
+bool AudioPolicyService::UidPolicy::isUidActive(uid_t uid) {
+ // Non-app UIDs are considered always active
+ if (uid < FIRST_APPLICATION_UID) {
+ return true;
+ }
+ Mutex::Autolock _l(mUidLock);
+ return isUidActiveLocked(uid);
+}
+
+bool AudioPolicyService::UidPolicy::isUidActiveLocked(uid_t uid) {
+ // Non-app UIDs are considered always active
+ if (uid < FIRST_APPLICATION_UID) {
+ return true;
+ }
+ auto it = mOverrideUids.find(uid);
+ if (it != mOverrideUids.end()) {
+ return it->second;
+ }
+ return mActiveUids.find(uid) != mActiveUids.end();
+}
// ----------- AudioPolicyService::AudioCommandThread implementation ----------
diff --git a/services/audiopolicy/service/AudioPolicyService.h b/services/audiopolicy/service/AudioPolicyService.h
index 833a230..9bc13c2 100644
--- a/services/audiopolicy/service/AudioPolicyService.h
+++ b/services/audiopolicy/service/AudioPolicyService.h
@@ -24,6 +24,7 @@
#include <utils/Vector.h>
#include <utils/SortedVector.h>
#include <binder/BinderService.h>
+#include <binder/IUidObserver.h>
#include <system/audio.h>
#include <system/audio_policy.h>
#include <media/IAudioPolicyService.h>
@@ -33,9 +34,13 @@
#include "AudioPolicyEffects.h"
#include "managerdefault/AudioPolicyManager.h"
+#include <unordered_map>
+#include <unordered_set>
namespace android {
+using namespace std;
+
// ----------------------------------------------------------------------------
class AudioPolicyService :
@@ -97,7 +102,10 @@
audio_port_handle_t *selectedDeviceId = NULL,
audio_port_handle_t *portId = NULL);
virtual status_t startInput(audio_io_handle_t input,
- audio_session_t session);
+ audio_session_t session,
+ audio_devices_t device,
+ uid_t uid,
+ bool *silenced);
virtual status_t stopInput(audio_io_handle_t input,
audio_session_t session);
virtual void releaseInput(audio_io_handle_t input,
@@ -235,6 +243,57 @@
status_t dumpInternals(int fd);
+ // Handles binder shell commands
+ virtual status_t shellCommand(int in, int out, int err, Vector<String16>& args);
+
+ // Sets whether the given UID records only silence
+ virtual void setRecordSilenced(uid_t uid, bool silenced);
+
+ // Overrides the UID state as if it is idle
+ status_t handleSetUidState(Vector<String16>& args, int err);
+
+ // Clears the override for the UID state
+ status_t handleResetUidState(Vector<String16>& args, int err);
+
+ // Gets the UID state
+ status_t handleGetUidState(Vector<String16>& args, int out, int err);
+
+ // Prints the shell command help
+ status_t printHelp(int out);
+
+ // If recording we need to make sure the UID is allowed to do that. If the UID is idle
+ // then it cannot record and gets buffers with zeros - silence. As soon as the UID
+ // transitions to an active state we will start reporting buffers with data. This approach
+ // transparently handles recording while the UID transitions between idle/active state
+ // avoiding to get stuck in a state receiving non-empty buffers while idle or in a state
+ // receiving empty buffers while active.
+ class UidPolicy : public BnUidObserver {
+ public:
+ explicit UidPolicy(wp<AudioPolicyService> service)
+ : mService(service) {}
+
+ void registerSelf();
+ void unregisterSelf();
+
+ bool isUidActive(uid_t uid);
+
+ void onUidGone(uid_t uid, bool disabled);
+ void onUidActive(uid_t uid);
+ void onUidIdle(uid_t uid, bool disabled);
+
+ void addOverrideUid(uid_t uid, bool active);
+ void removeOverrideUid(uid_t uid);
+
+ private:
+ bool isUidActiveLocked(uid_t uid);
+ void updateOverrideUid(uid_t uid, bool active, bool insert);
+
+ Mutex mUidLock;
+ wp<AudioPolicyService> mService;
+ std::unordered_set<uid_t> mActiveUids;
+ std::unordered_map<uid_t, bool> mOverrideUids;
+ };
+
// Thread used for tone playback and to send audio config commands to audio flinger
// For tone playback, using a separate thread is necessary to avoid deadlock with mLock because
// startTone() and stopTone() are normally called with mLock locked and requesting a tone start
@@ -306,7 +365,6 @@
const audio_config_base_t *deviceConfig,
audio_patch_handle_t patchHandle);
void insertCommand_l(AudioCommand *command, int delayMs = 0);
-
private:
class AudioCommandData;
@@ -575,6 +633,8 @@
// Manage all effects configured in audio_effects.conf
sp<AudioPolicyEffects> mAudioPolicyEffects;
audio_mode_t mPhoneState;
+
+ sp<UidPolicy> mUidPolicy;
};
} // namespace android
diff --git a/services/camera/libcameraservice/Android.mk b/services/camera/libcameraservice/Android.mk
index aeaca48..7b86180 100644
--- a/services/camera/libcameraservice/Android.mk
+++ b/services/camera/libcameraservice/Android.mk
@@ -60,6 +60,7 @@
LOCAL_SHARED_LIBRARIES:= \
libui \
liblog \
+ libutilscallstack \
libutils \
libbinder \
libcutils \
diff --git a/services/camera/libcameraservice/CameraService.cpp b/services/camera/libcameraservice/CameraService.cpp
index 6abfa81..5fc1fa3 100644
--- a/services/camera/libcameraservice/CameraService.cpp
+++ b/services/camera/libcameraservice/CameraService.cpp
@@ -33,14 +33,18 @@
#include <android-base/macros.h>
#include <android-base/parseint.h>
+#include <binder/ActivityManager.h>
#include <binder/AppOpsManager.h>
#include <binder/IPCThreadState.h>
#include <binder/IServiceManager.h>
#include <binder/MemoryBase.h>
#include <binder/MemoryHeapBase.h>
+#include <binder/PermissionController.h>
#include <binder/ProcessInfoService.h>
+#include <binder/IResultReceiver.h>
#include <cutils/atomic.h>
#include <cutils/properties.h>
+#include <cutils/misc.h>
#include <gui/Surface.h>
#include <hardware/hardware.h>
#include <memunreachable/memunreachable.h>
@@ -165,6 +169,8 @@
// ----------------------------------------------------------------------------
+static const String16 sManageCameraPermission("android.permission.MANAGE_CAMERA");
+
CameraService::CameraService() :
mEventLog(DEFAULT_EVENT_LOG_LENGTH),
mNumberOfCameras(0), mNumberOfNormalCameras(0),
@@ -196,6 +202,9 @@
}
CameraService::pingCameraServiceProxy();
+
+ mUidPolicy = new UidPolicy(this);
+ mUidPolicy->registerSelf();
}
status_t CameraService::enumerateProviders() {
@@ -275,6 +284,7 @@
CameraService::~CameraService() {
VendorTagDescriptor::clearGlobalVendorTagDescriptor();
+ mUidPolicy->unregisterSelf();
}
void CameraService::onNewProviderRegistered() {
@@ -933,6 +943,15 @@
clientName8.string(), clientUid, clientPid, cameraId.string());
}
+ // Make sure the UID is in an active state to use the camera
+ if (!mUidPolicy->isUidActive(callingUid)) {
+ ALOGE("Access Denial: can't use the camera from an idle UID pid=%d, uid=%d",
+ clientPid, clientUid);
+ return STATUS_ERROR_FMT(ERROR_DISABLED,
+ "Caller \"%s\" (PID %d, UID %d) cannot open camera \"%s\" from background",
+ clientName8.string(), clientUid, clientPid, cameraId.string());
+ }
+
// Only use passed in clientPid to check permission. Use calling PID as the client PID that's
// connected to camera service directly.
originalClientPid = clientPid;
@@ -1969,6 +1988,30 @@
// Permission checks
switch (code) {
+ case SHELL_COMMAND_TRANSACTION: {
+ int in = data.readFileDescriptor();
+ int out = data.readFileDescriptor();
+ int err = data.readFileDescriptor();
+ int argc = data.readInt32();
+ Vector<String16> args;
+ for (int i = 0; i < argc && data.dataAvail() > 0; i++) {
+ args.add(data.readString16());
+ }
+ sp<IBinder> unusedCallback;
+ sp<IResultReceiver> resultReceiver;
+ status_t status;
+ if ((status = data.readNullableStrongBinder(&unusedCallback)) != NO_ERROR) {
+ return status;
+ }
+ if ((status = data.readNullableStrongBinder(&resultReceiver)) != NO_ERROR) {
+ return status;
+ }
+ status = shellCommand(in, out, err, args);
+ if (resultReceiver != nullptr) {
+ resultReceiver->send(status);
+ }
+ return NO_ERROR;
+ }
case BnCameraService::NOTIFYSYSTEMEVENT: {
if (pid != selfPid) {
// Ensure we're being called by system_server, or similar process with
@@ -2286,15 +2329,21 @@
if (res != AppOpsManager::MODE_ALLOWED) {
ALOGI("Camera %s: Access for \"%s\" revoked", mCameraIdStr.string(),
myName.string());
- // Reset the client PID to allow server-initiated disconnect,
- // and to prevent further calls by client.
- mClientPid = getCallingPid();
- CaptureResultExtras resultExtras; // a dummy result (invalid)
- notifyError(hardware::camera2::ICameraDeviceCallbacks::ERROR_CAMERA_SERVICE, resultExtras);
- disconnect();
+ block();
}
}
+void CameraService::BasicClient::block() {
+ ATRACE_CALL();
+
+ // Reset the client PID to allow server-initiated disconnect,
+ // and to prevent further calls by client.
+ mClientPid = getCallingPid();
+ CaptureResultExtras resultExtras; // a dummy result (invalid)
+ notifyError(hardware::camera2::ICameraDeviceCallbacks::ERROR_CAMERA_DISABLED, resultExtras);
+ disconnect();
+}
+
// ----------------------------------------------------------------------------
void CameraService::Client::notifyError(int32_t errorCode,
@@ -2331,6 +2380,98 @@
}
// ----------------------------------------------------------------------------
+// UidPolicy
+// ----------------------------------------------------------------------------
+
+void CameraService::UidPolicy::registerSelf() {
+ ActivityManager am;
+ am.registerUidObserver(this, ActivityManager::UID_OBSERVER_GONE
+ | ActivityManager::UID_OBSERVER_IDLE
+ | ActivityManager::UID_OBSERVER_ACTIVE,
+ ActivityManager::PROCESS_STATE_UNKNOWN,
+ String16("cameraserver"));
+}
+
+void CameraService::UidPolicy::unregisterSelf() {
+ ActivityManager am;
+ am.unregisterUidObserver(this);
+}
+
+void CameraService::UidPolicy::onUidGone(uid_t uid, bool disabled) {
+ onUidIdle(uid, disabled);
+}
+
+void CameraService::UidPolicy::onUidActive(uid_t uid) {
+ Mutex::Autolock _l(mUidLock);
+ mActiveUids.insert(uid);
+}
+
+void CameraService::UidPolicy::onUidIdle(uid_t uid, bool /* disabled */) {
+ bool deleted = false;
+ {
+ Mutex::Autolock _l(mUidLock);
+ if (mActiveUids.erase(uid) > 0) {
+ deleted = true;
+ }
+ }
+ if (deleted) {
+ sp<CameraService> service = mService.promote();
+ if (service != nullptr) {
+ service->blockClientsForUid(uid);
+ }
+ }
+}
+
+bool CameraService::UidPolicy::isUidActive(uid_t uid) {
+ // Non-app UIDs are considered always active
+ if (uid < FIRST_APPLICATION_UID) {
+ return true;
+ }
+ Mutex::Autolock _l(mUidLock);
+ return isUidActiveLocked(uid);
+}
+
+bool CameraService::UidPolicy::isUidActiveLocked(uid_t uid) {
+ // Non-app UIDs are considered always active
+ if (uid < FIRST_APPLICATION_UID) {
+ return true;
+ }
+ auto it = mOverrideUids.find(uid);
+ if (it != mOverrideUids.end()) {
+ return it->second;
+ }
+ return mActiveUids.find(uid) != mActiveUids.end();
+}
+
+void CameraService::UidPolicy::UidPolicy::addOverrideUid(uid_t uid, bool active) {
+ updateOverrideUid(uid, active, true);
+}
+
+void CameraService::UidPolicy::removeOverrideUid(uid_t uid) {
+ updateOverrideUid(uid, false, false);
+}
+
+void CameraService::UidPolicy::updateOverrideUid(uid_t uid, bool active, bool insert) {
+ bool wasActive = false;
+ bool isActive = false;
+ {
+ Mutex::Autolock _l(mUidLock);
+ wasActive = isUidActiveLocked(uid);
+ mOverrideUids.erase(uid);
+ if (insert) {
+ mOverrideUids.insert(std::pair<uid_t, bool>(uid, active));
+ }
+ isActive = isUidActiveLocked(uid);
+ }
+ if (wasActive != isActive && !isActive) {
+ sp<CameraService> service = mService.promote();
+ if (service != nullptr) {
+ service->blockClientsForUid(uid);
+ }
+ }
+}
+
+// ----------------------------------------------------------------------------
// CameraState
// ----------------------------------------------------------------------------
@@ -2791,4 +2932,92 @@
return OK;
}
+void CameraService::blockClientsForUid(uid_t uid) {
+ const auto clients = mActiveClientManager.getAll();
+ for (auto& current : clients) {
+ if (current != nullptr) {
+ const auto basicClient = current->getValue();
+ if (basicClient.get() != nullptr && basicClient->getClientUid() == uid) {
+ basicClient->block();
+ }
+ }
+ }
+}
+
+// NOTE: This is a remote API - make sure all args are validated
+status_t CameraService::shellCommand(int in, int out, int err, const Vector<String16>& args) {
+ if (!checkCallingPermission(sManageCameraPermission, nullptr, nullptr)) {
+ return PERMISSION_DENIED;
+ }
+ if (in == BAD_TYPE || out == BAD_TYPE || err == BAD_TYPE) {
+ return BAD_VALUE;
+ }
+ if (args.size() == 3 && args[0] == String16("set-uid-state")) {
+ return handleSetUidState(args, err);
+ } else if (args.size() == 2 && args[0] == String16("reset-uid-state")) {
+ return handleResetUidState(args, err);
+ } else if (args.size() == 2 && args[0] == String16("get-uid-state")) {
+ return handleGetUidState(args, out, err);
+ } else if (args.size() == 1 && args[0] == String16("help")) {
+ printHelp(out);
+ return NO_ERROR;
+ }
+ printHelp(err);
+ return BAD_VALUE;
+}
+
+status_t CameraService::handleSetUidState(const Vector<String16>& args, int err) {
+ PermissionController pc;
+ int uid = pc.getPackageUid(args[1], 0);
+ if (uid <= 0) {
+ ALOGE("Unknown package: '%s'", String8(args[1]).string());
+ dprintf(err, "Unknown package: '%s'\n", String8(args[1]).string());
+ return BAD_VALUE;
+ }
+ bool active = false;
+ if (args[2] == String16("active")) {
+ active = true;
+ } else if ((args[2] != String16("idle"))) {
+ ALOGE("Expected active or idle but got: '%s'", String8(args[2]).string());
+ return BAD_VALUE;
+ }
+ mUidPolicy->addOverrideUid(uid, active);
+ return NO_ERROR;
+}
+
+status_t CameraService::handleResetUidState(const Vector<String16>& args, int err) {
+ PermissionController pc;
+ int uid = pc.getPackageUid(args[1], 0);
+ if (uid < 0) {
+ ALOGE("Unknown package: '%s'", String8(args[1]).string());
+ dprintf(err, "Unknown package: '%s'\n", String8(args[1]).string());
+ return BAD_VALUE;
+ }
+ mUidPolicy->removeOverrideUid(uid);
+ return NO_ERROR;
+}
+
+status_t CameraService::handleGetUidState(const Vector<String16>& args, int out, int err) {
+ PermissionController pc;
+ int uid = pc.getPackageUid(args[1], 0);
+ if (uid <= 0) {
+ ALOGE("Unknown package: '%s'", String8(args[1]).string());
+ dprintf(err, "Unknown package: '%s'\n", String8(args[1]).string());
+ return BAD_VALUE;
+ }
+ if (mUidPolicy->isUidActive(uid)) {
+ return dprintf(out, "active\n");
+ } else {
+ return dprintf(out, "idle\n");
+ }
+}
+
+status_t CameraService::printHelp(int out) {
+ return dprintf(out, "Camera service commands:\n"
+ " get-uid-state <PACKAGE> gets the uid state\n"
+ " set-uid-state <PACKAGE> <active|idle> overrides the uid state\n"
+ " reset-uid-state <PACKAGE> clears the uid state override\n"
+ " help print this message\n");
+}
+
}; // namespace android
diff --git a/services/camera/libcameraservice/CameraService.h b/services/camera/libcameraservice/CameraService.h
index e9373a6..575cebf 100644
--- a/services/camera/libcameraservice/CameraService.h
+++ b/services/camera/libcameraservice/CameraService.h
@@ -27,6 +27,7 @@
#include <binder/AppOpsManager.h>
#include <binder/BinderService.h>
#include <binder/IAppOpsCallback.h>
+#include <binder/IUidObserver.h>
#include <hardware/camera.h>
#include <android/hardware/camera/common/1.0/types.h>
@@ -47,6 +48,8 @@
#include <map>
#include <memory>
#include <utility>
+#include <unordered_map>
+#include <unordered_set>
namespace android {
@@ -163,6 +166,8 @@
virtual status_t dump(int fd, const Vector<String16>& args);
+ virtual status_t shellCommand(int in, int out, int err, const Vector<String16>& args);
+
/////////////////////////////////////////////////////////////////////
// Client functionality
@@ -233,6 +238,9 @@
// Check what API level is used for this client. This is used to determine which
// superclass this can be cast to.
virtual bool canCastToApiClient(apiLevel level) const;
+
+ // Block the client form using the camera
+ virtual void block();
protected:
BasicClient(const sp<CameraService>& cameraService,
const sp<IBinder>& remoteCallback,
@@ -506,6 +514,37 @@
CameraParameters mShimParams;
}; // class CameraState
+ // Observer for UID lifecycle enforcing that UIDs in idle
+ // state cannot use the camera to protect user privacy.
+ class UidPolicy : public BnUidObserver {
+ public:
+ explicit UidPolicy(sp<CameraService> service)
+ : mService(service) {}
+
+ void registerSelf();
+ void unregisterSelf();
+
+ bool isUidActive(uid_t uid);
+
+ void onUidGone(uid_t uid, bool disabled);
+ void onUidActive(uid_t uid);
+ void onUidIdle(uid_t uid, bool disabled);
+
+ void addOverrideUid(uid_t uid, bool active);
+ void removeOverrideUid(uid_t uid);
+
+ private:
+ bool isUidActiveLocked(uid_t uid);
+ void updateOverrideUid(uid_t uid, bool active, bool insert);
+
+ Mutex mUidLock;
+ wp<CameraService> mService;
+ std::unordered_set<uid_t> mActiveUids;
+ std::unordered_map<uid_t, bool> mOverrideUids;
+ }; // class UidPolicy
+
+ sp<UidPolicy> mUidPolicy;
+
// Delay-load the Camera HAL module
virtual void onFirstRef();
@@ -755,6 +794,21 @@
*/
binder::Status getLegacyParametersLazy(int cameraId, /*out*/CameraParameters* parameters);
+ // Blocks all clients from the UID
+ void blockClientsForUid(uid_t uid);
+
+ // Overrides the UID state as if it is idle
+ status_t handleSetUidState(const Vector<String16>& args, int err);
+
+ // Clears the override for the UID state
+ status_t handleResetUidState(const Vector<String16>& args, int err);
+
+ // Gets the UID state
+ status_t handleGetUidState(const Vector<String16>& args, int out, int err);
+
+ // Prints the shell command help
+ status_t printHelp(int out);
+
static int getCallingPid();
static int getCallingUid();
diff --git a/services/mediaextractor/MediaExtractorService.cpp b/services/mediaextractor/MediaExtractorService.cpp
index 0dc1fce..f0f44f5 100644
--- a/services/mediaextractor/MediaExtractorService.cpp
+++ b/services/mediaextractor/MediaExtractorService.cpp
@@ -56,7 +56,7 @@
}
status_t MediaExtractorService::dump(int fd, const Vector<String16>& args) {
- return dumpExtractors(fd, args);
+ return MediaExtractorFactory::dump(fd, args) || dumpExtractors(fd, args);
}
status_t MediaExtractorService::onTransact(uint32_t code, const Parcel& data, Parcel* reply,
diff --git a/services/oboeservice/AAudioEndpointManager.cpp b/services/oboeservice/AAudioEndpointManager.cpp
index b0c3771..7b8d817 100644
--- a/services/oboeservice/AAudioEndpointManager.cpp
+++ b/services/oboeservice/AAudioEndpointManager.cpp
@@ -102,8 +102,8 @@
}
}
- ALOGV("findExclusiveEndpoint_l(), found %p for device = %d",
- endpoint.get(), configuration.getDeviceId());
+ ALOGV("findExclusiveEndpoint_l(), found %p for device = %d, sessionId = %d",
+ endpoint.get(), configuration.getDeviceId(), configuration.getSessionId());
return endpoint;
}
@@ -118,8 +118,8 @@
}
}
- ALOGV("findSharedEndpoint_l(), found %p for device = %d",
- endpoint.get(), configuration.getDeviceId());
+ ALOGV("findSharedEndpoint_l(), found %p for device = %d, sessionId = %d",
+ endpoint.get(), configuration.getDeviceId(), configuration.getSessionId());
return endpoint;
}
@@ -151,19 +151,17 @@
return nullptr;
} else {
sp<AAudioServiceEndpointMMAP> endpointMMap = new AAudioServiceEndpointMMAP();
- ALOGD("openEndpoint(),created MMAP %p", endpointMMap.get());
+ ALOGD("openExclusiveEndpoint(), no match so try to open MMAP %p for dev %d",
+ endpointMMap.get(), configuration.getDeviceId());
endpoint = endpointMMap;
aaudio_result_t result = endpoint->open(request);
if (result != AAUDIO_OK) {
- ALOGE("openEndpoint(), open failed");
+ ALOGE("openExclusiveEndpoint(), open failed");
endpoint.clear();
} else {
mExclusiveStreams.push_back(endpointMMap);
}
-
- ALOGD("openEndpoint(), created %p for device = %d",
- endpoint.get(), configuration.getDeviceId());
}
if (endpoint.get() != nullptr) {
@@ -209,7 +207,7 @@
mSharedStreams.push_back(endpoint);
}
}
- ALOGD("openSharedEndpoint(), created %p for device = %d, dir = %d",
+ ALOGD("openSharedEndpoint(), created %p, requested device = %d, dir = %d",
endpoint.get(), configuration.getDeviceId(), (int)direction);
IPCThreadState::self()->restoreCallingIdentity(token);
}
diff --git a/services/oboeservice/AAudioEndpointManager.h b/services/oboeservice/AAudioEndpointManager.h
index 32c8454..f6aeb5a 100644
--- a/services/oboeservice/AAudioEndpointManager.h
+++ b/services/oboeservice/AAudioEndpointManager.h
@@ -47,7 +47,7 @@
std::string dump() const;
/**
- * Find a service endpoint for the given deviceId and direction.
+ * Find a service endpoint for the given deviceId, sessionId and direction.
* If an endpoint does not already exist then try to create one.
*
* @param audioService
diff --git a/services/oboeservice/AAudioServiceEndpoint.cpp b/services/oboeservice/AAudioServiceEndpoint.cpp
index f917675..33439fc 100644
--- a/services/oboeservice/AAudioServiceEndpoint.cpp
+++ b/services/oboeservice/AAudioServiceEndpoint.cpp
@@ -55,11 +55,16 @@
result << " Direction: " << ((getDirection() == AAUDIO_DIRECTION_OUTPUT)
? "OUTPUT" : "INPUT") << "\n";
- result << " Sample Rate: " << getSampleRate() << "\n";
- result << " Frames Per Burst: " << mFramesPerBurst << "\n";
- result << " Reference Count: " << mOpenCount << "\n";
result << " Requested Device Id: " << mRequestedDeviceId << "\n";
result << " Device Id: " << getDeviceId() << "\n";
+ result << " Sample Rate: " << getSampleRate() << "\n";
+ result << " Channel Count: " << getSamplesPerFrame() << "\n";
+ result << " Frames Per Burst: " << mFramesPerBurst << "\n";
+ result << " Usage: " << getUsage() << "\n";
+ result << " ContentType: " << getContentType() << "\n";
+ result << " InputPreset: " << getInputPreset() << "\n";
+ result << " Reference Count: " << mOpenCount << "\n";
+ result << " Session Id: " << getSessionId() << "\n";
result << " Connected: " << mConnected.load() << "\n";
result << " Registered Streams:" << "\n";
result << AAudioServiceStreamShared::dumpHeader() << "\n";
@@ -109,6 +114,10 @@
configuration.getDeviceId() != getDeviceId()) {
return false;
}
+ if (configuration.getSessionId() != AAUDIO_SESSION_ID_ALLOCATE &&
+ configuration.getSessionId() != getSessionId()) {
+ return false;
+ }
if (configuration.getSampleRate() != AAUDIO_UNSPECIFIED &&
configuration.getSampleRate() != getSampleRate()) {
return false;
diff --git a/services/oboeservice/AAudioServiceEndpointMMAP.cpp b/services/oboeservice/AAudioServiceEndpointMMAP.cpp
index 8db1761..db01c88 100644
--- a/services/oboeservice/AAudioServiceEndpointMMAP.cpp
+++ b/services/oboeservice/AAudioServiceEndpointMMAP.cpp
@@ -98,6 +98,9 @@
.flags = AUDIO_FLAG_LOW_LATENCY,
.tags = ""
};
+ ALOGV("open() MMAP attributes.usage = %d, content_type = %d, source = %d",
+ attributes.usage, attributes.content_type, attributes.source);
+
mMmapClient.clientUid = request.getUserId();
mMmapClient.clientPid = request.getProcessId();
mMmapClient.packageName.setTo(String16(""));
@@ -141,12 +144,16 @@
? MmapStreamInterface::DIRECTION_OUTPUT
: MmapStreamInterface::DIRECTION_INPUT;
+ aaudio_session_id_t requestedSessionId = getSessionId();
+ audio_session_t sessionId = AAudioConvert_aaudioToAndroidSessionId(requestedSessionId);
+
// Open HAL stream. Set mMmapStream
status_t status = MmapStreamInterface::openMmapStream(streamDirection,
&attributes,
&config,
mMmapClient,
&deviceId,
+ &sessionId,
this, // callback
mMmapStream,
&mPortHandle);
@@ -162,6 +169,17 @@
}
setDeviceId(deviceId);
+ if (sessionId == AUDIO_SESSION_ALLOCATE) {
+ ALOGW("open() - openMmapStream() failed to set sessionId");
+ }
+
+ aaudio_session_id_t actualSessionId =
+ (requestedSessionId == AAUDIO_SESSION_ID_NONE)
+ ? AAUDIO_SESSION_ID_NONE
+ : (aaudio_session_id_t) sessionId;
+ setSessionId(actualSessionId);
+ ALOGD("open() deviceId = %d, sessionId = %d", getDeviceId(), getSessionId());
+
// Create MMAP/NOIRQ buffer.
int32_t minSizeFrames = getBufferCapacity();
if (minSizeFrames <= 0) { // zero will get rejected
diff --git a/services/oboeservice/AAudioServiceEndpointShared.cpp b/services/oboeservice/AAudioServiceEndpointShared.cpp
index 6af9e7e..584efe5 100644
--- a/services/oboeservice/AAudioServiceEndpointShared.cpp
+++ b/services/oboeservice/AAudioServiceEndpointShared.cpp
@@ -60,18 +60,16 @@
aaudio_result_t result = AAUDIO_OK;
const AAudioStreamConfiguration &configuration = request.getConstantConfiguration();
+ copyFrom(configuration);
mRequestedDeviceId = configuration.getDeviceId();
- setDirection(configuration.getDirection());
AudioStreamBuilder builder;
+ builder.copyFrom(configuration);
+
builder.setSharingMode(AAUDIO_SHARING_MODE_EXCLUSIVE);
// Don't fall back to SHARED because that would cause recursion.
builder.setSharingModeMatchRequired(true);
- builder.setDeviceId(mRequestedDeviceId);
- builder.setFormat(configuration.getFormat());
- builder.setSampleRate(configuration.getSampleRate());
- builder.setSamplesPerFrame(configuration.getSamplesPerFrame());
- builder.setDirection(configuration.getDirection());
+
builder.setBufferCapacity(DEFAULT_BUFFER_CAPACITY);
result = mStreamInternal->open(builder);
@@ -79,6 +77,8 @@
setSampleRate(mStreamInternal->getSampleRate());
setSamplesPerFrame(mStreamInternal->getSamplesPerFrame());
setDeviceId(mStreamInternal->getDeviceId());
+ setSessionId(mStreamInternal->getSessionId());
+ ALOGD("open() deviceId = %d, sessionId = %d", getDeviceId(), getSessionId());
mFramesPerBurst = mStreamInternal->getFramesPerBurst();
return result;
diff --git a/services/soundtrigger/Android.mk b/services/soundtrigger/Android.mk
index 10ee141..ca44737 100644
--- a/services/soundtrigger/Android.mk
+++ b/services/soundtrigger/Android.mk
@@ -49,11 +49,15 @@
LOCAL_SHARED_LIBRARIES += \
libhwbinder \
libhidlbase \
+ libhidlmemory \
libhidltransport \
libbase \
libaudiohal \
android.hardware.soundtrigger@2.0 \
- android.hardware.audio.common@2.0
+ android.hardware.soundtrigger@2.1 \
+ android.hardware.audio.common@2.0 \
+ android.hidl.allocator@1.0 \
+ android.hidl.memory@1.0
endif
diff --git a/services/soundtrigger/SoundTriggerHalHidl.cpp b/services/soundtrigger/SoundTriggerHalHidl.cpp
index 0cd5cf7..adf252e 100644
--- a/services/soundtrigger/SoundTriggerHalHidl.cpp
+++ b/services/soundtrigger/SoundTriggerHalHidl.cpp
@@ -17,17 +17,87 @@
#define LOG_TAG "SoundTriggerHalHidl"
//#define LOG_NDEBUG 0
+#include <android/hidl/allocator/1.0/IAllocator.h>
#include <media/audiohal/hidl/HalDeathHandler.h>
#include <utils/Log.h>
#include "SoundTriggerHalHidl.h"
+#include <hidlmemory/mapping.h>
#include <hwbinder/IPCThreadState.h>
#include <hwbinder/ProcessState.h>
namespace android {
-using android::hardware::Return;
-using android::hardware::ProcessState;
-using android::hardware::audio::common::V2_0::AudioDevice;
+using ::android::hardware::ProcessState;
+using ::android::hardware::Return;
+using ::android::hardware::Status;
+using ::android::hardware::Void;
+using ::android::hardware::audio::common::V2_0::AudioDevice;
+using ::android::hardware::hidl_memory;
+using ::android::hidl::allocator::V1_0::IAllocator;
+using ::android::hidl::memory::V1_0::IMemory;
+
+namespace {
+
+// Backs up by the vector with the contents of shared memory.
+// It is assumed that the passed hidl_vector is empty, so it's
+// not cleared if the memory is a null object.
+// The caller needs to keep the returned sp<IMemory> as long as
+// the data is needed.
+std::pair<bool, sp<IMemory>> memoryAsVector(const hidl_memory& m, hidl_vec<uint8_t>* vec) {
+ sp<IMemory> memory;
+ if (m.size() == 0) {
+ return std::make_pair(true, memory);
+ }
+ memory = mapMemory(m);
+ if (memory != nullptr) {
+ memory->read();
+ vec->setToExternal(static_cast<uint8_t*>(static_cast<void*>(memory->getPointer())),
+ memory->getSize());
+ return std::make_pair(true, memory);
+ }
+ ALOGE("%s: Could not map HIDL memory to IMemory", __func__);
+ return std::make_pair(false, memory);
+}
+
+// Moves the data from the vector into allocated shared memory,
+// emptying the vector.
+// It is assumed that the passed hidl_memory is a null object, so it's
+// not reset if the vector is empty.
+// The caller needs to keep the returned sp<IMemory> as long as
+// the data is needed.
+std::pair<bool, sp<IMemory>> moveVectorToMemory(hidl_vec<uint8_t>* v, hidl_memory* mem) {
+ sp<IMemory> memory;
+ if (v->size() == 0) {
+ return std::make_pair(true, memory);
+ }
+ sp<IAllocator> ashmem = IAllocator::getService("ashmem");
+ if (ashmem == 0) {
+ ALOGE("Failed to retrieve ashmem allocator service");
+ return std::make_pair(false, memory);
+ }
+ bool success = false;
+ Return<void> r = ashmem->allocate(v->size(), [&](bool s, const hidl_memory& m) {
+ success = s;
+ if (success) *mem = m;
+ });
+ if (r.isOk() && success) {
+ memory = hardware::mapMemory(*mem);
+ if (memory != 0) {
+ memory->update();
+ memcpy(memory->getPointer(), v->data(), v->size());
+ memory->commit();
+ v->resize(0);
+ return std::make_pair(true, memory);
+ } else {
+ ALOGE("Failed to map allocated ashmem");
+ }
+ } else {
+ ALOGE("Failed to allocate %llu bytes from ashmem", (unsigned long long)v->size());
+ }
+ return std::make_pair(false, memory);
+}
+
+} // namespace
/* static */
sp<SoundTriggerHalInterface> SoundTriggerHalInterface::connectModule(const char *moduleName)
@@ -94,36 +164,62 @@
"loadSoundModel(): wrap around in sound model IDs, num loaded models %zd",
mSoundModels.size());
- ISoundTriggerHw::SoundModel *halSoundModel =
- convertSoundModelToHal(sound_model);
- if (halSoundModel == NULL) {
- return -EINVAL;
- }
-
Return<void> hidlReturn;
int ret;
SoundModelHandle halHandle;
- {
- AutoMutex lock(mHalLock);
- if (sound_model->type == SOUND_MODEL_TYPE_KEYPHRASE) {
+ sp<V2_1_ISoundTriggerHw> soundtrigger_2_1 = toService2_1(soundtrigger);
+ if (sound_model->type == SOUND_MODEL_TYPE_KEYPHRASE) {
+ if (!soundtrigger_2_1) {
+ ISoundTriggerHw::PhraseSoundModel halSoundModel;
+ convertPhraseSoundModelToHal(&halSoundModel, sound_model);
+ AutoMutex lock(mHalLock);
hidlReturn = soundtrigger->loadPhraseSoundModel(
- *(const ISoundTriggerHw::PhraseSoundModel *)halSoundModel,
+ halSoundModel,
this, modelId, [&](int32_t retval, auto res) {
- ret = retval;
- halHandle = res;
- });
-
+ ret = retval;
+ halHandle = res;
+ });
} else {
- hidlReturn = soundtrigger->loadSoundModel(*halSoundModel,
+ V2_1_ISoundTriggerHw::PhraseSoundModel halSoundModel;
+ auto result = convertPhraseSoundModelToHal(&halSoundModel, sound_model);
+ if (result.first) {
+ AutoMutex lock(mHalLock);
+ hidlReturn = soundtrigger_2_1->loadPhraseSoundModel_2_1(
+ halSoundModel,
+ this, modelId, [&](int32_t retval, auto res) {
+ ret = retval;
+ halHandle = res;
+ });
+ } else {
+ return NO_MEMORY;
+ }
+ }
+ } else {
+ if (!soundtrigger_2_1) {
+ ISoundTriggerHw::SoundModel halSoundModel;
+ convertSoundModelToHal(&halSoundModel, sound_model);
+ AutoMutex lock(mHalLock);
+ hidlReturn = soundtrigger->loadSoundModel(halSoundModel,
this, modelId, [&](int32_t retval, auto res) {
- ret = retval;
- halHandle = res;
- });
+ ret = retval;
+ halHandle = res;
+ });
+ } else {
+ V2_1_ISoundTriggerHw::SoundModel halSoundModel;
+ auto result = convertSoundModelToHal(&halSoundModel, sound_model);
+ if (result.first) {
+ AutoMutex lock(mHalLock);
+ hidlReturn = soundtrigger_2_1->loadSoundModel_2_1(halSoundModel,
+ this, modelId, [&](int32_t retval, auto res) {
+ ret = retval;
+ halHandle = res;
+ });
+ } else {
+ return NO_MEMORY;
+ }
}
}
- delete halSoundModel;
-
if (hidlReturn.isOk()) {
if (ret == 0) {
AutoMutex lock(mLock);
@@ -185,16 +281,27 @@
model->mRecognitionCallback = callback;
model->mRecognitionCookie = cookie;
- ISoundTriggerHw::RecognitionConfig *halConfig =
- convertRecognitionConfigToHal(config);
-
+ sp<V2_1_ISoundTriggerHw> soundtrigger_2_1 = toService2_1(soundtrigger);
Return<int32_t> hidlReturn(0);
- {
- AutoMutex lock(mHalLock);
- hidlReturn = soundtrigger->startRecognition(model->mHalHandle, *halConfig, this, handle);
- }
- delete halConfig;
+ if (!soundtrigger_2_1) {
+ ISoundTriggerHw::RecognitionConfig halConfig;
+ convertRecognitionConfigToHal(&halConfig, config);
+ {
+ AutoMutex lock(mHalLock);
+ hidlReturn = soundtrigger->startRecognition(model->mHalHandle, halConfig, this, handle);
+ }
+ } else {
+ V2_1_ISoundTriggerHw::RecognitionConfig halConfig;
+ auto result = convertRecognitionConfigToHal(&halConfig, config);
+ if (result.first) {
+ AutoMutex lock(mHalLock);
+ hidlReturn = soundtrigger_2_1->startRecognition_2_1(
+ model->mHalHandle, halConfig, this, handle);
+ } else {
+ return NO_MEMORY;
+ }
+ }
if (!hidlReturn.isOk()) {
ALOGE("startRecognition error %s", hidlReturn.description().c_str());
@@ -275,6 +382,12 @@
return mISoundTrigger;
}
+sp<V2_1_ISoundTriggerHw> SoundTriggerHalHidl::toService2_1(const sp<ISoundTriggerHw>& s)
+{
+ auto castResult_2_1 = V2_1_ISoundTriggerHw::castFrom(s);
+ return castResult_2_1.isOk() ? static_cast<sp<V2_1_ISoundTriggerHw>>(castResult_2_1) : nullptr;
+}
+
sp<SoundTriggerHalHidl::SoundModel> SoundTriggerHalHidl::getModel(sound_model_handle_t handle)
{
AutoMutex lock(mLock);
@@ -347,40 +460,52 @@
halTriggerPhrase->text = triggerPhrase->text;
}
-ISoundTriggerHw::SoundModel *SoundTriggerHalHidl::convertSoundModelToHal(
+
+void SoundTriggerHalHidl::convertTriggerPhrasesToHal(
+ hidl_vec<ISoundTriggerHw::Phrase> *halTriggerPhrases,
+ struct sound_trigger_phrase_sound_model *keyPhraseModel)
+{
+ halTriggerPhrases->resize(keyPhraseModel->num_phrases);
+ for (unsigned int i = 0; i < keyPhraseModel->num_phrases; i++) {
+ convertTriggerPhraseToHal(&(*halTriggerPhrases)[i], &keyPhraseModel->phrases[i]);
+ }
+}
+
+void SoundTriggerHalHidl::convertSoundModelToHal(ISoundTriggerHw::SoundModel *halModel,
const struct sound_trigger_sound_model *soundModel)
{
- ISoundTriggerHw::SoundModel *halModel = NULL;
- if (soundModel->type == SOUND_MODEL_TYPE_KEYPHRASE) {
- ISoundTriggerHw::PhraseSoundModel *halKeyPhraseModel =
- new ISoundTriggerHw::PhraseSoundModel();
- struct sound_trigger_phrase_sound_model *keyPhraseModel =
- (struct sound_trigger_phrase_sound_model *)soundModel;
- ISoundTriggerHw::Phrase *halPhrases =
- new ISoundTriggerHw::Phrase[keyPhraseModel->num_phrases];
-
-
- for (unsigned int i = 0; i < keyPhraseModel->num_phrases; i++) {
- convertTriggerPhraseToHal(&halPhrases[i],
- &keyPhraseModel->phrases[i]);
- }
- halKeyPhraseModel->phrases.setToExternal(halPhrases, keyPhraseModel->num_phrases);
- // FIXME: transfer buffer ownership. should have a method for that in hidl_vec
- halKeyPhraseModel->phrases.resize(keyPhraseModel->num_phrases);
-
- delete[] halPhrases;
-
- halModel = (ISoundTriggerHw::SoundModel *)halKeyPhraseModel;
- } else {
- halModel = new ISoundTriggerHw::SoundModel();
- }
halModel->type = (SoundModelType)soundModel->type;
convertUuidToHal(&halModel->uuid, &soundModel->uuid);
convertUuidToHal(&halModel->vendorUuid, &soundModel->vendor_uuid);
halModel->data.setToExternal((uint8_t *)soundModel + soundModel->data_offset, soundModel->data_size);
- halModel->data.resize(soundModel->data_size);
+}
- return halModel;
+std::pair<bool, sp<IMemory>> SoundTriggerHalHidl::convertSoundModelToHal(
+ V2_1_ISoundTriggerHw::SoundModel *halModel,
+ const struct sound_trigger_sound_model *soundModel)
+{
+ convertSoundModelToHal(&halModel->header, soundModel);
+ return moveVectorToMemory(&halModel->header.data, &halModel->data);
+}
+
+void SoundTriggerHalHidl::convertPhraseSoundModelToHal(
+ ISoundTriggerHw::PhraseSoundModel *halKeyPhraseModel,
+ const struct sound_trigger_sound_model *soundModel)
+{
+ struct sound_trigger_phrase_sound_model *keyPhraseModel =
+ (struct sound_trigger_phrase_sound_model *)soundModel;
+ convertTriggerPhrasesToHal(&halKeyPhraseModel->phrases, keyPhraseModel);
+ convertSoundModelToHal(&halKeyPhraseModel->common, soundModel);
+}
+
+std::pair<bool, sp<IMemory>> SoundTriggerHalHidl::convertPhraseSoundModelToHal(
+ V2_1_ISoundTriggerHw::PhraseSoundModel *halKeyPhraseModel,
+ const struct sound_trigger_sound_model *soundModel)
+{
+ struct sound_trigger_phrase_sound_model *keyPhraseModel =
+ (struct sound_trigger_phrase_sound_model *)soundModel;
+ convertTriggerPhrasesToHal(&halKeyPhraseModel->phrases, keyPhraseModel);
+ return convertSoundModelToHal(&halKeyPhraseModel->common, soundModel);
}
void SoundTriggerHalHidl::convertPhraseRecognitionExtraToHal(
@@ -390,52 +515,42 @@
halExtra->id = extra->id;
halExtra->recognitionModes = extra->recognition_modes;
halExtra->confidenceLevel = extra->confidence_level;
- ConfidenceLevel *halLevels =
- new ConfidenceLevel[extra->num_levels];
- for (unsigned int i = 0; i < extra->num_levels; i++) {
- halLevels[i].userId = extra->levels[i].user_id;
- halLevels[i].levelPercent = extra->levels[i].level;
- }
- halExtra->levels.setToExternal(halLevels, extra->num_levels);
- // FIXME: transfer buffer ownership. should have a method for that in hidl_vec
halExtra->levels.resize(extra->num_levels);
-
- delete[] halLevels;
+ for (unsigned int i = 0; i < extra->num_levels; i++) {
+ halExtra->levels[i].userId = extra->levels[i].user_id;
+ halExtra->levels[i].levelPercent = extra->levels[i].level;
+ }
}
-
-ISoundTriggerHw::RecognitionConfig *SoundTriggerHalHidl::convertRecognitionConfigToHal(
+void SoundTriggerHalHidl::convertRecognitionConfigToHal(
+ ISoundTriggerHw::RecognitionConfig *halConfig,
const struct sound_trigger_recognition_config *config)
{
- ISoundTriggerHw::RecognitionConfig *halConfig =
- new ISoundTriggerHw::RecognitionConfig();
-
halConfig->captureHandle = config->capture_handle;
halConfig->captureDevice = (AudioDevice)config->capture_device;
halConfig->captureRequested = (uint32_t)config->capture_requested;
- PhraseRecognitionExtra *halExtras =
- new PhraseRecognitionExtra[config->num_phrases];
-
+ halConfig->phrases.resize(config->num_phrases);
for (unsigned int i = 0; i < config->num_phrases; i++) {
- convertPhraseRecognitionExtraToHal(&halExtras[i],
+ convertPhraseRecognitionExtraToHal(&halConfig->phrases[i],
&config->phrases[i]);
}
- halConfig->phrases.setToExternal(halExtras, config->num_phrases);
- // FIXME: transfer buffer ownership. should have a method for that in hidl_vec
- halConfig->phrases.resize(config->num_phrases);
-
- delete[] halExtras;
halConfig->data.setToExternal((uint8_t *)config + config->data_offset, config->data_size);
+}
- return halConfig;
+std::pair<bool, sp<IMemory>> SoundTriggerHalHidl::convertRecognitionConfigToHal(
+ V2_1_ISoundTriggerHw::RecognitionConfig *halConfig,
+ const struct sound_trigger_recognition_config *config)
+{
+ convertRecognitionConfigToHal(&halConfig->header, config);
+ return moveVectorToMemory(&halConfig->header.data, &halConfig->data);
}
// ISoundTriggerHwCallback
::android::hardware::Return<void> SoundTriggerHalHidl::recognitionCallback(
- const ISoundTriggerHwCallback::RecognitionEvent& halEvent,
+ const V2_0_ISoundTriggerHwCallback::RecognitionEvent& halEvent,
CallbackCookie cookie)
{
sp<SoundModel> model;
@@ -459,7 +574,7 @@
}
::android::hardware::Return<void> SoundTriggerHalHidl::phraseRecognitionCallback(
- const ISoundTriggerHwCallback::PhraseRecognitionEvent& halEvent,
+ const V2_0_ISoundTriggerHwCallback::PhraseRecognitionEvent& halEvent,
CallbackCookie cookie)
{
sp<SoundModel> model;
@@ -471,14 +586,13 @@
}
}
- struct sound_trigger_recognition_event *event = convertRecognitionEventFromHal(
- (const ISoundTriggerHwCallback::RecognitionEvent *)&halEvent);
+ struct sound_trigger_phrase_recognition_event *event =
+ convertPhraseRecognitionEventFromHal(&halEvent);
if (event == NULL) {
return Return<void>();
}
-
- event->model = model->mHandle;
- model->mRecognitionCallback(event, model->mRecognitionCookie);
+ event->common.model = model->mHandle;
+ model->mRecognitionCallback(&event->common, model->mRecognitionCookie);
free(event);
@@ -486,7 +600,7 @@
}
::android::hardware::Return<void> SoundTriggerHalHidl::soundModelCallback(
- const ISoundTriggerHwCallback::ModelEvent& halEvent,
+ const V2_0_ISoundTriggerHwCallback::ModelEvent& halEvent,
CallbackCookie cookie)
{
sp<SoundModel> model;
@@ -511,9 +625,37 @@
return Return<void>();
}
+::android::hardware::Return<void> SoundTriggerHalHidl::recognitionCallback_2_1(
+ const ISoundTriggerHwCallback::RecognitionEvent& event, CallbackCookie cookie) {
+ // The data vector in the 'header' part of V2.1 structure is empty, thus copying is cheap.
+ V2_0_ISoundTriggerHwCallback::RecognitionEvent event_2_0 = event.header;
+ auto result = memoryAsVector(event.data, &event_2_0.data);
+ return result.first ? recognitionCallback(event_2_0, cookie) : Void();
+}
+
+::android::hardware::Return<void> SoundTriggerHalHidl::phraseRecognitionCallback_2_1(
+ const ISoundTriggerHwCallback::PhraseRecognitionEvent& event, int32_t cookie) {
+ V2_0_ISoundTriggerHwCallback::PhraseRecognitionEvent event_2_0;
+ // The data vector in the 'header' part of V2.1 structure is empty, thus copying is cheap.
+ event_2_0.common = event.common.header;
+ event_2_0.phraseExtras.setToExternal(
+ const_cast<PhraseRecognitionExtra*>(event.phraseExtras.data()),
+ event.phraseExtras.size());
+ auto result = memoryAsVector(event.common.data, &event_2_0.common.data);
+ return result.first ? phraseRecognitionCallback(event_2_0, cookie) : Void();
+}
+
+::android::hardware::Return<void> SoundTriggerHalHidl::soundModelCallback_2_1(
+ const ISoundTriggerHwCallback::ModelEvent& event, CallbackCookie cookie) {
+ // The data vector in the 'header' part of V2.1 structure is empty, thus copying is cheap.
+ V2_0_ISoundTriggerHwCallback::ModelEvent event_2_0 = event.header;
+ auto result = memoryAsVector(event.data, &event_2_0.data);
+ return result.first ? soundModelCallback(event_2_0, cookie) : Void();
+}
+
struct sound_trigger_model_event *SoundTriggerHalHidl::convertSoundModelEventFromHal(
- const ISoundTriggerHwCallback::ModelEvent *halEvent)
+ const V2_0_ISoundTriggerHwCallback::ModelEvent *halEvent)
{
struct sound_trigger_model_event *event = (struct sound_trigger_model_event *)malloc(
sizeof(struct sound_trigger_model_event) +
@@ -550,37 +692,55 @@
}
-struct sound_trigger_recognition_event *SoundTriggerHalHidl::convertRecognitionEventFromHal(
- const ISoundTriggerHwCallback::RecognitionEvent *halEvent)
+struct sound_trigger_phrase_recognition_event* SoundTriggerHalHidl::convertPhraseRecognitionEventFromHal(
+ const V2_0_ISoundTriggerHwCallback::PhraseRecognitionEvent *halPhraseEvent)
{
- struct sound_trigger_recognition_event *event;
-
- if (halEvent->type == SoundModelType::KEYPHRASE) {
- struct sound_trigger_phrase_recognition_event *phraseEvent =
- (struct sound_trigger_phrase_recognition_event *)malloc(
- sizeof(struct sound_trigger_phrase_recognition_event) +
- halEvent->data.size());
- if (phraseEvent == NULL) {
- return NULL;
- }
- const ISoundTriggerHwCallback::PhraseRecognitionEvent *halPhraseEvent =
- (const ISoundTriggerHwCallback::PhraseRecognitionEvent *)halEvent;
-
- for (unsigned int i = 0; i < halPhraseEvent->phraseExtras.size(); i++) {
- convertPhraseRecognitionExtraFromHal(&phraseEvent->phrase_extras[i],
- &halPhraseEvent->phraseExtras[i]);
- }
- phraseEvent->num_phrases = halPhraseEvent->phraseExtras.size();
- event = (struct sound_trigger_recognition_event *)phraseEvent;
- event->data_offset = sizeof(sound_trigger_phrase_recognition_event);
- } else {
- event = (struct sound_trigger_recognition_event *)malloc(
- sizeof(struct sound_trigger_recognition_event) + halEvent->data.size());
- if (event == NULL) {
- return NULL;
- }
- event->data_offset = sizeof(sound_trigger_recognition_event);
+ if (halPhraseEvent->common.type != SoundModelType::KEYPHRASE) {
+ ALOGE("Received non-keyphrase event type as PhraseRecognitionEvent");
+ return NULL;
}
+ struct sound_trigger_phrase_recognition_event *phraseEvent =
+ (struct sound_trigger_phrase_recognition_event *)malloc(
+ sizeof(struct sound_trigger_phrase_recognition_event) +
+ halPhraseEvent->common.data.size());
+ if (phraseEvent == NULL) {
+ return NULL;
+ }
+ phraseEvent->common.data_offset = sizeof(sound_trigger_phrase_recognition_event);
+
+ for (unsigned int i = 0; i < halPhraseEvent->phraseExtras.size(); i++) {
+ convertPhraseRecognitionExtraFromHal(&phraseEvent->phrase_extras[i],
+ &halPhraseEvent->phraseExtras[i]);
+ }
+ phraseEvent->num_phrases = halPhraseEvent->phraseExtras.size();
+
+ fillRecognitionEventFromHal(&phraseEvent->common, &halPhraseEvent->common);
+ return phraseEvent;
+}
+
+struct sound_trigger_recognition_event *SoundTriggerHalHidl::convertRecognitionEventFromHal(
+ const V2_0_ISoundTriggerHwCallback::RecognitionEvent *halEvent)
+{
+ if (halEvent->type == SoundModelType::KEYPHRASE) {
+ ALOGE("Received keyphrase event type as RecognitionEvent");
+ return NULL;
+ }
+ struct sound_trigger_recognition_event *event;
+ event = (struct sound_trigger_recognition_event *)malloc(
+ sizeof(struct sound_trigger_recognition_event) + halEvent->data.size());
+ if (event == NULL) {
+ return NULL;
+ }
+ event->data_offset = sizeof(sound_trigger_recognition_event);
+
+ fillRecognitionEventFromHal(event, halEvent);
+ return event;
+}
+
+void SoundTriggerHalHidl::fillRecognitionEventFromHal(
+ struct sound_trigger_recognition_event *event,
+ const V2_0_ISoundTriggerHwCallback::RecognitionEvent *halEvent)
+{
event->status = (int)halEvent->status;
event->type = (sound_trigger_sound_model_type_t)halEvent->type;
// event->model to be set by caller
@@ -597,8 +757,6 @@
uint8_t *dst = (uint8_t *)event + event->data_offset;
uint8_t *src = (uint8_t *)&halEvent->data[0];
memcpy(dst, src, halEvent->data.size());
-
- return event;
}
} // namespace android
diff --git a/services/soundtrigger/SoundTriggerHalHidl.h b/services/soundtrigger/SoundTriggerHalHidl.h
index 0c68cf1..0b44ae0 100644
--- a/services/soundtrigger/SoundTriggerHalHidl.h
+++ b/services/soundtrigger/SoundTriggerHalHidl.h
@@ -17,6 +17,8 @@
#ifndef ANDROID_HARDWARE_SOUNDTRIGGER_HAL_HIDL_H
#define ANDROID_HARDWARE_SOUNDTRIGGER_HAL_HIDL_H
+#include <utility>
+
#include <stdatomic.h>
#include <utils/RefBase.h>
#include <utils/KeyedVector.h>
@@ -24,21 +26,29 @@
#include <utils/threads.h>
#include "SoundTriggerHalInterface.h"
#include <android/hardware/soundtrigger/2.0/types.h>
-#include <android/hardware/soundtrigger/2.0/ISoundTriggerHw.h>
+#include <android/hardware/soundtrigger/2.1/ISoundTriggerHw.h>
#include <android/hardware/soundtrigger/2.0/ISoundTriggerHwCallback.h>
+#include <android/hardware/soundtrigger/2.1/ISoundTriggerHwCallback.h>
namespace android {
-using android::hardware::audio::common::V2_0::Uuid;
-using android::hardware::soundtrigger::V2_0::ConfidenceLevel;
-using android::hardware::soundtrigger::V2_0::PhraseRecognitionExtra;
-using android::hardware::soundtrigger::V2_0::SoundModelType;
-using android::hardware::soundtrigger::V2_0::SoundModelHandle;
-using android::hardware::soundtrigger::V2_0::ISoundTriggerHw;
-using android::hardware::soundtrigger::V2_0::ISoundTriggerHwCallback;
+using ::android::hardware::audio::common::V2_0::Uuid;
+using ::android::hardware::hidl_vec;
+using ::android::hardware::soundtrigger::V2_0::ConfidenceLevel;
+using ::android::hardware::soundtrigger::V2_0::PhraseRecognitionExtra;
+using ::android::hardware::soundtrigger::V2_0::SoundModelType;
+using ::android::hardware::soundtrigger::V2_0::SoundModelHandle;
+using ::android::hardware::soundtrigger::V2_0::ISoundTriggerHw;
+using V2_0_ISoundTriggerHwCallback =
+ ::android::hardware::soundtrigger::V2_0::ISoundTriggerHwCallback;
+using V2_1_ISoundTriggerHw =
+ ::android::hardware::soundtrigger::V2_1::ISoundTriggerHw;
+using V2_1_ISoundTriggerHwCallback =
+ ::android::hardware::soundtrigger::V2_1::ISoundTriggerHwCallback;
+using ::android::hidl::memory::V1_0::IMemory;
class SoundTriggerHalHidl : public SoundTriggerHalInterface,
- public virtual ISoundTriggerHwCallback
+ public virtual V2_1_ISoundTriggerHwCallback
{
public:
@@ -84,11 +94,17 @@
// ISoundTriggerHwCallback
virtual ::android::hardware::Return<void> recognitionCallback(
- const ISoundTriggerHwCallback::RecognitionEvent& event, CallbackCookie cookie);
+ const V2_0_ISoundTriggerHwCallback::RecognitionEvent& event, CallbackCookie cookie);
virtual ::android::hardware::Return<void> phraseRecognitionCallback(
- const ISoundTriggerHwCallback::PhraseRecognitionEvent& event, int32_t cookie);
+ const V2_0_ISoundTriggerHwCallback::PhraseRecognitionEvent& event, int32_t cookie);
virtual ::android::hardware::Return<void> soundModelCallback(
- const ISoundTriggerHwCallback::ModelEvent& event, CallbackCookie cookie);
+ const V2_0_ISoundTriggerHwCallback::ModelEvent& event, CallbackCookie cookie);
+ virtual ::android::hardware::Return<void> recognitionCallback_2_1(
+ const RecognitionEvent& event, CallbackCookie cookie);
+ virtual ::android::hardware::Return<void> phraseRecognitionCallback_2_1(
+ const PhraseRecognitionEvent& event, int32_t cookie);
+ virtual ::android::hardware::Return<void> soundModelCallback_2_1(
+ const ModelEvent& event, CallbackCookie cookie);
private:
class SoundModel : public RefBase {
public:
@@ -124,25 +140,48 @@
void convertTriggerPhraseToHal(
ISoundTriggerHw::Phrase *halTriggerPhrase,
const struct sound_trigger_phrase *triggerPhrase);
- ISoundTriggerHw::SoundModel *convertSoundModelToHal(
+ void convertTriggerPhrasesToHal(
+ hidl_vec<ISoundTriggerHw::Phrase> *halTriggerPhrases,
+ struct sound_trigger_phrase_sound_model *keyPhraseModel);
+ void convertSoundModelToHal(ISoundTriggerHw::SoundModel *halModel,
const struct sound_trigger_sound_model *soundModel);
+ std::pair<bool, sp<IMemory>> convertSoundModelToHal(
+ V2_1_ISoundTriggerHw::SoundModel *halModel,
+ const struct sound_trigger_sound_model *soundModel)
+ __attribute__((warn_unused_result));
+ void convertPhraseSoundModelToHal(ISoundTriggerHw::PhraseSoundModel *halKeyPhraseModel,
+ const struct sound_trigger_sound_model *soundModel);
+ std::pair<bool, sp<IMemory>> convertPhraseSoundModelToHal(
+ V2_1_ISoundTriggerHw::PhraseSoundModel *halKeyPhraseModel,
+ const struct sound_trigger_sound_model *soundModel)
+ __attribute__((warn_unused_result));
void convertPhraseRecognitionExtraToHal(
PhraseRecognitionExtra *halExtra,
const struct sound_trigger_phrase_recognition_extra *extra);
- ISoundTriggerHw::RecognitionConfig *convertRecognitionConfigToHal(
+ void convertRecognitionConfigToHal(ISoundTriggerHw::RecognitionConfig *halConfig,
const struct sound_trigger_recognition_config *config);
+ std::pair<bool, sp<IMemory>> convertRecognitionConfigToHal(
+ V2_1_ISoundTriggerHw::RecognitionConfig *halConfig,
+ const struct sound_trigger_recognition_config *config)
+ __attribute__((warn_unused_result));
struct sound_trigger_model_event *convertSoundModelEventFromHal(
- const ISoundTriggerHwCallback::ModelEvent *halEvent);
+ const V2_0_ISoundTriggerHwCallback::ModelEvent *halEvent);
void convertPhraseRecognitionExtraFromHal(
struct sound_trigger_phrase_recognition_extra *extra,
const PhraseRecognitionExtra *halExtra);
+ struct sound_trigger_phrase_recognition_event* convertPhraseRecognitionEventFromHal(
+ const V2_0_ISoundTriggerHwCallback::PhraseRecognitionEvent *halPhraseEvent);
struct sound_trigger_recognition_event *convertRecognitionEventFromHal(
- const ISoundTriggerHwCallback::RecognitionEvent *halEvent);
+ const V2_0_ISoundTriggerHwCallback::RecognitionEvent *halEvent);
+ void fillRecognitionEventFromHal(
+ struct sound_trigger_recognition_event *event,
+ const V2_0_ISoundTriggerHwCallback::RecognitionEvent *halEvent);
uint32_t nextUniqueId();
sp<ISoundTriggerHw> getService();
+ sp<V2_1_ISoundTriggerHw> toService2_1(const sp<ISoundTriggerHw>& s);
sp<SoundModel> getModel(sound_model_handle_t handle);
sp<SoundModel> removeModel(sound_model_handle_t handle);