Merge changes I6bcc87c6,If633e677
* changes:
Revert "audio policy: refactor audio record APIs"
Revert "audio flinger: move record permission checks to audio policy service"
diff --git a/include/media/MicrophoneInfo.h b/include/media/MicrophoneInfo.h
new file mode 100644
index 0000000..b0199d4
--- /dev/null
+++ b/include/media/MicrophoneInfo.h
@@ -0,0 +1,249 @@
+/*
+ * 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_MICROPHONE_INFO_H
+#define ANDROID_MICROPHONE_INFO_H
+
+#include <binder/Parcel.h>
+#include <binder/Parcelable.h>
+#include <system/audio.h>
+#include <utils/String16.h>
+#include <utils/Vector.h>
+
+namespace android {
+namespace media {
+
+#define RETURN_IF_FAILED(calledOnce) \
+ { \
+ status_t returnStatus = calledOnce; \
+ if (returnStatus) { \
+ ALOGE("Failed at %s:%d (%s)", __FILE__, __LINE__, __func__); \
+ return returnStatus; \
+ } \
+ }
+
+class MicrophoneInfo : public Parcelable {
+public:
+ MicrophoneInfo() = default;
+ MicrophoneInfo(const MicrophoneInfo& microphoneInfo) = default;
+ MicrophoneInfo(audio_microphone_characteristic_t& characteristic) {
+ mDeviceId = String16(&characteristic.device_id[0]);
+ mPortId = characteristic.id;
+ mType = characteristic.type;
+ mAddress = String16(&characteristic.address[0]);
+ mDeviceLocation = characteristic.location;
+ mDeviceGroup = characteristic.group;
+ mIndexInTheGroup = characteristic.index_in_the_group;
+ mGeometricLocation.push_back(characteristic.geometric_location.x);
+ mGeometricLocation.push_back(characteristic.geometric_location.y);
+ mGeometricLocation.push_back(characteristic.geometric_location.z);
+ mOrientation.push_back(characteristic.orientation.x);
+ mOrientation.push_back(characteristic.orientation.y);
+ mOrientation.push_back(characteristic.orientation.z);
+ Vector<float> frequencies;
+ Vector<float> responses;
+ for (size_t i = 0; i < characteristic.num_frequency_responses; i++) {
+ frequencies.push_back(characteristic.frequency_responses[0][i]);
+ responses.push_back(characteristic.frequency_responses[1][i]);
+ }
+ mFrequencyResponses.push_back(frequencies);
+ mFrequencyResponses.push_back(responses);
+ for (size_t i = 0; i < AUDIO_CHANNEL_COUNT_MAX; i++) {
+ mChannelMapping.push_back(characteristic.channel_mapping[i]);
+ }
+ mSensitivity = characteristic.sensitivity;
+ mMaxSpl = characteristic.max_spl;
+ mMinSpl = characteristic.min_spl;
+ mDirectionality = characteristic.directionality;
+ }
+
+ virtual ~MicrophoneInfo() = default;
+
+ virtual status_t writeToParcel(Parcel* parcel) const {
+ RETURN_IF_FAILED(parcel->writeString16(mDeviceId));
+ RETURN_IF_FAILED(parcel->writeInt32(mPortId));
+ RETURN_IF_FAILED(parcel->writeUint32(mType));
+ RETURN_IF_FAILED(parcel->writeString16(mAddress));
+ RETURN_IF_FAILED(parcel->writeInt32(mDeviceLocation));
+ RETURN_IF_FAILED(parcel->writeInt32(mDeviceGroup));
+ RETURN_IF_FAILED(parcel->writeInt32(mIndexInTheGroup));
+ RETURN_IF_FAILED(writeFloatVector(parcel, mGeometricLocation));
+ RETURN_IF_FAILED(writeFloatVector(parcel, mOrientation));
+ if (mFrequencyResponses.size() != 2) {
+ return BAD_VALUE;
+ }
+ for (size_t i = 0; i < mFrequencyResponses.size(); i++) {
+ RETURN_IF_FAILED(parcel->writeInt32(mFrequencyResponses[i].size()));
+ RETURN_IF_FAILED(writeFloatVector(parcel, mFrequencyResponses[i]));
+ }
+ std::vector<int> channelMapping;
+ for (size_t i = 0; i < mChannelMapping.size(); ++i) {
+ channelMapping.push_back(mChannelMapping[i]);
+ }
+ RETURN_IF_FAILED(parcel->writeInt32Vector(channelMapping));
+ RETURN_IF_FAILED(parcel->writeFloat(mSensitivity));
+ RETURN_IF_FAILED(parcel->writeFloat(mMaxSpl));
+ RETURN_IF_FAILED(parcel->writeFloat(mMinSpl));
+ RETURN_IF_FAILED(parcel->writeInt32(mDirectionality));
+ return OK;
+ }
+
+ virtual status_t readFromParcel(const Parcel* parcel) {
+ RETURN_IF_FAILED(parcel->readString16(&mDeviceId));
+ RETURN_IF_FAILED(parcel->readInt32(&mPortId));
+ RETURN_IF_FAILED(parcel->readUint32(&mType));
+ RETURN_IF_FAILED(parcel->readString16(&mAddress));
+ RETURN_IF_FAILED(parcel->readInt32(&mDeviceLocation));
+ RETURN_IF_FAILED(parcel->readInt32(&mDeviceGroup));
+ RETURN_IF_FAILED(parcel->readInt32(&mIndexInTheGroup));
+ RETURN_IF_FAILED(readFloatVector(parcel, &mGeometricLocation, 3));
+ RETURN_IF_FAILED(readFloatVector(parcel, &mOrientation, 3));
+ int32_t frequenciesNum;
+ RETURN_IF_FAILED(parcel->readInt32(&frequenciesNum));
+ Vector<float> frequencies;
+ RETURN_IF_FAILED(readFloatVector(parcel, &frequencies, frequenciesNum));
+ int32_t responsesNum;
+ RETURN_IF_FAILED(parcel->readInt32(&responsesNum));
+ Vector<float> responses;
+ RETURN_IF_FAILED(readFloatVector(parcel, &responses, responsesNum));
+ if (frequencies.size() != responses.size()) {
+ return BAD_VALUE;
+ }
+ mFrequencyResponses.push_back(frequencies);
+ mFrequencyResponses.push_back(responses);
+ std::vector<int> channelMapping;
+ status_t result = parcel->readInt32Vector(&channelMapping);
+ if (result != OK) {
+ return result;
+ }
+ if (channelMapping.size() != AUDIO_CHANNEL_COUNT_MAX) {
+ return BAD_VALUE;
+ }
+ for (size_t i = 0; i < channelMapping.size(); i++) {
+ mChannelMapping.push_back(channelMapping[i]);
+ }
+ RETURN_IF_FAILED(parcel->readFloat(&mSensitivity));
+ RETURN_IF_FAILED(parcel->readFloat(&mMaxSpl));
+ RETURN_IF_FAILED(parcel->readFloat(&mMinSpl));
+ RETURN_IF_FAILED(parcel->readInt32(&mDirectionality));
+ return OK;
+ }
+
+ String16 getDeviceId() const {
+ return mDeviceId;
+ }
+
+ int getPortId() const {
+ return mPortId;
+ }
+
+ unsigned int getType() const {
+ return mType;
+ }
+
+ String16 getAddress() const {
+ return mAddress;
+ }
+
+ int getDeviceLocation() const {
+ return mDeviceLocation;
+ }
+
+ int getDeviceGroup() const {
+ return mDeviceGroup;
+ }
+
+ int getIndexInTheGroup() const {
+ return mIndexInTheGroup;
+ }
+
+ const Vector<float>& getGeometricLocation() const {
+ return mGeometricLocation;
+ }
+
+ const Vector<float>& getOrientation() const {
+ return mOrientation;
+ }
+
+ const Vector<Vector<float>>& getFrequencyResponses() const {
+ return mFrequencyResponses;
+ }
+
+ const Vector<int>& getChannelMapping() const {
+ return mChannelMapping;
+ }
+
+ float getSensitivity() const {
+ return mSensitivity;
+ }
+
+ float getMaxSpl() const {
+ return mMaxSpl;
+ }
+
+ float getMinSpl() const {
+ return mMinSpl;
+ }
+
+ int getDirectionality() const {
+ return mDirectionality;
+ }
+
+private:
+ status_t readFloatVector(
+ const Parcel* parcel, Vector<float> *vectorPtr, size_t defaultLength) {
+ std::unique_ptr<std::vector<float>> v;
+ status_t result = parcel->readFloatVector(&v);
+ if (result != OK) return result;
+ vectorPtr->clear();
+ if (v.get() != nullptr) {
+ for (const auto& iter : *v) {
+ vectorPtr->push_back(iter);
+ }
+ } else {
+ vectorPtr->resize(defaultLength);
+ }
+ return OK;
+ }
+ status_t writeFloatVector(Parcel* parcel, const Vector<float>& vector) const {
+ std::vector<float> v;
+ for (size_t i = 0; i < vector.size(); i++) {
+ v.push_back(vector[i]);
+ }
+ return parcel->writeFloatVector(v);
+ }
+
+ String16 mDeviceId;
+ int32_t mPortId;
+ uint32_t mType;
+ String16 mAddress;
+ int32_t mDeviceLocation;
+ int32_t mDeviceGroup;
+ int32_t mIndexInTheGroup;
+ Vector<float> mGeometricLocation;
+ Vector<float> mOrientation;
+ Vector<Vector<float>> mFrequencyResponses;
+ Vector<int> mChannelMapping;
+ float mSensitivity;
+ float mMaxSpl;
+ float mMinSpl;
+ int32_t mDirectionality;
+};
+
+} // namespace media
+} // namespace android
+
+#endif
diff --git a/media/extractors/mp4/MPEG4Extractor.h b/media/extractors/mp4/MPEG4Extractor.h
index 8bfecaa..35c5321 100644
--- a/media/extractors/mp4/MPEG4Extractor.h
+++ b/media/extractors/mp4/MPEG4Extractor.h
@@ -22,6 +22,7 @@
#include <media/DataSource.h>
#include <media/MediaExtractor.h>
+#include <media/stagefright/foundation/AString.h>
#include <utils/List.h>
#include <utils/Vector.h>
#include <utils/String8.h>
diff --git a/media/extractors/mpeg2/MPEG2TSExtractor.cpp b/media/extractors/mpeg2/MPEG2TSExtractor.cpp
index fef4d30..5beaeda 100644
--- a/media/extractors/mpeg2/MPEG2TSExtractor.cpp
+++ b/media/extractors/mpeg2/MPEG2TSExtractor.cpp
@@ -202,9 +202,7 @@
break;
}
if (!haveVideo) {
- sp<AnotherPacketSource> impl =
- (AnotherPacketSource *)mParser->getSource(
- ATSParser::VIDEO).get();
+ sp<AnotherPacketSource> impl = mParser->getSource(ATSParser::VIDEO);
if (impl != NULL) {
sp<MetaData> format = impl->getFormat();
@@ -220,9 +218,7 @@
}
if (!haveAudio) {
- sp<AnotherPacketSource> impl =
- (AnotherPacketSource *)mParser->getSource(
- ATSParser::AUDIO).get();
+ sp<AnotherPacketSource> impl = mParser->getSource(ATSParser::AUDIO);
if (impl != NULL) {
sp<MetaData> format = impl->getFormat();
@@ -261,10 +257,8 @@
off64_t size;
if (mDataSource->getSize(&size) == OK && (haveAudio || haveVideo)) {
sp<AnotherPacketSource> impl = haveVideo
- ? (AnotherPacketSource *)mParser->getSource(
- ATSParser::VIDEO).get()
- : (AnotherPacketSource *)mParser->getSource(
- ATSParser::AUDIO).get();
+ ? mParser->getSource(ATSParser::VIDEO)
+ : mParser->getSource(ATSParser::AUDIO);
size_t prevSyncSize = 1;
int64_t durationUs = -1;
List<int64_t> durations;
@@ -420,8 +414,7 @@
ev.reset();
int64_t firstTimeUs;
- sp<AnotherPacketSource> src =
- (AnotherPacketSource *)mParser->getSource(type).get();
+ sp<AnotherPacketSource> src = mParser->getSource(type);
if (src == NULL || src->nextBufferTime(&firstTimeUs) != OK) {
continue;
}
@@ -449,7 +442,7 @@
if (!allDurationsFound) {
allDurationsFound = true;
for (auto t: {ATSParser::VIDEO, ATSParser::AUDIO}) {
- sp<AnotherPacketSource> src = (AnotherPacketSource *)mParser->getSource(t).get();
+ sp<AnotherPacketSource> src = mParser->getSource(t);
if (src == NULL) {
continue;
}
diff --git a/media/libaudioclient/AudioSystem.cpp b/media/libaudioclient/AudioSystem.cpp
index 3bb09d2..9dfb514 100644
--- a/media/libaudioclient/AudioSystem.cpp
+++ b/media/libaudioclient/AudioSystem.cpp
@@ -1278,6 +1278,13 @@
return aps->getStreamVolumeDB(stream, index, device);
}
+status_t AudioSystem::getMicrophones(std::vector<media::MicrophoneInfo> *microphones)
+{
+ const sp<IAudioFlinger>& af = AudioSystem::get_audio_flinger();
+ if (af == 0) return PERMISSION_DENIED;
+ return af->getMicrophones(microphones);
+}
+
// ---------------------------------------------------------------------------
int AudioSystem::AudioPolicyServiceClient::addAudioPortCallback(
diff --git a/media/libaudioclient/IAudioFlinger.cpp b/media/libaudioclient/IAudioFlinger.cpp
index ae9c96f..b6c98cc 100644
--- a/media/libaudioclient/IAudioFlinger.cpp
+++ b/media/libaudioclient/IAudioFlinger.cpp
@@ -85,6 +85,7 @@
GET_AUDIO_HW_SYNC_FOR_SESSION,
SYSTEM_READY,
FRAME_COUNT_HAL,
+ LIST_MICROPHONES,
};
#define MAX_ITEMS_PER_LIST 1024
@@ -842,6 +843,18 @@
}
return reply.readInt64();
}
+ virtual status_t getMicrophones(std::vector<media::MicrophoneInfo> *microphones)
+ {
+ Parcel data, reply;
+ data.writeInterfaceToken(IAudioFlinger::getInterfaceDescriptor());
+ status_t status = remote()->transact(LIST_MICROPHONES, data, &reply);
+ if (status != NO_ERROR ||
+ (status = (status_t)reply.readInt32()) != NO_ERROR) {
+ return status;
+ }
+ status = reply.readParcelableVector(microphones);
+ return status;
+ }
};
IMPLEMENT_META_INTERFACE(AudioFlinger, "android.media.IAudioFlinger");
@@ -1407,6 +1420,16 @@
reply->writeInt64( frameCountHAL((audio_io_handle_t) data.readInt32()) );
return NO_ERROR;
} break;
+ case LIST_MICROPHONES: {
+ CHECK_INTERFACE(IAudioFlinger, data, reply);
+ std::vector<media::MicrophoneInfo> microphones;
+ status_t status = getMicrophones(µphones);
+ reply->writeInt32(status);
+ if (status == NO_ERROR) {
+ reply->writeParcelableVector(microphones);
+ }
+ return NO_ERROR;
+ }
default:
return BBinder::onTransact(code, data, reply, flags);
}
diff --git a/media/libaudioclient/aidl/android/media/MicrophoneInfo.aidl b/media/libaudioclient/aidl/android/media/MicrophoneInfo.aidl
new file mode 100644
index 0000000..d6e46cb
--- /dev/null
+++ b/media/libaudioclient/aidl/android/media/MicrophoneInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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 android.media;
+
+parcelable MicrophoneInfo cpp_header "media/MicrophoneInfo.h";
diff --git a/media/libaudioclient/include/media/AudioSystem.h b/media/libaudioclient/include/media/AudioSystem.h
index f7c3d03..3c8e7bc 100644
--- a/media/libaudioclient/include/media/AudioSystem.h
+++ b/media/libaudioclient/include/media/AudioSystem.h
@@ -23,11 +23,13 @@
#include <media/AudioIoDescriptor.h>
#include <media/IAudioFlingerClient.h>
#include <media/IAudioPolicyServiceClient.h>
+#include <media/MicrophoneInfo.h>
#include <system/audio.h>
#include <system/audio_effect.h>
#include <system/audio_policy.h>
#include <utils/Errors.h>
#include <utils/Mutex.h>
+#include <vector>
namespace android {
@@ -340,6 +342,8 @@
static float getStreamVolumeDB(
audio_stream_type_t stream, int index, audio_devices_t device);
+ static status_t getMicrophones(std::vector<media::MicrophoneInfo> *microphones);
+
// ----------------------------------------------------------------------------
class AudioPortCallback : public RefBase
diff --git a/media/libaudioclient/include/media/IAudioFlinger.h b/media/libaudioclient/include/media/IAudioFlinger.h
index e8d405b..e6bf72f 100644
--- a/media/libaudioclient/include/media/IAudioFlinger.h
+++ b/media/libaudioclient/include/media/IAudioFlinger.h
@@ -35,6 +35,8 @@
#include <media/IEffect.h>
#include <media/IEffectClient.h>
#include <utils/String8.h>
+#include <media/MicrophoneInfo.h>
+#include <vector>
#include "android/media/IAudioRecord.h"
@@ -486,6 +488,9 @@
// Returns the number of frames per audio HAL buffer.
virtual size_t frameCountHAL(audio_io_handle_t ioHandle) const = 0;
+
+ /* List available microphones and their characteristics */
+ virtual status_t getMicrophones(std::vector<media::MicrophoneInfo> *microphones) = 0;
};
diff --git a/media/libheif/HeifDecoderImpl.cpp b/media/libheif/HeifDecoderImpl.cpp
index a63a2df..2dfbdca 100644
--- a/media/libheif/HeifDecoderImpl.cpp
+++ b/media/libheif/HeifDecoderImpl.cpp
@@ -22,6 +22,7 @@
#include <stdio.h>
#include <binder/IMemory.h>
+#include <binder/MemoryDealer.h>
#include <drm/drm_framework_common.h>
#include <media/IDataSource.h>
#include <media/mediametadataretriever.h>
diff --git a/media/libmedia/MediaPlayer2Factory.cpp b/media/libmedia/MediaPlayer2Factory.cpp
index df567ce..ac115f6 100644
--- a/media/libmedia/MediaPlayer2Factory.cpp
+++ b/media/libmedia/MediaPlayer2Factory.cpp
@@ -34,9 +34,17 @@
namespace android {
Mutex MediaPlayer2Factory::sLock;
-MediaPlayer2Factory::tFactoryMap MediaPlayer2Factory::sFactoryMap;
+MediaPlayer2Factory::tFactoryMap *MediaPlayer2Factory::sFactoryMap;
bool MediaPlayer2Factory::sInitComplete = false;
+// static
+bool MediaPlayer2Factory::ensureInit_l() {
+ if (sFactoryMap == NULL) {
+ sFactoryMap = new (std::nothrow) tFactoryMap();
+ }
+ return (sFactoryMap != NULL);
+}
+
status_t MediaPlayer2Factory::registerFactory_l(IFactory* factory,
player2_type type) {
if (NULL == factory) {
@@ -45,13 +53,17 @@
return BAD_VALUE;
}
- if (sFactoryMap.indexOfKey(type) >= 0) {
+ if (!ensureInit_l()) {
+ return NO_INIT;
+ }
+
+ if (sFactoryMap->indexOfKey(type) >= 0) {
ALOGE("Failed to register MediaPlayer2Factory of type %d, type is"
" already registered.", type);
return ALREADY_EXISTS;
}
- if (sFactoryMap.add(type, factory) < 0) {
+ if (sFactoryMap->add(type, factory) < 0) {
ALOGE("Failed to register MediaPlayer2Factory of type %d, failed to add"
" to map.", type);
return UNKNOWN_ERROR;
@@ -64,31 +76,24 @@
return PLAYER2_NU_PLAYER2;
}
-status_t MediaPlayer2Factory::registerFactory(IFactory* factory,
- player2_type type) {
- Mutex::Autolock lock_(&sLock);
- return registerFactory_l(factory, type);
-}
-
-void MediaPlayer2Factory::unregisterFactory(player2_type type) {
- Mutex::Autolock lock_(&sLock);
- sFactoryMap.removeItem(type);
-}
-
#define GET_PLAYER_TYPE_IMPL(a...) \
Mutex::Autolock lock_(&sLock); \
\
player2_type ret = PLAYER2_STAGEFRIGHT_PLAYER; \
float bestScore = 0.0; \
\
- for (size_t i = 0; i < sFactoryMap.size(); ++i) { \
+ if (!ensureInit_l()) { \
+ return ret; \
+ } \
\
- IFactory* v = sFactoryMap.valueAt(i); \
+ for (size_t i = 0; i < sFactoryMap->size(); ++i) { \
+ \
+ IFactory* v = sFactoryMap->valueAt(i); \
float thisScore; \
CHECK(v != NULL); \
thisScore = v->scoreFactory(a, bestScore); \
if (thisScore > bestScore) { \
- ret = sFactoryMap.keyAt(i); \
+ ret = sFactoryMap->keyAt(i); \
bestScore = thisScore; \
} \
} \
@@ -133,13 +138,17 @@
status_t init_result;
Mutex::Autolock lock_(&sLock);
- if (sFactoryMap.indexOfKey(playerType) < 0) {
+ if (!ensureInit_l()) {
+ return NULL;
+ }
+
+ if (sFactoryMap->indexOfKey(playerType) < 0) {
ALOGE("Failed to create player object of type %d, no registered"
" factory", playerType);
return p;
}
- factory = sFactoryMap.valueFor(playerType);
+ factory = sFactoryMap->valueFor(playerType);
CHECK(NULL != factory);
p = factory->createPlayer(pid);
diff --git a/media/libmedia/MediaPlayer2Factory.h b/media/libmedia/MediaPlayer2Factory.h
index 799b5f3..416d241 100644
--- a/media/libmedia/MediaPlayer2Factory.h
+++ b/media/libmedia/MediaPlayer2Factory.h
@@ -50,9 +50,6 @@
virtual sp<MediaPlayer2Base> createPlayer(pid_t pid) = 0;
};
- static status_t registerFactory(IFactory* factory,
- player2_type type);
- static void unregisterFactory(player2_type type);
static player2_type getPlayerType(const sp<MediaPlayer2Engine>& client,
const char* url);
static player2_type getPlayerType(const sp<MediaPlayer2Engine>& client,
@@ -76,11 +73,13 @@
MediaPlayer2Factory() { }
+ static bool ensureInit_l();
+
static status_t registerFactory_l(IFactory* factory,
player2_type type);
static Mutex sLock;
- static tFactoryMap sFactoryMap;
+ static tFactoryMap *sFactoryMap;
static bool sInitComplete;
DISALLOW_EVIL_CONSTRUCTORS(MediaPlayer2Factory);
diff --git a/media/libmedia/nuplayer2/GenericSource.cpp b/media/libmedia/nuplayer2/GenericSource.cpp
index 6d5b14d..094af7e 100644
--- a/media/libmedia/nuplayer2/GenericSource.cpp
+++ b/media/libmedia/nuplayer2/GenericSource.cpp
@@ -26,6 +26,7 @@
#include <media/DataSource.h>
#include <media/MediaBufferHolder.h>
#include <media/IMediaExtractorService.h>
+#include <media/IMediaSource.h>
#include <media/MediaHTTPService.h>
#include <media/MediaExtractor.h>
#include <media/MediaSource.h>
diff --git a/media/libmedia/nuplayer2/GenericSource.h b/media/libmedia/nuplayer2/GenericSource.h
index 0666d27..5a71edb 100644
--- a/media/libmedia/nuplayer2/GenericSource.h
+++ b/media/libmedia/nuplayer2/GenericSource.h
@@ -33,6 +33,7 @@
struct ARTSPController;
class DataSource;
class IDataSource;
+class IMediaSource;
struct MediaHTTPService;
struct MediaSource;
class MediaBuffer;
diff --git a/media/libmediaextractor/include/media/DataSource.h b/media/libmediaextractor/include/media/DataSource.h
index 44f94a0..9925a21 100644
--- a/media/libmediaextractor/include/media/DataSource.h
+++ b/media/libmediaextractor/include/media/DataSource.h
@@ -19,23 +19,15 @@
#define DATA_SOURCE_H_
#include <sys/types.h>
-#include <media/stagefright/foundation/ADebug.h>
#include <media/stagefright/MediaErrors.h>
#include <utils/Errors.h>
-#include <utils/KeyedVector.h>
-#include <utils/List.h>
#include <utils/RefBase.h>
#include <utils/threads.h>
#include <drm/DrmManagerClient.h>
namespace android {
-struct AMessage;
-struct AString;
-class IDataSource;
-struct IMediaHTTPService;
class String8;
-struct HTTPBase;
class DataSource : public RefBase {
public:
diff --git a/media/libmediaextractor/include/media/MediaExtractor.h b/media/libmediaextractor/include/media/MediaExtractor.h
index f197b5e..276cbed 100644
--- a/media/libmediaextractor/include/media/MediaExtractor.h
+++ b/media/libmediaextractor/include/media/MediaExtractor.h
@@ -27,10 +27,7 @@
namespace android {
class DataSource;
-class IMediaSource;
-class MediaExtractorFactory;
class MetaData;
-class Parcel;
class String8;
struct AMessage;
struct MediaSource;
diff --git a/media/libmediaextractor/include/media/MediaSource.h b/media/libmediaextractor/include/media/MediaSource.h
index 25d691d..3df9a0d 100644
--- a/media/libmediaextractor/include/media/MediaSource.h
+++ b/media/libmediaextractor/include/media/MediaSource.h
@@ -20,17 +20,13 @@
#include <sys/types.h>
-#include <binder/IMemory.h>
-#include <binder/MemoryDealer.h>
#include <media/stagefright/MediaErrors.h>
#include <media/stagefright/MetaData.h>
#include <utils/RefBase.h>
-#include <utils/Vector.h>
namespace android {
class MediaBuffer;
-class IMediaSource;
struct MediaSource : public virtual RefBase {
MediaSource();
diff --git a/media/libmediaextractor/include/media/stagefright/MediaBuffer.h b/media/libmediaextractor/include/media/stagefright/MediaBuffer.h
index 2b51081..a8f8375 100644
--- a/media/libmediaextractor/include/media/stagefright/MediaBuffer.h
+++ b/media/libmediaextractor/include/media/stagefright/MediaBuffer.h
@@ -30,7 +30,6 @@
namespace android {
struct ABuffer;
-class GraphicBuffer;
class MediaBuffer;
class MediaBufferObserver;
class MetaData;
@@ -143,7 +142,7 @@
return mObserver != nullptr;
}
- ~MediaBuffer();
+ virtual ~MediaBuffer();
sp<IMemory> mMemory;
diff --git a/media/libmediaplayerservice/nuplayer/GenericSource.h b/media/libmediaplayerservice/nuplayer/GenericSource.h
index 856f03b..2406665 100644
--- a/media/libmediaplayerservice/nuplayer/GenericSource.h
+++ b/media/libmediaplayerservice/nuplayer/GenericSource.h
@@ -35,6 +35,7 @@
class IDataSource;
struct IMediaHTTPService;
struct MediaSource;
+class IMediaSource;
class MediaBuffer;
struct MediaClock;
struct NuCachedSource2;
diff --git a/media/libstagefright/MediaExtractorFactory.cpp b/media/libstagefright/MediaExtractorFactory.cpp
index 472c137..e79696c 100644
--- a/media/libstagefright/MediaExtractorFactory.cpp
+++ b/media/libstagefright/MediaExtractorFactory.cpp
@@ -358,12 +358,16 @@
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());
+ if (gPluginsRegistered) {
+ 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());
+ }
+ } else {
+ out.append(" (no plugins registered)\n");
}
write(fd, out.string(), out.size());
return OK;
diff --git a/media/libstagefright/include/FrameDecoder.h b/media/libstagefright/include/FrameDecoder.h
index d7c074c..6f07838 100644
--- a/media/libstagefright/include/FrameDecoder.h
+++ b/media/libstagefright/include/FrameDecoder.h
@@ -27,6 +27,7 @@
struct AMessage;
class MediaCodecBuffer;
+class IMediaSource;
class VideoFrame;
struct FrameDecoder {
diff --git a/media/libstagefright/include/HTTPBase.h b/media/libstagefright/include/HTTPBase.h
index 26d7e8a..a924197 100644
--- a/media/libstagefright/include/HTTPBase.h
+++ b/media/libstagefright/include/HTTPBase.h
@@ -21,6 +21,7 @@
#include <media/DataSource.h>
#include <media/stagefright/foundation/ABase.h>
#include <media/stagefright/MediaErrors.h>
+#include <utils/List.h>
#include <utils/threads.h>
namespace android {
diff --git a/media/libstagefright/include/media/stagefright/CallbackMediaSource.h b/media/libstagefright/include/media/stagefright/CallbackMediaSource.h
index 3459de1..944d951 100644
--- a/media/libstagefright/include/media/stagefright/CallbackMediaSource.h
+++ b/media/libstagefright/include/media/stagefright/CallbackMediaSource.h
@@ -22,6 +22,8 @@
namespace android {
+class IMediaSource;
+
// A stagefright MediaSource that wraps a binder IMediaSource.
class CallbackMediaSource : public MediaSource {
public:
diff --git a/media/libstagefright/include/media/stagefright/InterfaceUtils.h b/media/libstagefright/include/media/stagefright/InterfaceUtils.h
index 224c1f1..d449aec 100644
--- a/media/libstagefright/include/media/stagefright/InterfaceUtils.h
+++ b/media/libstagefright/include/media/stagefright/InterfaceUtils.h
@@ -17,13 +17,17 @@
#ifndef INTERFACE_UTILS_H_
#define INTERFACE_UTILS_H_
-#include <media/MediaExtractor.h>
-#include <media/MediaSource.h>
-#include <media/IMediaExtractor.h>
-#include <media/IMediaSource.h>
+#include <utils/RefBase.h>
namespace android {
+class DataSource;
+class MediaExtractor;
+struct MediaSource;
+class IDataSource;
+class IMediaExtractor;
+class IMediaSource;
+
// Creates a DataSource which wraps the given IDataSource object.
sp<DataSource> CreateDataSourceFromIDataSource(const sp<IDataSource> &source);
diff --git a/media/libstagefright/include/media/stagefright/MediaExtractorFactory.h b/media/libstagefright/include/media/stagefright/MediaExtractorFactory.h
index e5c67e1..4610359 100644
--- a/media/libstagefright/include/media/stagefright/MediaExtractorFactory.h
+++ b/media/libstagefright/include/media/stagefright/MediaExtractorFactory.h
@@ -22,6 +22,7 @@
#include <media/IMediaExtractor.h>
#include <media/MediaExtractor.h>
+#include <utils/List.h>
namespace android {
diff --git a/media/libstagefright/include/media/stagefright/RemoteMediaExtractor.h b/media/libstagefright/include/media/stagefright/RemoteMediaExtractor.h
index 2bd71ee..bdbad7a 100644
--- a/media/libstagefright/include/media/stagefright/RemoteMediaExtractor.h
+++ b/media/libstagefright/include/media/stagefright/RemoteMediaExtractor.h
@@ -19,6 +19,7 @@
#include <media/IMediaExtractor.h>
#include <media/MediaExtractor.h>
+#include <media/stagefright/foundation/ABase.h>
namespace android {
diff --git a/media/libstagefright/mpeg2ts/ATSParser.cpp b/media/libstagefright/mpeg2ts/ATSParser.cpp
index 464ee90..9b12b2d 100644
--- a/media/libstagefright/mpeg2ts/ATSParser.cpp
+++ b/media/libstagefright/mpeg2ts/ATSParser.cpp
@@ -23,6 +23,8 @@
#include "ESQueue.h"
#include <android/hardware/cas/native/1.0/IDescrambler.h>
+#include <binder/IMemory.h>
+#include <binder/MemoryDealer.h>
#include <cutils/native_handle.h>
#include <media/stagefright/foundation/ABitReader.h>
#include <media/stagefright/foundation/ABuffer.h>
@@ -77,7 +79,7 @@
void signalEOS(status_t finalResult);
- sp<MediaSource> getSource(SourceType type);
+ sp<AnotherPacketSource> getSource(SourceType type);
bool hasSource(SourceType type) const;
int64_t convertPTSToTimestamp(uint64_t PTS);
@@ -170,7 +172,7 @@
void signalEOS(status_t finalResult);
SourceType getSourceType();
- sp<MediaSource> getSource(SourceType type);
+ sp<AnotherPacketSource> getSource(SourceType type);
bool isAudio() const;
bool isVideo() const;
@@ -274,7 +276,7 @@
ATSParser::SyncEvent::SyncEvent(off64_t offset)
: mHasReturnedData(false), mOffset(offset), mTimeUs(0) {}
-void ATSParser::SyncEvent::init(off64_t offset, const sp<MediaSource> &source,
+void ATSParser::SyncEvent::init(off64_t offset, const sp<AnotherPacketSource> &source,
int64_t timeUs, SourceType type) {
mHasReturnedData = true;
mOffset = offset;
@@ -641,9 +643,9 @@
return mLastRecoveredPTS;
}
-sp<MediaSource> ATSParser::Program::getSource(SourceType type) {
+sp<AnotherPacketSource> ATSParser::Program::getSource(SourceType type) {
for (size_t i = 0; i < mStreams.size(); ++i) {
- sp<MediaSource> source = mStreams.editValueAt(i)->getSource(type);
+ sp<AnotherPacketSource> source = mStreams.editValueAt(i)->getSource(type);
if (source != NULL) {
return source;
}
@@ -1607,7 +1609,7 @@
return NUM_SOURCE_TYPES;
}
-sp<MediaSource> ATSParser::Stream::getSource(SourceType type) {
+sp<AnotherPacketSource> ATSParser::Stream::getSource(SourceType type) {
switch (type) {
case VIDEO:
{
@@ -2042,11 +2044,11 @@
return err;
}
-sp<MediaSource> ATSParser::getSource(SourceType type) {
- sp<MediaSource> firstSourceFound;
+sp<AnotherPacketSource> ATSParser::getSource(SourceType type) {
+ sp<AnotherPacketSource> firstSourceFound;
for (size_t i = 0; i < mPrograms.size(); ++i) {
const sp<Program> &program = mPrograms.editItemAt(i);
- sp<MediaSource> source = program->getSource(type);
+ sp<AnotherPacketSource> source = program->getSource(type);
if (source == NULL) {
continue;
}
diff --git a/media/libstagefright/mpeg2ts/ATSParser.h b/media/libstagefright/mpeg2ts/ATSParser.h
index 6079afc..45ca06b 100644
--- a/media/libstagefright/mpeg2ts/ATSParser.h
+++ b/media/libstagefright/mpeg2ts/ATSParser.h
@@ -81,13 +81,13 @@
struct SyncEvent {
explicit SyncEvent(off64_t offset);
- void init(off64_t offset, const sp<MediaSource> &source,
+ void init(off64_t offset, const sp<AnotherPacketSource> &source,
int64_t timeUs, SourceType type);
bool hasReturnedData() const { return mHasReturnedData; }
void reset();
off64_t getOffset() const { return mOffset; }
- const sp<MediaSource> &getMediaSource() const { return mMediaSource; }
+ const sp<AnotherPacketSource> &getMediaSource() const { return mMediaSource; }
int64_t getTimeUs() const { return mTimeUs; }
SourceType getType() const { return mType; }
@@ -100,7 +100,7 @@
*/
off64_t mOffset;
/* The media source object for this event. */
- sp<MediaSource> mMediaSource;
+ sp<AnotherPacketSource> mMediaSource;
/* The timestamp of the sync frame. */
int64_t mTimeUs;
SourceType mType;
@@ -126,7 +126,7 @@
void signalEOS(status_t finalResult);
- sp<MediaSource> getSource(SourceType type);
+ sp<AnotherPacketSource> getSource(SourceType type);
bool hasSource(SourceType type) const;
bool PTSTimeDeltaEstablished();
diff --git a/media/ndk/NdkMediaExtractor.cpp b/media/ndk/NdkMediaExtractor.cpp
index 5dee8b0..ea43d2e 100644
--- a/media/ndk/NdkMediaExtractor.cpp
+++ b/media/ndk/NdkMediaExtractor.cpp
@@ -51,7 +51,6 @@
struct AMediaExtractor {
sp<NuMediaExtractor> mImpl;
sp<ABuffer> mPsshBuf;
-
};
extern "C" {
@@ -127,6 +126,13 @@
}
EXPORT
+AMediaFormat* AMediaExtractor_getFileFormat(AMediaExtractor *mData) {
+ sp<AMessage> format;
+ mData->mImpl->getFileFormat(&format);
+ return AMediaFormat_fromMsg(&format);
+}
+
+EXPORT
size_t AMediaExtractor_getTrackCount(AMediaExtractor *mData) {
return mData->mImpl->countTracks();
}
@@ -188,6 +194,16 @@
}
EXPORT
+ssize_t AMediaExtractor_getSampleSize(AMediaExtractor *mData) {
+ size_t sampleSize;
+ status_t err = mData->mImpl->getSampleSize(&sampleSize);
+ if (err != OK) {
+ return -1;
+ }
+ return sampleSize;
+}
+
+EXPORT
uint32_t AMediaExtractor_getSampleFlags(AMediaExtractor *mData) {
int sampleFlags = 0;
sp<MetaData> meta;
@@ -385,6 +401,15 @@
(size_t*) crypteddata);
}
+EXPORT
+int64_t AMediaExtractor_getCachedDuration(AMediaExtractor *ex) {
+ bool eos;
+ int64_t durationUs;
+ if (ex->mImpl->getCachedDuration(&durationUs, &eos)) {
+ return durationUs;
+ }
+ return -1;
+}
} // extern "C"
diff --git a/media/ndk/include/media/NdkMediaExtractor.h b/media/ndk/include/media/NdkMediaExtractor.h
index 820e9f5..3c9e23d 100644
--- a/media/ndk/include/media/NdkMediaExtractor.h
+++ b/media/ndk/include/media/NdkMediaExtractor.h
@@ -162,12 +162,49 @@
AMediaCodecCryptoInfo *AMediaExtractor_getSampleCryptoInfo(AMediaExtractor *);
-
enum {
AMEDIAEXTRACTOR_SAMPLE_FLAG_SYNC = 1,
AMEDIAEXTRACTOR_SAMPLE_FLAG_ENCRYPTED = 2,
};
+#if __ANDROID_API__ >= 28
+
+/**
+ * Returns the format of the extractor. The caller must free the returned format
+ * using AMediaFormat_delete(format).
+ *
+ * This function will always return a format; however, the format could be empty
+ * (no key-value pairs) if the media container does not provide format information.
+ */
+AMediaFormat* AMediaExtractor_getFileFormat(AMediaExtractor*);
+
+/**
+ * Returns the size of the current sample in bytes, or -1 when no samples are
+ * available (end of stream). This API can be used in in conjunction with
+ * AMediaExtractor_readSampleData:
+ *
+ * ssize_t sampleSize = AMediaExtractor_getSampleSize(ex);
+ * uint8_t *buf = new uint8_t[sampleSize];
+ * AMediaExtractor_readSampleData(ex, buf, sampleSize);
+ *
+ */
+ssize_t AMediaExtractor_getSampleSize(AMediaExtractor*);
+
+/**
+ * Returns the duration of cached media samples downloaded from a network data source
+ * (AMediaExtractor_setDataSource with a "http(s)" URI) in microseconds.
+ *
+ * This information is calculated using total bitrate; if total bitrate is not in the
+ * media container it is calculated using total duration and file size.
+ *
+ * Returns -1 when the extractor is not reading from a network data source, or when the
+ * cached duration cannot be calculated (bitrate, duration, and file size information
+ * not available).
+ */
+int64_t AMediaExtractor_getCachedDuration(AMediaExtractor *);
+
+#endif /* __ANDROID_API__ >= 28 */
+
#endif /* __ANDROID_API__ >= 21 */
__END_DECLS
diff --git a/media/ndk/libmediandk.map.txt b/media/ndk/libmediandk.map.txt
index 613cc63..37c557a 100644
--- a/media/ndk/libmediandk.map.txt
+++ b/media/ndk/libmediandk.map.txt
@@ -153,9 +153,12 @@
AMediaDrm_verify;
AMediaExtractor_advance;
AMediaExtractor_delete;
+ AMediaExtractor_getCachedDuration; # introduced=28
+ AMediaExtractor_getFileFormat; # introduced=28
AMediaExtractor_getPsshInfo;
AMediaExtractor_getSampleCryptoInfo;
AMediaExtractor_getSampleFlags;
+ AMediaExtractor_getSampleSize; # introduced=28
AMediaExtractor_getSampleTime;
AMediaExtractor_getSampleTrackIndex;
AMediaExtractor_getTrackCount;
diff --git a/packages/MediaComponents/Android.mk b/packages/MediaComponents/Android.mk
index 61d73a0..7c7718e 100644
--- a/packages/MediaComponents/Android.mk
+++ b/packages/MediaComponents/Android.mk
@@ -27,7 +27,10 @@
# TODO: Use System SDK once public APIs are approved
# LOCAL_SDK_VERSION := system_current
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SRC_FILES := \
+ $(call all-java-files-under, src) \
+ $(call all-Iaidl-files-under, src)
+
LOCAL_PROGUARD_FLAG_FILES := proguard.cfg
LOCAL_MULTILIB := first
diff --git a/packages/MediaComponents/res/layout/media_controller.xml b/packages/MediaComponents/res/layout/media_controller.xml
index 4d05546..74a8306 100644
--- a/packages/MediaComponents/res/layout/media_controller.xml
+++ b/packages/MediaComponents/res/layout/media_controller.xml
@@ -136,7 +136,13 @@
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:visibility="gone"
- android:orientation="horizontal" >
+ android:orientation="horizontal"
+ android:gravity="center">
+
+ <LinearLayout
+ android:id="@+id/custom_buttons"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
<ImageButton
android:id="@+id/mute"
diff --git a/packages/MediaComponents/src/com/android/media/IMediaSession2.aidl b/packages/MediaComponents/src/com/android/media/IMediaSession2.aidl
new file mode 100644
index 0000000..fc62a68
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/media/IMediaSession2.aidl
@@ -0,0 +1,55 @@
+/*
+ * 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.media;
+
+import android.os.Bundle;
+import android.os.ResultReceiver;
+
+import com.android.media.IMediaSession2Callback;
+
+/**
+ * Interface to MediaSession2.
+ * <p>
+ * Keep this interface oneway. Otherwise a malicious app may implement fake version of this,
+ * and holds calls from session to make session owner(s) frozen.
+ */
+interface IMediaSession2 {
+ // TODO(jaewan): add onCommand() to send private command
+ // TODO(jaewan): Due to the nature of oneway calls, APIs can be called in out of order
+ // Add id for individual calls to address this.
+
+ // TODO(jaewan): We may consider to add another binder just for the connection
+ // not to expose other methods to the controller whose connection wasn't accepted.
+ // But this would be enough for now because it's the same as existing
+ // MediaBrowser and MediaBrowserService.
+ void connect(String callingPackage, IMediaSession2Callback callback);
+ void release(IMediaSession2Callback caller);
+
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ // send command
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ void sendCommand(IMediaSession2Callback caller, in Bundle command, in Bundle args);
+ void sendTransportControlCommand(IMediaSession2Callback caller,
+ int commandCode, in Bundle args);
+ void sendCustomCommand(IMediaSession2Callback caller, in Bundle command, in Bundle args,
+ in ResultReceiver receiver);
+
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ // Get library service specific
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ void getBrowserRoot(IMediaSession2Callback callback, in Bundle rootHints);
+}
diff --git a/packages/MediaComponents/src/com/android/media/IMediaSession2Callback.aidl b/packages/MediaComponents/src/com/android/media/IMediaSession2Callback.aidl
new file mode 100644
index 0000000..aabbc69
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/media/IMediaSession2Callback.aidl
@@ -0,0 +1,57 @@
+/*
+ * 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.media;
+
+import android.os.Bundle;
+import android.os.ResultReceiver;
+
+import com.android.media.IMediaSession2;
+
+/**
+ * Interface from MediaSession2 to MediaSession2Record.
+ * <p>
+ * Keep this interface oneway. Otherwise a malicious app may implement fake version of this,
+ * and holds calls from session to make session owner(s) frozen.
+ */
+oneway interface IMediaSession2Callback {
+ void onPlaybackStateChanged(in Bundle state);
+ void onPlaylistChanged(in List<Bundle> playlist);
+ void onPlaylistParamsChanged(in Bundle params);
+
+ /**
+ * Called only when the controller is created with service's token.
+ *
+ * @param sessionBinder {@code null} if the connect is rejected or is disconnected. a session
+ * binder if the connect is accepted.
+ * @param commands initially allowed commands.
+ */
+ // TODO(jaewan): Also need to pass flags for allowed actions for permission check.
+ // For example, a media can allow setRating only for whitelisted apps
+ // it's better for controller to know such information in advance.
+ // Follow-up TODO: Add similar functions to the session.
+ // TODO(jaewan): Is term 'accepted/rejected' correct? For permission, 'grant' is used.
+ void onConnectionChanged(IMediaSession2 sessionBinder, in Bundle commandGroup);
+
+ void onCustomLayoutChanged(in List<Bundle> commandButtonlist);
+
+ void sendCustomCommand(in Bundle command, in Bundle args, in ResultReceiver receiver);
+
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ // Browser sepcific
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ void onGetRootResult(in Bundle rootHints, String rootMediaId, in Bundle rootExtra);
+}
diff --git a/packages/MediaComponents/src/com/android/media/MediaBrowser2Impl.java b/packages/MediaComponents/src/com/android/media/MediaBrowser2Impl.java
index 511eed1..d21edae 100644
--- a/packages/MediaComponents/src/com/android/media/MediaBrowser2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaBrowser2Impl.java
@@ -17,7 +17,6 @@
package com.android.media;
import android.content.Context;
-import android.media.IMediaSession2;
import android.media.MediaBrowser2;
import android.media.MediaBrowser2.BrowserCallback;
import android.media.MediaSession2.CommandButton;
diff --git a/packages/MediaComponents/src/com/android/media/MediaController2Impl.java b/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
index 43f8473..30e32ec 100644
--- a/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
@@ -21,8 +21,6 @@
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
-import android.media.IMediaSession2;
-import android.media.IMediaSession2Callback;
import android.media.MediaController2.PlaybackInfo;
import android.media.MediaItem2;
import android.media.MediaSession2;
@@ -56,13 +54,6 @@
private final MediaController2 mInstance;
- /**
- * Flag used by MediaController2Record to filter playback callback.
- */
- static final int CALLBACK_FLAG_PLAYBACK = 0x1;
-
- static final int REQUEST_CODE_ALL = 0;
-
private final Object mLock = new Object();
private final Context mContext;
@@ -73,11 +64,15 @@
private final IBinder.DeathRecipient mDeathRecipient;
@GuardedBy("mLock")
- private final List<PlaybackListenerHolder> mPlaybackListeners = new ArrayList<>();
- @GuardedBy("mLock")
private SessionServiceConnection mServiceConnection;
@GuardedBy("mLock")
private boolean mIsReleased;
+ @GuardedBy("mLock")
+ private PlaybackState2 mPlaybackState;
+ @GuardedBy("mLock")
+ private List<MediaItem2> mPlaylist;
+ @GuardedBy("mLock")
+ private PlaylistParams mPlaylistParams;
// Assignment should be used with the lock hold, but should be used without a lock to prevent
// potential deadlock.
@@ -92,7 +87,6 @@
public MediaController2Impl(Context context, MediaController2 instance, SessionToken2 token,
Executor executor, ControllerCallback callback) {
mInstance = instance;
-
if (context == null) {
throw new IllegalArgumentException("context shouldn't be null");
}
@@ -115,21 +109,28 @@
};
mSessionBinder = null;
+ }
- if (token.getSessionBinder() == null) {
+ @Override
+ public void initialize() {
+ SessionToken2Impl impl = SessionToken2Impl.from(mToken);
+ // TODO(jaewan): More sanity checks.
+ if (impl.getSessionBinder() == null) {
+ // Session service
mServiceConnection = new SessionServiceConnection();
connectToService();
} else {
+ // Session
mServiceConnection = null;
- connectToSession(token.getSessionBinder());
+ connectToSession(impl.getSessionBinder());
}
}
- // Should be only called by constructor.
private void connectToService() {
// Service. Needs to get fresh binder whenever connection is needed.
+ SessionToken2Impl impl = SessionToken2Impl.from(mToken);
final Intent intent = new Intent(MediaSessionService2.SERVICE_INTERFACE);
- intent.setClassName(mToken.getPackageName(), mToken.getServiceName());
+ intent.setClassName(mToken.getPackageName(), impl.getServiceName());
// Use bindService() instead of startForegroundService() to start session service for three
// reasons.
@@ -166,7 +167,7 @@
@Override
public void close_impl() {
if (DEBUG) {
- Log.d(TAG, "relese from " + mToken);
+ Log.d(TAG, "release from " + mToken);
}
final IMediaSession2 binder;
synchronized (mLock) {
@@ -179,7 +180,6 @@
mContext.unbindService(mServiceConnection);
mServiceConnection = null;
}
- mPlaybackListeners.clear();
binder = mSessionBinder;
mSessionBinder = null;
mSessionCallbackStub.destroy();
@@ -222,38 +222,38 @@
@Override
public void play_impl() {
- sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_START);
+ sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_START);
}
@Override
public void pause_impl() {
- sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE);
+ sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE);
}
@Override
public void stop_impl() {
- sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_STOP);
+ sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_STOP);
}
@Override
public void skipToPrevious_impl() {
- sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM);
+ sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM);
}
@Override
public void skipToNext_impl() {
- sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM);
+ sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM);
}
- private void sendCommand(int code) {
- // TODO(jaewan): optimization) Cache Command objects?
- Command command = new Command(code);
- // TODO(jaewan): Check if the command is in the allowed group.
+ private void sendTransportControlCommand(int commandCode) {
+ sendTransportControlCommand(commandCode, null);
+ }
+ private void sendTransportControlCommand(int commandCode, Bundle args) {
final IMediaSession2 binder = mSessionBinder;
if (binder != null) {
try {
- binder.sendCommand(mSessionCallbackStub, command.toBundle(), null);
+ binder.sendTransportControlCommand(mSessionCallbackStub, commandCode, args);
} catch (RemoteException e) {
Log.w(TAG, "Cannot connect to the service or the session is gone", e);
}
@@ -330,44 +330,64 @@
@Override
public void sendCustomCommand_impl(Command command, Bundle args, ResultReceiver cb) {
- // TODO(jaewan): Implement
+ if (command == null) {
+ throw new IllegalArgumentException("command shouldn't be null");
+ }
+ // TODO(jaewan): Also check if the command is allowed.
+ final IMediaSession2 binder = mSessionBinder;
+ if (binder != null) {
+ try {
+ binder.sendCustomCommand(mSessionCallbackStub, command.toBundle(), args, cb);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Cannot connect to the service or the session is gone", e);
+ }
+ } else {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ }
}
@Override
public List<MediaItem2> getPlaylist_impl() {
- // TODO(jaewan): Implement
- return null;
+ synchronized (mLock) {
+ return mPlaylist;
+ }
}
@Override
public void prepare_impl() {
- // TODO(jaewan): Implement
+ sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_PREPARE);
}
@Override
public void fastForward_impl() {
- // TODO(jaewan): Implement
+ sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_FAST_FORWARD);
}
@Override
public void rewind_impl() {
- // TODO(jaewan): Implement
+ sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_REWIND);
}
@Override
public void seekTo_impl(long pos) {
- // TODO(jaewan): Implement
+ Bundle args = new Bundle();
+ args.putLong(MediaSession2Stub.ARGUMENT_KEY_POSITION, pos);
+ sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SEEK_TO, args);
}
@Override
public void setCurrentPlaylistItem_impl(int index) {
- // TODO(jaewan): Implement
+ Bundle args = new Bundle();
+ args.putInt(MediaSession2Stub.ARGUMENT_KEY_ITEM_INDEX, index);
+ sendTransportControlCommand(
+ MediaSession2.COMMAND_CODE_PLAYBACK_SET_CURRENT_PLAYLIST_ITEM, args);
}
@Override
public PlaybackState2 getPlaybackState_impl() {
- // TODO(jaewan): Implement
- return null;
+ synchronized (mLock) {
+ return mPlaybackState;
+ }
}
@Override
@@ -381,32 +401,66 @@
}
@Override
- public PlaylistParams getPlaylistParam_impl() {
- // TODO(jaewan): Implement
- return null;
+ public PlaylistParams getPlaylistParams_impl() {
+ synchronized (mLock) {
+ return mPlaylistParams;
+ }
+ }
+
+ @Override
+ public void setPlaylistParams_impl(PlaylistParams params) {
+ if (params == null) {
+ throw new IllegalArgumentException("PlaylistParams should not be null!");
+ }
+ Bundle args = new Bundle();
+ args.putBundle(MediaSession2Stub.ARGUMENT_KEY_PLAYLIST_PARAMS, params.toBundle());
+ sendTransportControlCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SET_PLAYLIST_PARAMS, args);
}
///////////////////////////////////////////////////
// Protected or private methods
///////////////////////////////////////////////////
- // Should be used without a lock to prevent potential deadlock.
- private void registerCallbackForPlaybackNotLocked() {
- final IMediaSession2 binder = mSessionBinder;
- if (binder != null) {
- try {
- binder.registerCallback(mSessionCallbackStub,
- CALLBACK_FLAG_PLAYBACK, REQUEST_CODE_ALL);
- } catch (RemoteException e) {
- Log.e(TAG, "Cannot connect to the service or the session is gone", e);
- }
- }
- }
-
private void pushPlaybackStateChanges(final PlaybackState2 state) {
synchronized (mLock) {
- for (int i = 0; i < mPlaybackListeners.size(); i++) {
- mPlaybackListeners.get(i).postPlaybackChange(state);
+ mPlaybackState = state;
+ }
+ mCallbackExecutor.execute(() -> {
+ if (!mInstance.isConnected()) {
+ return;
}
+ mCallback.onPlaybackStateChanged(state);
+ });
+ }
+
+ private void pushPlaylistParamsChanges(final PlaylistParams params) {
+ synchronized (mLock) {
+ mPlaylistParams = params;
+ }
+ mCallbackExecutor.execute(() -> {
+ if (!mInstance.isConnected()) {
+ return;
+ }
+ mCallback.onPlaylistParamsChanged(params);
+ });
+ }
+
+ private void pushPlaylistChanges(final List<Bundle> list) {
+ final List<MediaItem2> playlist = new ArrayList<>();
+ for (int i = 0; i < list.size(); i++) {
+ MediaItem2 item = MediaItem2.fromBundle(mContext, list.get(i));
+ if (item != null) {
+ playlist.add(item);
+ }
+ }
+
+ synchronized (mLock) {
+ mPlaylist = playlist;
+ mCallbackExecutor.execute(() -> {
+ if (!mInstance.isConnected()) {
+ return;
+ }
+ mCallback.onPlaylistChanged(playlist);
+ });
}
}
@@ -425,7 +479,6 @@
release = true;
return;
}
- boolean registerCallbackForPlaybackNeeded;
synchronized (mLock) {
if (mIsReleased) {
return;
@@ -448,15 +501,11 @@
release = true;
return;
}
- registerCallbackForPlaybackNeeded = !mPlaybackListeners.isEmpty();
}
// TODO(jaewan): Keep commands to prevents illegal API calls.
mCallbackExecutor.execute(() -> {
mCallback.onConnected(commandGroup);
});
- if (registerCallbackForPlaybackNeeded) {
- registerCallbackForPlaybackNotLocked();
- }
} finally {
if (release) {
// Trick to call release() without holding the lock, to prevent potential deadlock
@@ -466,6 +515,17 @@
}
}
+ private void onCustomCommand(final Command command, final Bundle args,
+ final ResultReceiver receiver) {
+ if (DEBUG) {
+ Log.d(TAG, "onCustomCommand cmd=" + command);
+ }
+ mCallbackExecutor.execute(() -> {
+ // TODO(jaewan): Double check if the controller exists.
+ mCallback.onCustomCommand(command, args, receiver);
+ });
+ }
+
// TODO(jaewan): Pull out this from the controller2, and rename it to the MediaController2Stub
// or MediaBrowser2Stub.
static class MediaSession2CallbackStub extends IMediaSession2Callback.Stub {
@@ -498,11 +558,44 @@
@Override
public void onPlaybackStateChanged(Bundle state) throws RuntimeException {
- final MediaController2Impl controller = getController();
+ final MediaController2Impl controller;
+ try {
+ controller = getController();
+ } catch (IllegalStateException e) {
+ Log.w(TAG, "Don't fail silently here. Highly likely a bug");
+ return;
+ }
controller.pushPlaybackStateChanges(PlaybackState2.fromBundle(state));
}
@Override
+ public void onPlaylistChanged(List<Bundle> playlist) throws RuntimeException {
+ final MediaController2Impl controller;
+ try {
+ controller = getController();
+ } catch (IllegalStateException e) {
+ Log.w(TAG, "Don't fail silently here. Highly likely a bug");
+ return;
+ }
+ if (playlist == null) {
+ return;
+ }
+ controller.pushPlaylistChanges(playlist);
+ }
+
+ @Override
+ public void onPlaylistParamsChanged(Bundle params) throws RuntimeException {
+ final MediaController2Impl controller;
+ try {
+ controller = getController();
+ } catch (IllegalStateException e) {
+ Log.w(TAG, "Don't fail silently here. Highly likely a bug");
+ return;
+ }
+ controller.pushPlaylistParamsChanges(PlaylistParams.fromBundle(params));
+ }
+
+ @Override
public void onConnectionChanged(IMediaSession2 sessionBinder, Bundle commandGroup)
throws RuntimeException {
final MediaController2Impl controller;
@@ -559,6 +652,22 @@
}
browser.onCustomLayoutChanged(layout);
}
+
+ @Override
+ public void sendCustomCommand(Bundle commandBundle, Bundle args, ResultReceiver receiver) {
+ final MediaController2Impl controller;
+ try {
+ controller = getController();
+ } catch (IllegalStateException e) {
+ Log.w(TAG, "Don't fail silently here. Highly likely a bug");
+ return;
+ }
+ Command command = Command.fromBundle(commandBundle);
+ if (command == null) {
+ return;
+ }
+ controller.onCustomCommand(command, args, receiver);
+ }
}
// This will be called on the main thread.
diff --git a/packages/MediaComponents/src/com/android/media/MediaItem2Impl.java b/packages/MediaComponents/src/com/android/media/MediaItem2Impl.java
new file mode 100644
index 0000000..5639346
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/media/MediaItem2Impl.java
@@ -0,0 +1,163 @@
+/*
+ * 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.media;
+
+import static android.media.MediaItem2.FLAG_BROWSABLE;
+import static android.media.MediaItem2.FLAG_PLAYABLE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.media.DataSourceDesc;
+import android.media.MediaItem2;
+import android.media.MediaItem2.Flags;
+import android.media.MediaMetadata2;
+import android.media.update.MediaItem2Provider;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+public class MediaItem2Impl implements MediaItem2Provider {
+ private static final String KEY_ID = "android.media.mediaitem2.id";
+ private static final String KEY_FLAGS = "android.media.mediaitem2.flags";
+ private static final String KEY_METADATA = "android.media.mediaitem2.metadata";
+
+ private final Context mContext;
+ private final MediaItem2 mInstance;
+ private final String mId;
+ private final int mFlags;
+ private MediaMetadata2 mMetadata;
+ private DataSourceDesc mDataSourceDesc;
+
+ // From the public API
+ public MediaItem2Impl(Context context, MediaItem2 instance, String mediaId,
+ DataSourceDesc dsd, MediaMetadata2 metadata, @Flags int flags) {
+ if (mediaId == null) {
+ throw new IllegalArgumentException("mediaId shouldn't be null");
+ }
+ if (dsd == null) {
+ throw new IllegalArgumentException("dsd shouldn't be null");
+ }
+ if (metadata != null && !TextUtils.equals(mediaId, metadata.getMediaId())) {
+ throw new IllegalArgumentException("metadata's id should be match with the mediaid");
+ }
+
+ mContext = context;
+ mInstance = instance;
+
+ mId = mediaId;
+ mDataSourceDesc = dsd;
+ mMetadata = metadata;
+ mFlags = flags;
+ }
+
+ // Create anonymized version
+ public MediaItem2Impl(Context context, String mediaId, MediaMetadata2 metadata,
+ @Flags int flags) {
+ if (mediaId == null) {
+ throw new IllegalArgumentException("mediaId shouldn't be null");
+ }
+ if (metadata != null && !TextUtils.equals(mediaId, metadata.getMediaId())) {
+ throw new IllegalArgumentException("metadata's id should be match with the mediaid");
+ }
+ mContext =context;
+ mId = mediaId;
+ mMetadata = metadata;
+ mFlags = flags;
+ mInstance = new MediaItem2(this);
+ }
+
+ /**
+ * Return this object as a bundle to share between processes.
+ *
+ * @return a new bundle instance
+ */
+ public Bundle toBundle_impl() {
+ Bundle bundle = new Bundle();
+ bundle.putString(KEY_ID, mId);
+ bundle.putInt(KEY_FLAGS, mFlags);
+ if (mMetadata != null) {
+ bundle.putBundle(KEY_METADATA, mMetadata.getBundle());
+ }
+ return bundle;
+ }
+
+ public static MediaItem2 fromBundle(Context context, Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+ final String id = bundle.getString(KEY_ID);
+ final Bundle metadataBundle = bundle.getBundle(KEY_METADATA);
+ final MediaMetadata2 metadata = metadataBundle != null
+ ? new MediaMetadata2(metadataBundle) : null;
+ final int flags = bundle.getInt(KEY_FLAGS);
+ return new MediaItem2Impl(context, id, metadata, flags).getInstance();
+ }
+
+ private MediaItem2 getInstance() {
+ return mInstance;
+ }
+
+ @Override
+ public String toString_impl() {
+ final StringBuilder sb = new StringBuilder("MediaItem2{");
+ sb.append("mFlags=").append(mFlags);
+ sb.append(", mMetadata=").append(mMetadata);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
+ public @Flags int getFlags_impl() {
+ return mFlags;
+ }
+
+ @Override
+ public boolean isBrowsable_impl() {
+ return (mFlags & FLAG_BROWSABLE) != 0;
+ }
+
+ @Override
+ public boolean isPlayable_impl() {
+ return (mFlags & FLAG_PLAYABLE) != 0;
+ }
+
+ @Override
+ public void setMetadata_impl(@NonNull MediaMetadata2 metadata) {
+ if (metadata == null) {
+ throw new IllegalArgumentException("metadata shouldn't be null");
+ }
+ if (TextUtils.isEmpty(metadata.getMediaId())) {
+ throw new IllegalArgumentException("metadata must have a non-empty media id");
+ }
+ mMetadata = metadata;
+ }
+
+ @Override
+ public MediaMetadata2 getMetadata_impl() {
+ return mMetadata;
+ }
+
+ @Override
+ public @Nullable String getMediaId_impl() {
+ return mMetadata.getMediaId();
+ }
+
+ @Override
+ public @Nullable DataSourceDesc getDataSourceDesc_impl() {
+ return mDataSourceDesc;
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/media/MediaLibraryService2Impl.java b/packages/MediaComponents/src/com/android/media/MediaLibraryService2Impl.java
index bbb3411..4b1b9de 100644
--- a/packages/MediaComponents/src/com/android/media/MediaLibraryService2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaLibraryService2Impl.java
@@ -18,15 +18,14 @@
import android.app.PendingIntent;
import android.content.Context;
-import android.content.Intent;
import android.media.MediaLibraryService2;
import android.media.MediaLibraryService2.MediaLibrarySession;
import android.media.MediaLibraryService2.MediaLibrarySessionCallback;
-import android.media.MediaPlayerBase;
+import android.media.MediaPlayerInterface;
import android.media.MediaSession2;
import android.media.MediaSession2.ControllerInfo;
-import android.media.MediaSession2.SessionCallback;
import android.media.MediaSessionService2;
+import android.media.SessionToken2;
import android.media.VolumeProvider;
import android.media.update.MediaLibraryService2Provider;
import android.os.Bundle;
@@ -56,10 +55,8 @@
}
@Override
- Intent createServiceIntent() {
- Intent serviceIntent = new Intent(mInstance, mInstance.getClass());
- serviceIntent.setAction(MediaLibraryService2.SERVICE_INTERFACE);
- return serviceIntent;
+ int getSessionType() {
+ return SessionToken2.TYPE_LIBRARY_SERVICE;
}
public static class MediaLibrarySessionImpl extends MediaSession2Impl
@@ -68,8 +65,8 @@
private final MediaLibrarySessionCallback mCallback;
public MediaLibrarySessionImpl(Context context, MediaLibrarySession instance,
- MediaPlayerBase player, String id, VolumeProvider volumeProvider, int ratingType,
- PendingIntent sessionActivity, Executor callbackExecutor,
+ MediaPlayerInterface player, String id, VolumeProvider volumeProvider,
+ int ratingType, PendingIntent sessionActivity, Executor callbackExecutor,
MediaLibrarySessionCallback callback) {
super(context, instance, player, id, volumeProvider, ratingType, sessionActivity,
callbackExecutor, callback);
diff --git a/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java b/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java
index 22a3187..7c36739 100644
--- a/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java
@@ -16,15 +16,21 @@
package com.android.media;
+import static android.media.SessionToken2.TYPE_LIBRARY_SERVICE;
+import static android.media.SessionToken2.TYPE_SESSION;
+import static android.media.SessionToken2.TYPE_SESSION_SERVICE;
+
import android.Manifest.permission;
import android.app.PendingIntent;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
-import android.media.AudioAttributes;
-import android.media.IMediaSession2Callback;
+import android.content.pm.ResolveInfo;
import android.media.MediaItem2;
-import android.media.MediaPlayerBase;
+import android.media.MediaLibraryService2;
+import android.media.MediaPlayerInterface;
+import android.media.MediaPlayerInterface.PlaybackListener;
import android.media.MediaSession2;
import android.media.MediaSession2.Builder;
import android.media.MediaSession2.Command;
@@ -33,17 +39,20 @@
import android.media.MediaSession2.ControllerInfo;
import android.media.MediaSession2.PlaylistParams;
import android.media.MediaSession2.SessionCallback;
+import android.media.MediaSessionService2;
import android.media.PlaybackState2;
import android.media.SessionToken2;
import android.media.VolumeProvider;
import android.media.session.MediaSessionManager;
import android.media.update.MediaSession2Provider;
import android.os.Bundle;
-import android.os.Handler;
+import android.os.Process;
import android.os.IBinder;
-import android.os.Looper;
import android.os.ResultReceiver;
+import android.support.annotation.GuardedBy;
+import android.text.TextUtils;
import android.util.Log;
+
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
@@ -53,24 +62,29 @@
private static final String TAG = "MediaSession2";
private static final boolean DEBUG = true;//Log.isLoggable(TAG, Log.DEBUG);
- private final MediaSession2 mInstance;
+ private final Object mLock = new Object();
+ private final MediaSession2 mInstance;
private final Context mContext;
private final String mId;
- private final Handler mHandler;
private final Executor mCallbackExecutor;
+ private final SessionCallback mCallback;
private final MediaSession2Stub mSessionStub;
private final SessionToken2 mSessionToken;
-
- private MediaPlayerBase mPlayer;
-
private final List<PlaybackListenerHolder> mListeners = new ArrayList<>();
+
+ @GuardedBy("mLock")
+ private MediaPlayerInterface mPlayer;
+ @GuardedBy("mLock")
private MyPlaybackListener mListener;
- private MediaSession2 instance;
+ @GuardedBy("mLock")
+ private PlaylistParams mPlaylistParams;
+ @GuardedBy("mLock")
+ private List<MediaItem2> mPlaylist;
/**
* Can be only called by the {@link Builder#build()}.
- *
+ *
* @param instance
* @param context
* @param player
@@ -80,7 +94,7 @@
* @param ratingType
* @param sessionActivity
*/
- public MediaSession2Impl(Context context, MediaSession2 instance, MediaPlayerBase player,
+ public MediaSession2Impl(Context context, MediaSession2 instance, MediaPlayerInterface player,
String id, VolumeProvider volumeProvider, int ratingType, PendingIntent sessionActivity,
Executor callbackExecutor, SessionCallback callback) {
mInstance = instance;
@@ -90,63 +104,109 @@
// Initialize finals first.
mContext = context;
mId = id;
- mHandler = new Handler(Looper.myLooper());
+ mCallback = callback;
mCallbackExecutor = callbackExecutor;
- mSessionStub = new MediaSession2Stub(this, callback);
- // Ask server to create session token for following reasons.
- // 1. Make session ID unique per package.
- // Server can only know if the package has another process and has another session
- // with the same id. Let server check this.
- // Note that 'ID is unique per package' is important for controller to distinguish
- // a session in another package.
- // 2. Easier to know the type of session.
- // Session created here can be the session service token. In order distinguish,
- // we need to iterate AndroidManifest.xml but it's already done by the server.
- // Let server to create token with the type.
+ mSessionStub = new MediaSession2Stub(this);
+
+ // Infer type from the id and package name.
+ String libraryService = getServiceName(context, MediaLibraryService2.SERVICE_INTERFACE, id);
+ String sessionService = getServiceName(context, MediaSessionService2.SERVICE_INTERFACE, id);
+ if (sessionService != null && libraryService != null) {
+ throw new IllegalArgumentException("Ambiguous session type. Multiple"
+ + " session services define the same id=" + id);
+ } else if (libraryService != null) {
+ mSessionToken = new SessionToken2Impl(context, Process.myUid(), TYPE_LIBRARY_SERVICE,
+ mContext.getPackageName(), libraryService, id, mSessionStub).getInstance();
+ } else if (sessionService != null) {
+ mSessionToken = new SessionToken2Impl(context, Process.myUid(), TYPE_SESSION_SERVICE,
+ mContext.getPackageName(), sessionService, id, mSessionStub).getInstance();
+ } else {
+ mSessionToken = new SessionToken2Impl(context, Process.myUid(), TYPE_SESSION,
+ mContext.getPackageName(), null, id, mSessionStub).getInstance();
+ }
+
+ // Only remember player. Actual settings will be done in the initialize().
+ mPlayer = player;
+ }
+
+ private static String getServiceName(Context context, String serviceAction, String id) {
+ PackageManager manager = context.getPackageManager();
+ Intent serviceIntent = new Intent(serviceAction);
+ serviceIntent.setPackage(context.getPackageName());
+ List<ResolveInfo> services = manager.queryIntentServices(serviceIntent,
+ PackageManager.GET_META_DATA);
+ String serviceName = null;
+ if (services != null) {
+ for (int i = 0; i < services.size(); i++) {
+ String serviceId = SessionToken2Impl.getSessionId(services.get(i));
+ if (serviceId != null && TextUtils.equals(id, serviceId)) {
+ if (services.get(i).serviceInfo == null) {
+ continue;
+ }
+ if (serviceName != null) {
+ throw new IllegalArgumentException("Ambiguous session type. Multiple"
+ + " session services define the same id=" + id);
+ }
+ serviceName = services.get(i).serviceInfo.name;
+ }
+ }
+ }
+ return serviceName;
+ }
+
+ @Override
+ public void initialize() {
+ synchronized (mLock) {
+ setPlayerLocked(mPlayer);
+ }
+ // Ask server for the sanity check, and starts
+ // Sanity check for making session ID unique 'per package' cannot be done in here.
+ // Server can only know if the package has another process and has another session with the
+ // same id. Note that 'ID is unique per package' is important for controller to distinguish
+ // a session in another package.
MediaSessionManager manager =
(MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
- mSessionToken = manager.createSessionToken(mContext.getPackageName(), mId, mSessionStub);
- if (mSessionToken == null) {
+ if (!manager.onSessionCreated(mSessionToken)) {
throw new IllegalStateException("Session with the same id is already used by"
+ " another process. Use MediaController2 instead.");
}
-
- setPlayerInternal(player);
}
// TODO(jaewan): Add explicit release() and do not remove session object with the
// setPlayer(null). Token can be available when player is null, and
// controller can also attach to session.
@Override
- public void setPlayer_impl(MediaPlayerBase player, VolumeProvider volumeProvider) throws IllegalArgumentException {
+ public void setPlayer_impl(MediaPlayerInterface player, VolumeProvider volumeProvider)
+ throws IllegalArgumentException {
ensureCallingThread();
if (player == null) {
throw new IllegalArgumentException("player shouldn't be null");
}
- setPlayerInternal(player);
- }
-
- private void setPlayerInternal(MediaPlayerBase player) {
- if (mPlayer == player) {
- // Player didn't changed. No-op.
+ if (player == mPlayer) {
return;
}
- // TODO(jaewan): Find equivalent for the executor
- //mHandler.removeCallbacksAndMessages(null);
+ synchronized (mLock) {
+ setPlayerLocked(player);
+ }
+ }
+
+ private void setPlayerLocked(MediaPlayerInterface player) {
if (mPlayer != null && mListener != null) {
// This might not work for a poorly implemented player.
mPlayer.removePlaybackListener(mListener);
}
+ mPlayer = player;
mListener = new MyPlaybackListener(this, player);
player.addPlaybackListener(mCallbackExecutor, mListener);
- notifyPlaybackStateChanged(player.getPlaybackState());
- mPlayer = player;
}
@Override
public void close_impl() {
- // Flush any pending messages.
- mHandler.removeCallbacksAndMessages(null);
+ // Stop system service from listening this session first.
+ MediaSessionManager manager =
+ (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
+ manager.onSessionDestroyed(mSessionToken);
+
if (mSessionStub != null) {
if (DEBUG) {
Log.d(TAG, "session is now unavailable, id=" + mId);
@@ -154,10 +214,17 @@
// Invalidate previously published session stub.
mSessionStub.destroyNotLocked();
}
+ synchronized (mLock) {
+ if (mPlayer != null) {
+ // close can be called multiple times
+ mPlayer.removePlaybackListener(mListener);
+ mPlayer = null;
+ }
+ }
}
@Override
- public MediaPlayerBase getPlayer_impl() {
+ public MediaPlayerInterface getPlayer_impl() {
return getPlayer();
}
@@ -173,11 +240,6 @@
}
@Override
- public void setAudioAttributes_impl(AudioAttributes attributes) {
- // implement
- }
-
- @Override
public void setAudioFocusRequest_impl(int focusGain) {
// implement
}
@@ -229,11 +291,31 @@
mSessionStub.notifyCustomLayoutNotLocked(controller, layout);
}
+ @Override
+ public void setPlaylistParams_impl(PlaylistParams params) {
+ if (params == null) {
+ throw new IllegalArgumentException("PlaylistParams should not be null!");
+ }
+ ensureCallingThread();
+ ensurePlayer();
+ synchronized (mLock) {
+ mPlaylistParams = params;
+ }
+ mPlayer.setPlaylistParams(params);
+ mSessionStub.notifyPlaylistParamsChanged(params);
+ }
+
+ @Override
+ public PlaylistParams getPlaylistParams_impl() {
+ // TODO: Do we need to synchronize here for preparing Controller2.setPlaybackParams?
+ return mPlaylistParams;
+ }
+
//////////////////////////////////////////////////////////////////////////////////////
// TODO(jaewan): Implement follows
//////////////////////////////////////////////////////////////////////////////////////
@Override
- public void setPlayer_impl(MediaPlayerBase player) {
+ public void setPlayer_impl(MediaPlayerInterface player) {
// TODO(jaewan): Implement
}
@@ -250,42 +332,106 @@
@Override
public void sendCustomCommand_impl(ControllerInfo controller, Command command, Bundle args,
ResultReceiver receiver) {
- // TODO(jaewan): Implement
+ mSessionStub.sendCustomCommand(controller, command, args, receiver);
}
@Override
public void sendCustomCommand_impl(Command command, Bundle args) {
- // TODO(jaewan): Implement
+ mSessionStub.sendCustomCommand(command, args);
}
@Override
- public void setPlaylist_impl(List<MediaItem2> playlist, PlaylistParams param) {
- // TODO(jaewan): Implement
+ public void setPlaylist_impl(List<MediaItem2> playlist) {
+ if (playlist == null) {
+ throw new IllegalArgumentException("Playlist should not be null!");
+ }
+ ensureCallingThread();
+ ensurePlayer();
+ synchronized (mLock) {
+ mPlaylist = playlist;
+ }
+ mPlayer.setPlaylist(playlist);
+ mSessionStub.notifyPlaylistChanged(playlist);
+ }
+
+ @Override
+ public List<MediaItem2> getPlaylist_impl() {
+ synchronized (mLock) {
+ return mPlaylist;
+ }
}
@Override
public void prepare_impl() {
- // TODO(jaewan): Implement
+ ensureCallingThread();
+ ensurePlayer();
+ mPlayer.prepare();
}
@Override
public void fastForward_impl() {
- // TODO(jaewan): Implement
+ ensureCallingThread();
+ ensurePlayer();
+ mPlayer.fastForward();
}
@Override
public void rewind_impl() {
- // TODO(jaewan): Implement
+ ensureCallingThread();
+ ensurePlayer();
+ mPlayer.rewind();
}
@Override
public void seekTo_impl(long pos) {
- // TODO(jaewan): Implement
+ ensureCallingThread();
+ ensurePlayer();
+ mPlayer.seekTo(pos);
}
@Override
public void setCurrentPlaylistItem_impl(int index) {
- // TODO(jaewan): Implement
+ ensureCallingThread();
+ ensurePlayer();
+ mPlayer.setCurrentPlaylistItem(index);
+ }
+
+ @Override
+ public void addPlaybackListener_impl(Executor executor, PlaybackListener listener) {
+ if (executor == null) {
+ throw new IllegalArgumentException("executor shouldn't be null");
+ }
+ if (listener == null) {
+ throw new IllegalArgumentException("listener shouldn't be null");
+ }
+ ensureCallingThread();
+ if (PlaybackListenerHolder.contains(mListeners, listener)) {
+ Log.w(TAG, "listener is already added. Ignoring.");
+ return;
+ }
+ mListeners.add(new PlaybackListenerHolder(executor, listener));
+ executor.execute(() -> listener.onPlaybackChanged(getInstance().getPlaybackState()));
+ }
+
+ @Override
+ public void removePlaybackListener_impl(PlaybackListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("listener shouldn't be null");
+ }
+ ensureCallingThread();
+ int idx = PlaybackListenerHolder.indexOf(mListeners, listener);
+ if (idx >= 0) {
+ mListeners.remove(idx);
+ }
+ }
+
+ @Override
+ public PlaybackState2 getPlaybackState_impl() {
+ ensureCallingThread();
+ ensurePlayer();
+ // TODO(jaewan): Is it safe to be called on any thread?
+ // Otherwise we should cache the result from listener.
+ return mPlayer.getPlaybackState();
}
///////////////////////////////////////////////////
@@ -312,7 +458,6 @@
}*/
}
-
private void ensurePlayer() {
// TODO(jaewan): Should we pend command instead? Follow the decision from MP2.
// Alternatively we can add a API like setAcceptsPendingCommands(boolean).
@@ -321,14 +466,14 @@
}
}
- Handler getHandler() {
- return mHandler;
- }
-
- private void notifyPlaybackStateChanged(PlaybackState2 state) {
+ private void notifyPlaybackStateChangedNotLocked(PlaybackState2 state) {
+ List<PlaybackListenerHolder> listeners = new ArrayList<>();
+ synchronized (mLock) {
+ listeners.addAll(mListeners);
+ }
// Notify to listeners added directly to this session
- for (int i = 0; i < mListeners.size(); i++) {
- mListeners.get(i).postPlaybackChange(state);
+ for (int i = 0; i < listeners.size(); i++) {
+ listeners.get(i).postPlaybackChange(state);
}
// Notify to controllers as well.
mSessionStub.notifyPlaybackStateChangedNotLocked(state);
@@ -342,15 +487,23 @@
return mInstance;
}
- MediaPlayerBase getPlayer() {
+ MediaPlayerInterface getPlayer() {
return mPlayer;
}
- private static class MyPlaybackListener implements MediaPlayerBase.PlaybackListener {
- private final WeakReference<MediaSession2Impl> mSession;
- private final MediaPlayerBase mPlayer;
+ Executor getCallbackExecutor() {
+ return mCallbackExecutor;
+ }
- private MyPlaybackListener(MediaSession2Impl session, MediaPlayerBase player) {
+ SessionCallback getCallback() {
+ return mCallback;
+ }
+
+ private static class MyPlaybackListener implements MediaPlayerInterface.PlaybackListener {
+ private final WeakReference<MediaSession2Impl> mSession;
+ private final MediaPlayerInterface mPlayer;
+
+ private MyPlaybackListener(MediaSession2Impl session, MediaPlayerInterface player) {
mSession = new WeakReference<>(session);
mPlayer = player;
}
@@ -363,7 +516,7 @@
new IllegalStateException());
return;
}
- session.notifyPlaybackStateChanged(state);
+ session.notifyPlaybackStateChangedNotLocked(state);
}
}
@@ -374,11 +527,6 @@
private final boolean mIsTrusted;
private final IMediaSession2Callback mControllerBinder;
- // Flag to indicate which callbacks should be returned for the controller binder.
- // Either 0 or combination of {@link #CALLBACK_FLAG_PLAYBACK},
- // {@link #CALLBACK_FLAG_SESSION_ACTIVENESS}
- private int mFlag;
-
public ControllerInfoImpl(Context context, ControllerInfo instance, int uid,
int pid, String packageName, IMediaSession2Callback callback) {
mInstance = instance;
@@ -462,18 +610,6 @@
return mControllerBinder;
}
- public boolean containsFlag(int flag) {
- return (mFlag & flag) != 0;
- }
-
- public void addFlag(int flag) {
- mFlag |= flag;
- }
-
- public void removeFlag(int flag) {
- mFlag &= ~flag;
- }
-
public static ControllerInfoImpl from(ControllerInfo controller) {
return (ControllerInfoImpl) controller.getProvider();
}
diff --git a/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java b/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java
index 2f75dfa..4fc69b9 100644
--- a/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java
+++ b/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java
@@ -16,11 +16,7 @@
package com.android.media;
-import static com.android.media.MediaController2Impl.CALLBACK_FLAG_PLAYBACK;
-
-import android.content.Context;
-import android.media.IMediaSession2;
-import android.media.IMediaSession2Callback;
+import android.media.MediaItem2;
import android.media.MediaLibraryService2.BrowserRoot;
import android.media.MediaLibraryService2.MediaLibrarySessionCallback;
import android.media.MediaSession2;
@@ -28,53 +24,47 @@
import android.media.MediaSession2.CommandButton;
import android.media.MediaSession2.CommandGroup;
import android.media.MediaSession2.ControllerInfo;
-import android.media.MediaSession2.SessionCallback;
+import android.media.MediaSession2.PlaylistParams;
import android.media.PlaybackState2;
-import android.media.session.PlaybackState;
import android.os.Binder;
import android.os.Bundle;
-import android.os.Handler;
import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
import android.os.RemoteException;
+import android.os.ResultReceiver;
import android.support.annotation.GuardedBy;
import android.util.ArrayMap;
import android.util.Log;
+
import com.android.media.MediaSession2Impl.ControllerInfoImpl;
+
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
public class MediaSession2Stub extends IMediaSession2.Stub {
+
+ static final String ARGUMENT_KEY_POSITION = "android.media.media_session2.key_position";
+ static final String ARGUMENT_KEY_ITEM_INDEX = "android.media.media_session2.key_item_index";
+ static final String ARGUMENT_KEY_PLAYLIST_PARAMS =
+ "android.media.media_session2.key_playlist_params";
+
private static final String TAG = "MediaSession2Stub";
private static final boolean DEBUG = true; // TODO(jaewan): Rename.
private final Object mLock = new Object();
- private final CommandHandler mCommandHandler;
private final WeakReference<MediaSession2Impl> mSession;
- private final Context mContext;
- private final SessionCallback mSessionCallback;
- private final MediaLibrarySessionCallback mLibraryCallback;
@GuardedBy("mLock")
private final ArrayMap<IBinder, ControllerInfo> mControllers = new ArrayMap<>();
- public MediaSession2Stub(MediaSession2Impl session, SessionCallback callback) {
+ public MediaSession2Stub(MediaSession2Impl session) {
mSession = new WeakReference<>(session);
- mContext = session.getContext();
- // TODO(jaewan): Should be executor from the session builder
- mCommandHandler = new CommandHandler(session.getHandler().getLooper());
- mSessionCallback = callback;
- mLibraryCallback = (callback instanceof MediaLibrarySessionCallback)
- ? (MediaLibrarySessionCallback) callback : null;
}
public void destroyNotLocked() {
final List<ControllerInfo> list;
synchronized (mLock) {
mSession.clear();
- mCommandHandler.removeCallbacksAndMessages(null);
list = getControllers();
mControllers.clear();
}
@@ -99,14 +89,60 @@
}
@Override
- public void connect(String callingPackage, IMediaSession2Callback callback) {
- if (callback == null) {
- // Requesting connect without callback to receive result.
- return;
- }
- ControllerInfo request = new ControllerInfo(mContext,
+ public void connect(String callingPackage, final IMediaSession2Callback callback)
+ throws RuntimeException {
+ final MediaSession2Impl sessionImpl = getSession();
+ final ControllerInfo request = new ControllerInfo(sessionImpl.getContext(),
Binder.getCallingUid(), Binder.getCallingPid(), callingPackage, callback);
- mCommandHandler.postConnect(request);
+ sessionImpl.getCallbackExecutor().execute(() -> {
+ final MediaSession2Impl session = mSession.get();
+ if (session == null) {
+ return;
+ }
+ CommandGroup allowedCommands = session.getCallback().onConnect(request);
+ // Don't reject connection for the request from trusted app.
+ // Otherwise server will fail to retrieve session's information to dispatch
+ // media keys to.
+ boolean accept = allowedCommands != null || request.isTrusted();
+ ControllerInfoImpl impl = ControllerInfoImpl.from(request);
+ if (accept) {
+ synchronized (mLock) {
+ mControllers.put(impl.getId(), request);
+ }
+ if (allowedCommands == null) {
+ // For trusted apps, send non-null allowed commands to keep connection.
+ allowedCommands = new CommandGroup();
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "onConnectResult, request=" + request
+ + " accept=" + accept);
+ }
+ try {
+ callback.onConnectionChanged(
+ accept ? MediaSession2Stub.this : null,
+ allowedCommands == null ? null : allowedCommands.toBundle());
+ } catch (RemoteException e) {
+ // Controller may be died prematurely.
+ }
+ if (accept) {
+ // If connection is accepted, notify the current state to the controller.
+ // It's needed because we cannot call synchronous calls between session/controller.
+ // Note: We're doing this after the onConnectionChanged(), but there's no guarantee
+ // that events here are notified after the onConnected() because
+ // IMediaSession2Callback is oneway (i.e. async call) and CallbackStub will
+ // use thread poll for incoming calls.
+ // TODO(jaewan): Should we protect getting playback state?
+ final PlaybackState2 state = session.getInstance().getPlaybackState();
+ final Bundle bundle = state != null ? state.toBundle() : null;
+ try {
+ callback.onPlaybackStateChanged(bundle);
+ } catch (RemoteException e) {
+ // TODO(jaewan): Handle this.
+ // Controller may be died prematurely.
+ }
+ }
+ });
}
@Override
@@ -122,20 +158,106 @@
@Override
public void sendCommand(IMediaSession2Callback caller, Bundle command, Bundle args)
throws RuntimeException {
- ControllerInfo controller = getController(caller);
+ // TODO(jaewan): Generic command
+ }
+
+ @Override
+ public void sendTransportControlCommand(IMediaSession2Callback caller,
+ int commandCode, Bundle args) throws RuntimeException {
+ final MediaSession2Impl sessionImpl = getSession();
+ final ControllerInfo controller = getController(caller);
if (controller == null) {
if (DEBUG) {
Log.d(TAG, "Command from a controller that hasn't connected. Ignore");
}
return;
}
- mCommandHandler.postCommand(controller, Command.fromBundle(command), args);
+ sessionImpl.getCallbackExecutor().execute(() -> {
+ final MediaSession2Impl session = mSession.get();
+ if (session == null) {
+ return;
+ }
+ // TODO(jaewan): Sanity check.
+ Command command = new Command(commandCode);
+ boolean accepted = session.getCallback().onCommandRequest(controller, command);
+ if (!accepted) {
+ // Don't run rejected command.
+ if (DEBUG) {
+ Log.d(TAG, "Command " + commandCode + " from "
+ + controller + " was rejected by " + session);
+ }
+ return;
+ }
+
+ switch (commandCode) {
+ case MediaSession2.COMMAND_CODE_PLAYBACK_START:
+ session.getInstance().play();
+ break;
+ case MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE:
+ session.getInstance().pause();
+ break;
+ case MediaSession2.COMMAND_CODE_PLAYBACK_STOP:
+ session.getInstance().stop();
+ break;
+ case MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM:
+ session.getInstance().skipToPrevious();
+ break;
+ case MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM:
+ session.getInstance().skipToNext();
+ break;
+ case MediaSession2.COMMAND_CODE_PLAYBACK_PREPARE:
+ session.getInstance().prepare();
+ break;
+ case MediaSession2.COMMAND_CODE_PLAYBACK_FAST_FORWARD:
+ session.getInstance().fastForward();
+ break;
+ case MediaSession2.COMMAND_CODE_PLAYBACK_REWIND:
+ session.getInstance().rewind();
+ break;
+ case MediaSession2.COMMAND_CODE_PLAYBACK_SEEK_TO:
+ session.getInstance().seekTo(args.getLong(ARGUMENT_KEY_POSITION));
+ break;
+ case MediaSession2.COMMAND_CODE_PLAYBACK_SET_CURRENT_PLAYLIST_ITEM:
+ session.getInstance().setCurrentPlaylistItem(
+ args.getInt(ARGUMENT_KEY_ITEM_INDEX));
+ break;
+ case MediaSession2.COMMAND_CODE_PLAYBACK_SET_PLAYLIST_PARAMS:
+ session.getInstance().setPlaylistParams(
+ PlaylistParams.fromBundle(
+ args.getBundle(ARGUMENT_KEY_PLAYLIST_PARAMS)));
+ break;
+ default:
+ // TODO(jaewan): Resend unknown (new) commands through the custom command.
+ }
+ });
+ }
+
+ @Override
+ public void sendCustomCommand(final IMediaSession2Callback caller, final Bundle commandBundle,
+ final Bundle args, final ResultReceiver receiver) {
+ final MediaSession2Impl sessionImpl = getSession();
+ final ControllerInfo controller = getController(caller);
+ if (controller == null) {
+ if (DEBUG) {
+ Log.d(TAG, "Command from a controller that hasn't connected. Ignore");
+ }
+ return;
+ }
+ sessionImpl.getCallbackExecutor().execute(() -> {
+ final MediaSession2Impl session = mSession.get();
+ if (session == null) {
+ return;
+ }
+ final Command command = Command.fromBundle(commandBundle);
+ session.getCallback().onCustomCommand(controller, command, args, receiver);
+ });
}
@Override
public void getBrowserRoot(IMediaSession2Callback caller, Bundle rootHints)
throws RuntimeException {
- if (mLibraryCallback == null) {
+ final MediaSession2Impl sessionImpl = getSession();
+ if (!(sessionImpl.getCallback() instanceof MediaLibrarySessionCallback)) {
if (DEBUG) {
Log.d(TAG, "Session cannot hand getBrowserRoot()");
}
@@ -148,51 +270,36 @@
}
return;
}
- mCommandHandler.postOnGetRoot(controller, rootHints);
- }
-
- @Deprecated
- @Override
- public Bundle getPlaybackState() throws RemoteException {
- MediaSession2Impl session = getSession();
- // TODO(jaewan): Check if mPlayer.getPlaybackState() is safe here.
- return session.getInstance().getPlayer().getPlaybackState().toBundle();
- }
-
- @Deprecated
- @Override
- public void registerCallback(final IMediaSession2Callback callbackBinder,
- final int callbackFlag, final int requestCode) throws RemoteException {
- // TODO(jaewan): Call onCommand() here. To do so, you should pend message.
- synchronized (mLock) {
- ControllerInfo controllerInfo = getController(callbackBinder);
- if (controllerInfo == null) {
+ sessionImpl.getCallbackExecutor().execute(() -> {
+ final MediaSession2Impl session = mSession.get();
+ if (session == null) {
return;
}
- ControllerInfoImpl.from(controllerInfo).addFlag(callbackFlag);
- }
- }
-
- @Deprecated
- @Override
- public void unregisterCallback(IMediaSession2Callback callbackBinder, int callbackFlag)
- throws RemoteException {
- // TODO(jaewan): Call onCommand() here. To do so, you should pend message.
- synchronized (mLock) {
- ControllerInfo controllerInfo = getController(callbackBinder);
- if (controllerInfo == null) {
- return;
+ final MediaLibrarySessionCallback libraryCallback =
+ (MediaLibrarySessionCallback) session.getCallback();
+ final ControllerInfoImpl controllerImpl = ControllerInfoImpl.from(controller);
+ BrowserRoot root = libraryCallback.onGetRoot(controller, rootHints);
+ try {
+ controllerImpl.getControllerBinder().onGetRootResult(rootHints,
+ root == null ? null : root.getRootId(),
+ root == null ? null : root.getExtras());
+ } catch (RemoteException e) {
+ // Controller may be died prematurely.
+ // TODO(jaewan): Handle this.
}
- ControllerInfoImpl.from(controllerInfo).removeFlag(callbackFlag);
- }
+ });
}
private ControllerInfo getController(IMediaSession2Callback caller) {
+ // TODO(jaewan): Device a way to return connection-in-progress-controller
+ // to be included here, because session owner may want to send some datas
+ // while onConnected() hasn't returned.
synchronized (mLock) {
return mControllers.get(caller.asBinder());
}
}
+ // TODO(jaewan): Need a way to get controller with permissions
public List<ControllerInfo> getControllers() {
ArrayList<ControllerInfo> controllers = new ArrayList<>();
synchronized (mLock) {
@@ -203,27 +310,15 @@
return controllers;
}
- public List<ControllerInfo> getControllersWithFlag(int flag) {
- ArrayList<ControllerInfo> controllers = new ArrayList<>();
- synchronized (mLock) {
- for (int i = 0; i < mControllers.size(); i++) {
- ControllerInfo controllerInfo = mControllers.valueAt(i);
- if (ControllerInfoImpl.from(controllerInfo).containsFlag(flag)) {
- controllers.add(controllerInfo);
- }
- }
- }
- return controllers;
- }
-
// Should be used without a lock to prevent potential deadlock.
public void notifyPlaybackStateChangedNotLocked(PlaybackState2 state) {
- final List<ControllerInfo> list = getControllersWithFlag(CALLBACK_FLAG_PLAYBACK);
+ final List<ControllerInfo> list = getControllers();
for (int i = 0; i < list.size(); i++) {
IMediaSession2Callback callbackBinder =
ControllerInfoImpl.from(list.get(i)).getControllerBinder();
try {
- callbackBinder.onPlaybackStateChanged(state.toBundle());
+ final Bundle bundle = state != null ? state.toBundle() : null;
+ callbackBinder.onPlaybackStateChanged(bundle);
} catch (RemoteException e) {
Log.w(TAG, "Controller is gone", e);
// TODO(jaewan): What to do when the controller is gone?
@@ -251,130 +346,83 @@
}
}
- // TODO(jaewan): Remove this. We should use Executor given by the session builder.
- private class CommandHandler extends Handler {
- public static final int MSG_CONNECT = 1000;
- public static final int MSG_COMMAND = 1001;
- public static final int MSG_ON_GET_ROOT = 2000;
-
- public CommandHandler(Looper looper) {
- super(looper);
+ public void notifyPlaylistChanged(List<MediaItem2> playlist) {
+ if (playlist == null) {
+ return;
}
-
- @Override
- public void handleMessage(Message msg) {
- final MediaSession2Impl session = MediaSession2Stub.this.mSession.get();
- if (session == null || session.getPlayer() == null) {
- return;
- }
-
- switch (msg.what) {
- case MSG_CONNECT: {
- ControllerInfo request = (ControllerInfo) msg.obj;
- CommandGroup allowedCommands = mSessionCallback.onConnect(request);
- // Don't reject connection for the request from trusted app.
- // Otherwise server will fail to retrieve session's information to dispatch
- // media keys to.
- boolean accept = allowedCommands != null || request.isTrusted();
- ControllerInfoImpl impl = ControllerInfoImpl.from(request);
- if (accept) {
- synchronized (mLock) {
- mControllers.put(impl.getId(), request);
- }
- if (allowedCommands == null) {
- // For trusted apps, send non-null allowed commands to keep connection.
- allowedCommands = new CommandGroup();
- }
- }
- if (DEBUG) {
- Log.d(TAG, "onConnectResult, request=" + request
- + " accept=" + accept);
- }
- try {
- impl.getControllerBinder().onConnectionChanged(
- accept ? MediaSession2Stub.this : null,
- allowedCommands == null ? null : allowedCommands.toBundle());
- } catch (RemoteException e) {
- // Controller may be died prematurely.
- }
- break;
- }
- case MSG_COMMAND: {
- CommandParam param = (CommandParam) msg.obj;
- Command command = param.command;
- boolean accepted = mSessionCallback.onCommandRequest(
- param.controller, command);
- if (!accepted) {
- // Don't run rejected command.
- if (DEBUG) {
- Log.d(TAG, "Command " + command + " from "
- + param.controller + " was rejected by " + session);
- }
- return;
- }
-
- switch (param.command.getCommandCode()) {
- case MediaSession2.COMMAND_CODE_PLAYBACK_START:
- session.getInstance().play();
- break;
- case MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE:
- session.getInstance().pause();
- break;
- case MediaSession2.COMMAND_CODE_PLAYBACK_STOP:
- session.getInstance().stop();
- break;
- case MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM:
- session.getInstance().skipToPrevious();
- break;
- case MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM:
- session.getInstance().skipToNext();
- break;
- default:
- // TODO(jaewan): Handle custom command.
- }
- break;
- }
- case MSG_ON_GET_ROOT: {
- final CommandParam param = (CommandParam) msg.obj;
- final ControllerInfoImpl controller = ControllerInfoImpl.from(param.controller);
- BrowserRoot root = mLibraryCallback.onGetRoot(param.controller, param.args);
- try {
- controller.getControllerBinder().onGetRootResult(param.args,
- root == null ? null : root.getRootId(),
- root == null ? null : root.getExtras());
- } catch (RemoteException e) {
- // Controller may be died prematurely.
- // TODO(jaewan): Handle this.
- }
- break;
+ final List<Bundle> bundleList = new ArrayList<>();
+ for (int i = 0; i < playlist.size(); i++) {
+ if (playlist.get(i) != null) {
+ Bundle bundle = playlist.get(i).toBundle();
+ if (bundle != null) {
+ bundleList.add(bundle);
}
}
}
-
- public void postConnect(ControllerInfo request) {
- obtainMessage(MSG_CONNECT, request).sendToTarget();
- }
-
- public void postCommand(ControllerInfo controller, Command command, Bundle args) {
- CommandParam param = new CommandParam(controller, command, args);
- obtainMessage(MSG_COMMAND, param).sendToTarget();
- }
-
- public void postOnGetRoot(ControllerInfo controller, Bundle rootHints) {
- CommandParam param = new CommandParam(controller, null, rootHints);
- obtainMessage(MSG_ON_GET_ROOT, param).sendToTarget();
+ final List<ControllerInfo> list = getControllers();
+ for (int i = 0; i < list.size(); i++) {
+ IMediaSession2Callback callbackBinder =
+ ControllerInfoImpl.from(list.get(i)).getControllerBinder();
+ try {
+ callbackBinder.onPlaylistChanged(bundleList);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Controller is gone", e);
+ // TODO(jaewan): What to do when the controller is gone?
+ }
}
}
- private static class CommandParam {
- public final ControllerInfo controller;
- public final Command command;
- public final Bundle args;
+ public void notifyPlaylistParamsChanged(MediaSession2.PlaylistParams params) {
+ final List<ControllerInfo> list = getControllers();
+ for (int i = 0; i < list.size(); i++) {
+ IMediaSession2Callback callbackBinder =
+ ControllerInfoImpl.from(list.get(i)).getControllerBinder();
+ try {
+ callbackBinder.onPlaylistParamsChanged(params.toBundle());
+ } catch (RemoteException e) {
+ Log.w(TAG, "Controller is gone", e);
+ // TODO(jaewan): What to do when the controller is gone?
+ }
+ }
+ }
- private CommandParam(ControllerInfo controller, Command command, Bundle args) {
- this.controller = controller;
- this.command = command;
- this.args = args;
+ public void sendCustomCommand(ControllerInfo controller, Command command, Bundle args,
+ ResultReceiver receiver) {
+ if (receiver != null && controller == null) {
+ throw new IllegalArgumentException("Controller shouldn't be null if result receiver is"
+ + " specified");
+ }
+ if (command == null) {
+ throw new IllegalArgumentException("command shouldn't be null");
+ }
+ final IMediaSession2Callback callbackBinder =
+ ControllerInfoImpl.from(controller).getControllerBinder();
+ if (getController(callbackBinder) == null) {
+ throw new IllegalArgumentException("Controller is gone");
+ }
+ sendCustomCommandInternal(controller, command, args, receiver);
+ }
+
+ public void sendCustomCommand(Command command, Bundle args) {
+ if (command == null) {
+ throw new IllegalArgumentException("command shouldn't be null");
+ }
+ final List<ControllerInfo> controllers = getControllers();
+ for (int i = 0; i < controllers.size(); i++) {
+ sendCustomCommand(controllers.get(i), command, args, null);
+ }
+ }
+
+ private void sendCustomCommandInternal(ControllerInfo controller, Command command, Bundle args,
+ ResultReceiver receiver) {
+ final IMediaSession2Callback callbackBinder =
+ ControllerInfoImpl.from(controller).getControllerBinder();
+ try {
+ Bundle commandBundle = command.toBundle();
+ callbackBinder.sendCustomCommand(commandBundle, args, receiver);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Controller is gone", e);
+ // TODO(jaewan): What to do when the controller is gone?
}
}
}
diff --git a/packages/MediaComponents/src/com/android/media/MediaSessionService2Impl.java b/packages/MediaComponents/src/com/android/media/MediaSessionService2Impl.java
index 9d24082..7dce109 100644
--- a/packages/MediaComponents/src/com/android/media/MediaSessionService2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaSessionService2Impl.java
@@ -20,17 +20,16 @@
import android.app.NotificationManager;
import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.media.MediaPlayerBase.PlaybackListener;
+import android.media.MediaPlayerInterface.PlaybackListener;
import android.media.MediaSession2;
import android.media.MediaSessionService2;
import android.media.MediaSessionService2.MediaNotification;
import android.media.PlaybackState2;
+import android.media.SessionToken2;
+import android.media.SessionToken2.TokenType;
import android.media.session.PlaybackState;
import android.media.update.MediaSessionService2Provider;
import android.os.IBinder;
-import android.os.Looper;
import android.support.annotation.GuardedBy;
import android.util.Log;
@@ -82,39 +81,27 @@
NOTIFICATION_SERVICE);
mStartSelfIntent = new Intent(mInstance, mInstance.getClass());
- Intent serviceIntent = createServiceIntent();
- ResolveInfo resolveInfo = mInstance.getPackageManager()
- .resolveService(serviceIntent, PackageManager.GET_META_DATA);
- String id;
- if (resolveInfo == null || resolveInfo.serviceInfo == null) {
- throw new IllegalArgumentException("service " + mInstance + " doesn't implement"
- + serviceIntent.getAction());
- } else if (resolveInfo.serviceInfo.metaData == null) {
- if (DEBUG) {
- Log.d(TAG, "Failed to resolve ID for " + mInstance + ". Using empty id");
- }
- id = "";
- } else {
- id = resolveInfo.serviceInfo.metaData.getString(
- MediaSessionService2.SERVICE_META_DATA, "");
+ SessionToken2 token = new SessionToken2(mInstance, mInstance.getPackageName(),
+ mInstance.getClass().getName());
+ if (token.getType() != getSessionType()) {
+ throw new RuntimeException("Expected session service, but was " + token.getType());
}
- mSession = mInstance.onCreateSession(id);
- if (mSession == null || !id.equals(mSession.getToken().getId())) {
- throw new RuntimeException("Expected session with id " + id + ", but got " + mSession);
+ mSession = mInstance.onCreateSession(token.getId());
+ if (mSession == null || !token.getId().equals(mSession.getToken().getId())) {
+ throw new RuntimeException("Expected session with id " + token.getId()
+ + ", but got " + mSession);
}
// TODO(jaewan): Uncomment here.
// mSession.addPlaybackListener(mListener, mSession.getExecutor());
}
- Intent createServiceIntent() {
- Intent serviceIntent = new Intent(mInstance, mInstance.getClass());
- serviceIntent.setAction(MediaSessionService2.SERVICE_INTERFACE);
- return serviceIntent;
+ @TokenType int getSessionType() {
+ return SessionToken2.TYPE_SESSION_SERVICE;
}
public IBinder onBind_impl(Intent intent) {
if (MediaSessionService2.SERVICE_INTERFACE.equals(intent.getAction())) {
- return mSession.getToken().getSessionBinder().asBinder();
+ return SessionToken2Impl.from(mSession.getToken()).getSessionBinder().asBinder();
}
return null;
}
@@ -152,11 +139,6 @@
return;
}
MediaSession2Impl impl = (MediaSession2Impl) mSession.getProvider();
- if (impl.getHandler().getLooper() != Looper.myLooper()) {
- Log.w(TAG, "Ignoring " + state + ". Expected " + impl.getHandler().getLooper()
- + " but " + Looper.myLooper());
- return;
- }
updateNotification(state);
}
}
diff --git a/packages/MediaComponents/src/com/android/media/PlaybackListenerHolder.java b/packages/MediaComponents/src/com/android/media/PlaybackListenerHolder.java
index 7b336c4..4241f85 100644
--- a/packages/MediaComponents/src/com/android/media/PlaybackListenerHolder.java
+++ b/packages/MediaComponents/src/com/android/media/PlaybackListenerHolder.java
@@ -16,11 +16,9 @@
package com.android.media;
-import android.media.MediaPlayerBase.PlaybackListener;
+import android.media.MediaPlayerInterface.PlaybackListener;
import android.media.PlaybackState2;
-import android.media.session.PlaybackState;
import android.os.Handler;
-import android.os.Message;
import android.support.annotation.NonNull;
import java.util.List;
diff --git a/packages/MediaComponents/src/com/android/media/SessionToken2Impl.java b/packages/MediaComponents/src/com/android/media/SessionToken2Impl.java
new file mode 100644
index 0000000..c91a89c
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/media/SessionToken2Impl.java
@@ -0,0 +1,248 @@
+/*
+ * 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.media;
+
+import static android.media.SessionToken2.TYPE_SESSION;
+import static android.media.SessionToken2.TYPE_SESSION_SERVICE;
+import static android.media.SessionToken2.TYPE_LIBRARY_SERVICE;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.media.MediaLibraryService2;
+import android.media.MediaSessionService2;
+import android.media.SessionToken2;
+import android.media.SessionToken2.TokenType;
+import android.media.update.SessionToken2Provider;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.text.TextUtils;
+
+public class SessionToken2Impl implements SessionToken2Provider {
+ private static final String KEY_UID = "android.media.token.uid";
+ private static final String KEY_TYPE = "android.media.token.type";
+ private static final String KEY_PACKAGE_NAME = "android.media.token.package_name";
+ private static final String KEY_SERVICE_NAME = "android.media.token.service_name";
+ private static final String KEY_ID = "android.media.token.id";
+ private static final String KEY_SESSION_BINDER = "android.media.token.session_binder";
+
+ private final SessionToken2 mInstance;
+ private final int mUid;
+ private final @TokenType int mType;
+ private final String mPackageName;
+ private final String mServiceName;
+ private final String mId;
+ private final IMediaSession2 mSessionBinder;
+
+ /**
+ * Public constructor for the legacy support (i.e. browser can try connecting to any browser service
+ * if it knows the service name)
+ */
+ public SessionToken2Impl(Context context, SessionToken2 instance,
+ String packageName, String serviceName, int uid) {
+ if (TextUtils.isEmpty(packageName)) {
+ throw new IllegalArgumentException("package name shouldn't be null");
+ }
+ if (TextUtils.isEmpty(serviceName)) {
+ throw new IllegalArgumentException("service name shouldn't be null");
+ }
+ mInstance = instance;
+ // Calculate uid if it's not specified.
+ final PackageManager manager = context.getPackageManager();
+ if (uid < 0) {
+ try {
+ uid = manager.getPackageUid(packageName, 0);
+ } catch (NameNotFoundException e) {
+ throw new IllegalArgumentException("Cannot find package " + packageName);
+ }
+ }
+ mUid = uid;
+ // calculate id and type
+ Intent serviceIntent = new Intent(MediaLibraryService2.SERVICE_INTERFACE);
+ serviceIntent.setClassName(packageName, serviceName);
+ String id = getSessionId(manager.resolveService(serviceIntent,
+ PackageManager.GET_META_DATA));
+ int type = TYPE_LIBRARY_SERVICE;
+ if (id == null) {
+ // retry with session service
+ serviceIntent.setClassName(packageName, serviceName);
+ id = getSessionId(manager.resolveService(serviceIntent,
+ PackageManager.GET_META_DATA));
+ type = TYPE_SESSION_SERVICE;
+ }
+ if (id == null) {
+ throw new IllegalArgumentException("service " + serviceName + " doesn't implement"
+ + " session service nor library service");
+ }
+ mId = id;
+ mType = type;
+ mPackageName = packageName;
+ mServiceName = serviceName;
+ mSessionBinder = null;
+ }
+
+ public SessionToken2Impl(Context context, int uid, int type,
+ String packageName, String serviceName, String id, IMediaSession2 sessionBinder) {
+ // TODO(jaewan): Add sanity check
+ mUid = uid;
+ mType = type;
+ mPackageName = packageName;
+ mServiceName = serviceName;
+ mId = id;
+ mSessionBinder = sessionBinder;
+ mInstance = new SessionToken2(this);
+ }
+
+ public static String getSessionId(ResolveInfo resolveInfo) {
+ if (resolveInfo == null || resolveInfo.serviceInfo == null) {
+ return null;
+ } else if (resolveInfo.serviceInfo.metaData == null) {
+ return "";
+ } else {
+ return resolveInfo.serviceInfo.metaData.getString(
+ MediaSessionService2.SERVICE_META_DATA, "");
+ }
+ }
+
+ public SessionToken2 getInstance() {
+ return mInstance;
+ }
+
+ @Override
+ public String getPackageName_impl() {
+ return mPackageName;
+ }
+
+ @Override
+ public int getUid_impl() {
+ return mUid;
+ }
+
+ @Override
+ public String getId_imp() {
+ return mId;
+ }
+
+ @Override
+ public int getType_impl() {
+ return mType;
+ }
+
+ public String getServiceName() {
+ return mServiceName;
+ }
+
+ public IMediaSession2 getSessionBinder() {
+ return mSessionBinder;
+ }
+
+ public static SessionToken2 fromBundle(Context context, Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+ final int uid = bundle.getInt(KEY_UID);
+ final @TokenType int type = bundle.getInt(KEY_TYPE, -1);
+ final String packageName = bundle.getString(KEY_PACKAGE_NAME);
+ final String serviceName = bundle.getString(KEY_SERVICE_NAME);
+ final String id = bundle.getString(KEY_ID);
+ final IBinder sessionBinder = bundle.getBinder(KEY_SESSION_BINDER);
+
+ // Sanity check.
+ switch (type) {
+ case TYPE_SESSION:
+ if (sessionBinder == null) {
+ throw new IllegalArgumentException("Unexpected sessionBinder for session,"
+ + " binder=" + sessionBinder);
+ }
+ break;
+ case TYPE_SESSION_SERVICE:
+ case TYPE_LIBRARY_SERVICE:
+ if (TextUtils.isEmpty(serviceName)) {
+ throw new IllegalArgumentException("Session service needs service name");
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid type");
+ }
+ if (TextUtils.isEmpty(packageName) || id == null) {
+ throw new IllegalArgumentException("Package name nor ID cannot be null.");
+ }
+ // TODO(jaewan): Revisit here when we add connection callback to the session for individual
+ // controller's permission check. With it, sessionBinder should be available
+ // if and only if for session, not session service.
+ return new SessionToken2Impl(context, uid, type, packageName, serviceName, id,
+ sessionBinder != null ? IMediaSession2.Stub.asInterface(sessionBinder) : null)
+ .getInstance();
+ }
+
+ @Override
+ public Bundle toBundle_impl() {
+ Bundle bundle = new Bundle();
+ bundle.putInt(KEY_UID, mUid);
+ bundle.putString(KEY_PACKAGE_NAME, mPackageName);
+ bundle.putString(KEY_SERVICE_NAME, mServiceName);
+ bundle.putString(KEY_ID, mId);
+ bundle.putInt(KEY_TYPE, mType);
+ bundle.putBinder(KEY_SESSION_BINDER,
+ mSessionBinder != null ? mSessionBinder.asBinder() : null);
+ return bundle;
+ }
+
+ @Override
+ public int hashCode_impl() {
+ final int prime = 31;
+ return mType
+ + prime * (mUid
+ + prime * (mPackageName.hashCode()
+ + prime * (mId.hashCode()
+ + prime * ((mServiceName != null ? mServiceName.hashCode() : 0)
+ + prime * (mSessionBinder != null ? mSessionBinder.asBinder().hashCode() : 0)))));
+ }
+
+ @Override
+ public boolean equals_impl(Object obj) {
+ if (!(obj instanceof SessionToken2)) {
+ return false;
+ }
+ SessionToken2Impl other = from((SessionToken2) obj);
+ if (mUid != other.mUid
+ || !TextUtils.equals(mPackageName, other.mPackageName)
+ || !TextUtils.equals(mServiceName, other.mServiceName)
+ || !TextUtils.equals(mId, other.mId)
+ || mType != other.mType) {
+ return false;
+ }
+ if (mSessionBinder == other.mSessionBinder) {
+ return true;
+ } else if (mSessionBinder == null || other.mSessionBinder == null) {
+ return false;
+ }
+ return mSessionBinder.asBinder().equals(other.mSessionBinder.asBinder());
+ }
+
+ @Override
+ public String toString_impl() {
+ return "SessionToken {pkg=" + mPackageName + " id=" + mId + " type=" + mType
+ + " service=" + mServiceName + " binder=" + mSessionBinder + "}";
+ }
+
+ public static SessionToken2Impl from(SessionToken2 token) {
+ return ((SessionToken2Impl) token.getProvider());
+ }
+}
diff --git a/packages/MediaComponents/src/com/android/media/update/ApiFactory.java b/packages/MediaComponents/src/com/android/media/update/ApiFactory.java
index 8ce7bef..b9d7612 100644
--- a/packages/MediaComponents/src/com/android/media/update/ApiFactory.java
+++ b/packages/MediaComponents/src/com/android/media/update/ApiFactory.java
@@ -20,42 +20,53 @@
import android.content.Context;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
+import android.media.DataSourceDesc;
import android.media.MediaBrowser2;
import android.media.MediaBrowser2.BrowserCallback;
import android.media.MediaController2;
import android.media.MediaController2.ControllerCallback;
+import android.media.MediaItem2;
import android.media.MediaLibraryService2;
import android.media.MediaLibraryService2.MediaLibrarySession;
import android.media.MediaLibraryService2.MediaLibrarySessionCallback;
-import android.media.MediaPlayerBase;
+import android.media.MediaMetadata2;
+import android.media.MediaPlayerInterface;
import android.media.MediaSession2;
import android.media.MediaSession2.ControllerInfo;
import android.media.MediaSession2.SessionCallback;
import android.media.MediaSessionService2;
-import android.media.IMediaSession2Callback;
+import android.media.SessionPlayer2;
import android.media.SessionToken2;
import android.media.VolumeProvider;
import android.media.update.MediaBrowser2Provider;
import android.media.update.MediaControlView2Provider;
import android.media.update.MediaController2Provider;
+import android.media.update.MediaItem2Provider;
import android.media.update.MediaLibraryService2Provider.MediaLibrarySessionProvider;
import android.media.update.MediaSession2Provider;
import android.media.update.MediaSessionService2Provider;
+import android.media.update.SessionPlayer2Provider;
+import android.media.update.SessionToken2Provider;
import android.media.update.VideoView2Provider;
import android.media.update.StaticProvider;
import android.media.update.ViewProvider;
+import android.os.Bundle;
import android.os.IInterface;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.widget.MediaControlView2;
import android.widget.VideoView2;
+import com.android.media.IMediaSession2;
+import com.android.media.IMediaSession2Callback;
import com.android.media.MediaBrowser2Impl;
import com.android.media.MediaController2Impl;
+import com.android.media.MediaItem2Impl;
import com.android.media.MediaLibraryService2Impl;
import com.android.media.MediaLibraryService2Impl.MediaLibrarySessionImpl;
import com.android.media.MediaSession2Impl;
import com.android.media.MediaSessionService2Impl;
+import com.android.media.SessionToken2Impl;
import com.android.widget.MediaControlView2Impl;
import com.android.widget.VideoView2Impl;
@@ -83,7 +94,7 @@
@Override
public MediaSession2Provider createMediaSession2(Context context, MediaSession2 instance,
- MediaPlayerBase player, String id, VolumeProvider volumeProvider,
+ MediaPlayerInterface player, String id, VolumeProvider volumeProvider,
int ratingType, PendingIntent sessionActivity, Executor callbackExecutor,
SessionCallback callback) {
return new MediaSession2Impl(context, instance, player, id, volumeProvider, ratingType,
@@ -112,7 +123,7 @@
@Override
public MediaLibrarySessionProvider createMediaLibraryService2MediaLibrarySession(
- Context context, MediaLibrarySession instance, MediaPlayerBase player,
+ Context context, MediaLibrarySession instance, MediaPlayerInterface player,
String id, VolumeProvider volumeProvider, int ratingType, PendingIntent sessionActivity,
Executor callbackExecutor, MediaLibrarySessionCallback callback) {
return new MediaLibrarySessionImpl(context, instance, player, id, volumeProvider,
@@ -131,4 +142,32 @@
@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
return new VideoView2Impl(instance, superProvider, attrs, defStyleAttr, defStyleRes);
}
+
+ @Override
+ public SessionToken2Provider createSessionToken2(Context context, SessionToken2 instance,
+ String packageName, String serviceName, int uid) {
+ return new SessionToken2Impl(context, instance, packageName, serviceName, uid);
+ }
+
+ @Override
+ public SessionToken2 SessionToken2_fromBundle(Context context, Bundle bundle) {
+ return SessionToken2Impl.fromBundle(context, bundle);
+ }
+
+ @Override
+ public SessionPlayer2Provider createSessionPlayer2(Context context, SessionPlayer2 instance) {
+ // TODO(jaewan): Implement this
+ return null;
+ }
+
+ @Override
+ public MediaItem2Provider createMediaItem2Provider(Context context, MediaItem2 instance,
+ String mediaId, DataSourceDesc dsd, MediaMetadata2 metadata, int flags) {
+ return new MediaItem2Impl(context, instance, mediaId, dsd, metadata, flags);
+ }
+
+ @Override
+ public MediaItem2 fromBundle_MediaItem2(Context context, Bundle bundle) {
+ return MediaItem2Impl.fromBundle(context, bundle);
+ }
}
diff --git a/packages/MediaComponents/src/com/android/widget/MediaControlView2Impl.java b/packages/MediaComponents/src/com/android/widget/MediaControlView2Impl.java
index bc370d8..688aadc 100644
--- a/packages/MediaComponents/src/com/android/widget/MediaControlView2Impl.java
+++ b/packages/MediaComponents/src/com/android/widget/MediaControlView2Impl.java
@@ -24,11 +24,11 @@
import android.media.update.ViewProvider;
import android.os.Bundle;
import android.util.Log;
-import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityManager;
+import android.widget.Button;
import android.widget.ImageButton;
import android.widget.MediaControlView2;
import android.widget.ProgressBar;
@@ -40,6 +40,7 @@
import com.android.media.update.R;
import java.util.Formatter;
+import java.util.List;
import java.util.Locale;
public class MediaControlView2Impl implements MediaControlView2Provider {
@@ -48,10 +49,6 @@
private final MediaControlView2 mInstance;
private final ViewProvider mSuperProvider;
- static final String COMMAND_SHOW_SUBTITLE = "showSubtitle";
- static final String COMMAND_HIDE_SUBTITLE = "hideSubtitle";
- static final String COMMAND_SET_FULLSCREEN = "setFullscreen";
-
static final String ARGUMENT_KEY_FULLSCREEN = "fullScreen";
static final String KEY_STATE_CONTAINS_SUBTITLE = "StateContainsSubtitle";
@@ -84,7 +81,6 @@
private boolean mSubtitleIsEnabled;
private boolean mContainsSubtitle;
private boolean mSeekAvailable;
- private View.OnClickListener mNextListener, mPrevListener;
private ImageButton mPlayPauseButton;
private ImageButton mFfwdButton;
private ImageButton mRewButton;
@@ -97,6 +93,7 @@
private ImageButton mOverflowButtonRight;
private ViewGroup mExtraControls;
+ private ViewGroup mCustomButtons;
private ImageButton mOverflowButtonLeft;
private ImageButton mMuteButton;
private ImageButton mAspectRationButton;
@@ -142,7 +139,7 @@
}
@Override
- public void show_impl(int timeout) {
+ public void show_impl(long timeout) {
if (!mShowing) {
setProgress();
if (mPlayPauseButton != null) {
@@ -184,92 +181,65 @@
}
@Override
- public void showSubtitle_impl() {
- mController.sendCommand(COMMAND_SHOW_SUBTITLE, null, null);
- }
-
- @Override
- public void hideSubtitle_impl() {
- mController.sendCommand(COMMAND_HIDE_SUBTITLE, null, null);
- }
-
- @Override
- public void setPrevNextListeners_impl(View.OnClickListener next, View.OnClickListener prev) {
- mNextListener = next;
- mPrevListener = prev;
-
- if (mNextButton != null) {
- mNextButton.setOnClickListener(mNextListener);
- mNextButton.setEnabled(mNextListener != null);
- mNextButton.setVisibility(View.VISIBLE);
- }
- if (mPrevButton != null) {
- mPrevButton.setOnClickListener(mPrevListener);
- mPrevButton.setEnabled(mPrevListener != null);
- mPrevButton.setVisibility(View.VISIBLE);
- }
- }
-
- @Override
- public void setButtonVisibility_impl(int button, boolean visible) {
+ public void setButtonVisibility_impl(int button, int visibility) {
switch (button) {
case MediaControlView2.BUTTON_PLAY_PAUSE:
if (mPlayPauseButton != null && canPause()) {
- mPlayPauseButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+ mPlayPauseButton.setVisibility(visibility);
}
break;
case MediaControlView2.BUTTON_FFWD:
if (mFfwdButton != null && canSeekForward()) {
- mFfwdButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+ mFfwdButton.setVisibility(visibility);
}
break;
case MediaControlView2.BUTTON_REW:
if (mRewButton != null && canSeekBackward()) {
- mRewButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+ mRewButton.setVisibility(visibility);
}
break;
case MediaControlView2.BUTTON_NEXT:
// TODO: this button is not visible unless its listener is manually set. Should this
// function still be provided?
if (mNextButton != null) {
- mNextButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+ mNextButton.setVisibility(visibility);
}
break;
case MediaControlView2.BUTTON_PREV:
// TODO: this button is not visible unless its listener is manually set. Should this
// function still be provided?
if (mPrevButton != null) {
- mPrevButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+ mPrevButton.setVisibility(visibility);
}
break;
case MediaControlView2.BUTTON_SUBTITLE:
if (mSubtitleButton != null && mContainsSubtitle) {
- mSubtitleButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+ mSubtitleButton.setVisibility(visibility);
}
break;
case MediaControlView2.BUTTON_FULL_SCREEN:
if (mFullScreenButton != null) {
- mFullScreenButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+ mFullScreenButton.setVisibility(visibility);
}
break;
case MediaControlView2.BUTTON_OVERFLOW:
if (mOverflowButtonRight != null) {
- mOverflowButtonRight.setVisibility((visible) ? View.VISIBLE : View.GONE);
+ mOverflowButtonRight.setVisibility(visibility);
}
break;
case MediaControlView2.BUTTON_MUTE:
if (mMuteButton != null) {
- mMuteButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+ mMuteButton.setVisibility(visibility);
}
break;
case MediaControlView2.BUTTON_ASPECT_RATIO:
if (mAspectRationButton != null) {
- mAspectRationButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+ mAspectRationButton.setVisibility(visibility);
}
break;
case MediaControlView2.BUTTON_SETTINGS:
if (mSettingsButton != null) {
- mSettingsButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+ mSettingsButton.setVisibility(visibility);
}
break;
default:
@@ -305,62 +275,11 @@
}
@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 && !isPlaying()) {
- togglePausePlayState();
- mInstance.show(DEFAULT_TIMEOUT_MS);
- }
- return true;
- } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
- || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
- if (uniqueDown && 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);
@@ -475,11 +394,11 @@
}
mNextButton = v.findViewById(R.id.next);
if (mNextButton != null) {
- mNextButton.setVisibility(View.GONE);
+ mNextButton.setOnClickListener(mNextListener);
}
mPrevButton = v.findViewById(R.id.prev);
if (mPrevButton != null) {
- mPrevButton.setVisibility(View.GONE);
+ mPrevButton.setOnClickListener(mPrevListener);
}
mBasicControls = v.findViewById(R.id.basic_controls);
@@ -501,6 +420,7 @@
// TODO: should these buttons be shown as default?
mExtraControls = v.findViewById(R.id.extra_controls);
+ mCustomButtons = v.findViewById(R.id.custom_buttons);
mOverflowButtonLeft = v.findViewById(R.id.overflow_left);
if (mOverflowButtonLeft != null) {
mOverflowButtonLeft.setOnClickListener(mOverflowLeftListener);
@@ -741,6 +661,22 @@
}
};
+ private final View.OnClickListener mNextListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mControls.skipToNext();
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ }
+ };
+
+ private final View.OnClickListener mPrevListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mControls.skipToPrevious();
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ }
+ };
+
private final View.OnClickListener mSubtitleListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
@@ -748,13 +684,13 @@
mSubtitleButton.setImageDrawable(
ApiHelper.getLibResources().getDrawable(
R.drawable.ic_media_subtitle_enabled, null));
- mInstance.showSubtitle();
+ mController.sendCommand(MediaControlView2.COMMAND_SHOW_SUBTITLE, null, null);
mSubtitleIsEnabled = true;
} else {
mSubtitleButton.setImageDrawable(
ApiHelper.getLibResources().getDrawable(
R.drawable.ic_media_subtitle_disabled, null));
- mInstance.hideSubtitle();
+ mController.sendCommand(MediaControlView2.COMMAND_HIDE_SUBTITLE, null, null);
mSubtitleIsEnabled = false;
}
mInstance.show(DEFAULT_TIMEOUT_MS);
@@ -776,7 +712,7 @@
}
Bundle args = new Bundle();
args.putBoolean(ARGUMENT_KEY_FULLSCREEN, isEnteringFullScreen);
- mController.sendCommand(COMMAND_SET_FULLSCREEN, args, null);
+ mController.sendCommand(MediaControlView2.COMMAND_SET_FULLSCREEN, args, null);
mIsFullScreen = isEnteringFullScreen;
mInstance.show(DEFAULT_TIMEOUT_MS);
@@ -875,6 +811,31 @@
}
mPlaybackActions = newActions;
}
+
+ // Add buttons if custom actions are present.
+ List<PlaybackState.CustomAction> customActions = mPlaybackState.getCustomActions();
+ mCustomButtons.removeAllViews();
+ if (customActions.size() > 0) {
+ for (PlaybackState.CustomAction action : customActions) {
+ ImageButton button = new ImageButton(mInstance.getContext(),
+ null /* AttributeSet */, 0 /* Style */);
+ // TODO: Apply R.style.BottomBarButton to this button using library context.
+ // Refer Constructor with argument (int defStyleRes) of View.java
+ button.setImageResource(action.getIcon());
+ button.setTooltipText(action.getName());
+ final String actionString = action.getAction().toString();
+ button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // TODO: Currently, we are just sending extras that came from session.
+ // Is it the right behavior?
+ mControls.sendCustomAction(actionString, action.getExtras());
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ }
+ });
+ mCustomButtons.addView(button);
+ }
+ }
}
@Override
diff --git a/packages/MediaComponents/src/com/android/widget/VideoView2Impl.java b/packages/MediaComponents/src/com/android/widget/VideoView2Impl.java
index f3aaec8..38ade41 100644
--- a/packages/MediaComponents/src/com/android/widget/VideoView2Impl.java
+++ b/packages/MediaComponents/src/com/android/widget/VideoView2Impl.java
@@ -16,6 +16,7 @@
package com.android.widget;
+import android.annotation.NonNull;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
@@ -25,11 +26,9 @@
import android.media.AudioManager;
import android.media.MediaMetadata;
import android.media.MediaPlayer;
-import android.media.MediaPlayerBase;
+import android.media.MediaPlayerInterface;
import android.media.Cea708CaptionRenderer;
import android.media.ClosedCaptionRenderer;
-import android.media.MediaMetadata;
-import android.media.MediaPlayer;
import android.media.Metadata;
import android.media.PlaybackParams;
import android.media.SubtitleController;
@@ -47,7 +46,6 @@
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.FrameLayout.LayoutParams;
@@ -56,11 +54,17 @@
import com.android.media.update.ApiHelper;
import com.android.media.update.R;
+import com.android.support.mediarouter.media.MediaRouter;
+import com.android.support.mediarouter.media.MediaRouteSelector;
+import com.android.support.mediarouter.media.RemotePlaybackClient;
+import com.android.support.mediarouter.media.MediaItemStatus;
+import com.android.support.mediarouter.media.MediaSessionStatus;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.Executor;
public class VideoView2Impl implements VideoView2Provider, VideoViewInterface.SurfaceListener {
private static final String TAG = "VideoView2";
@@ -80,14 +84,14 @@
private final AudioManager mAudioManager;
private AudioAttributes mAudioAttributes;
private int mAudioFocusType = AudioManager.AUDIOFOCUS_GAIN; // legacy focus gain
- private int mAudioSession;
+ private VideoView2.OnCustomActionListener mOnCustomActionListener;
private VideoView2.OnPreparedListener mOnPreparedListener;
private VideoView2.OnCompletionListener mOnCompletionListener;
private VideoView2.OnErrorListener mOnErrorListener;
private VideoView2.OnInfoListener mOnInfoListener;
private VideoView2.OnViewTypeChangedListener mOnViewTypeChangedListener;
- private VideoView2.OnFullScreenChangedListener mOnFullScreenChangedListener;
+ private VideoView2.OnFullScreenRequestListener mOnFullScreenRequestListener;
private VideoViewInterface mCurrentView;
private VideoTextureView mTextureView;
@@ -97,10 +101,14 @@
private MediaControlView2 mMediaControlView;
private MediaSession mMediaSession;
private MediaController mMediaController;
+ private MediaSession.Callback mRouteSessionCallback = new RouteSessionCallback();
+ private MediaRouter mMediaRouter;
+ private MediaRouteSelector mMediaRouteSelector;
private Metadata mMetadata;
private String mTitle;
private PlaybackState.Builder mStateBuilder;
+ private List<PlaybackState.CustomAction> mCustomActionList;
private int mTargetState = STATE_IDLE;
private int mCurrentState = STATE_IDLE;
private int mCurrentBufferPercentage;
@@ -182,6 +190,9 @@
@Override
public MediaController getMediaController_impl() {
+ if (mMediaSession == null) {
+ throw new IllegalStateException("MediaSession instance is not available.");
+ }
return mMediaController;
}
@@ -191,16 +202,6 @@
}
@Override
- public int getAudioSessionId_impl() {
- if (mAudioSession == 0) {
- MediaPlayer foo = new MediaPlayer();
- mAudioSession = foo.getAudioSessionId();
- foo.release();
- }
- return mAudioSession;
- }
-
- @Override
public void showSubtitle_impl() {
// Retrieve all tracks that belong to the current video.
MediaPlayer.TrackInfo[] trackInfos = mMediaPlayer.getTrackInfo();
@@ -228,13 +229,6 @@
}
}
- @Override
- public void setFullScreen_impl(boolean fullScreen) {
- if (mOnFullScreenChangedListener != null) {
- mOnFullScreenChangedListener.onFullScreenChanged(fullScreen);
- }
- }
-
// TODO: remove setSpeed_impl once MediaController2 is ready.
@Override
public void setSpeed_impl(float speed) {
@@ -270,22 +264,37 @@
}
@Override
- public void setRouteAttributes_impl(List<String> routeCategories, MediaPlayerBase player) {
+ public void setRouteAttributes_impl(List<String> routeCategories, MediaPlayerInterface player) {
// TODO: implement this.
}
@Override
+ public void setRouteAttributes_impl(@NonNull List<String> routeCategories,
+ MediaSession.Callback sessionPlayer) {
+ MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
+ for (String category : routeCategories) {
+ builder.addControlCategory(category);
+ }
+ mMediaRouteSelector = builder.build();
+ mMediaRouter = MediaRouter.getInstance(mInstance.getContext());
+ mRouteSessionCallback = sessionPlayer;
+ if (mMediaSession != null) {
+ mMediaRouter.setMediaSession(mMediaSession);
+ }
+ }
+
+ @Override
public void setVideoPath_impl(String path) {
- mInstance.setVideoURI(Uri.parse(path));
+ mInstance.setVideoUri(Uri.parse(path));
}
@Override
- public void setVideoURI_impl(Uri uri) {
- mInstance.setVideoURI(uri, null);
+ public void setVideoUri_impl(Uri uri) {
+ mInstance.setVideoUri(uri, null);
}
@Override
- public void setVideoURI_impl(Uri uri, Map<String, String> headers) {
+ public void setVideoUri_impl(Uri uri, Map<String, String> headers) {
mSeekWhenPrepared = 0;
openVideo(uri, headers);
}
@@ -315,34 +324,49 @@
return mCurrentView.getViewType();
}
+ // TODO: Handle executor properly for all the set listener methods.
@Override
- public void setOnPreparedListener_impl(VideoView2.OnPreparedListener l) {
+ public void setCustomActions_impl(
+ List<PlaybackState.CustomAction> actionList,
+ Executor executor, VideoView2.OnCustomActionListener listener) {
+ mCustomActionList = actionList;
+ mOnCustomActionListener = listener;
+
+ // Create a new playback builder in order to clear existing the custom actions.
+ mStateBuilder = null;
+ updatePlaybackState();
+ }
+
+ @Override
+ public void setOnPreparedListener_impl(Executor executor, VideoView2.OnPreparedListener l) {
mOnPreparedListener = l;
}
@Override
- public void setOnCompletionListener_impl(VideoView2.OnCompletionListener l) {
+ public void setOnCompletionListener_impl(Executor executor, VideoView2.OnCompletionListener l) {
mOnCompletionListener = l;
}
@Override
- public void setOnErrorListener_impl(VideoView2.OnErrorListener l) {
+ public void setOnErrorListener_impl(Executor executor, VideoView2.OnErrorListener l) {
mOnErrorListener = l;
}
@Override
- public void setOnInfoListener_impl(VideoView2.OnInfoListener l) {
+ public void setOnInfoListener_impl(Executor executor, VideoView2.OnInfoListener l) {
mOnInfoListener = l;
}
@Override
- public void setOnViewTypeChangedListener_impl(VideoView2.OnViewTypeChangedListener l) {
+ public void setOnViewTypeChangedListener_impl(Executor executor,
+ VideoView2.OnViewTypeChangedListener l) {
mOnViewTypeChangedListener = l;
}
@Override
- public void setFullScreenChangedListener_impl(VideoView2.OnFullScreenChangedListener l) {
- mOnFullScreenChangedListener = l;
+ public void setFullScreenRequestListener_impl(Executor executor,
+ VideoView2.OnFullScreenRequestListener l) {
+ mOnFullScreenRequestListener = l;
}
@Override
@@ -353,6 +377,9 @@
mMediaSession = new MediaSession(mInstance.getContext(), "VideoView2MediaSession");
mMediaSession.setCallback(new MediaSessionCallback());
mMediaController = mMediaSession.getController();
+ if (mMediaRouter != null) {
+ mMediaRouter.setMediaSession(mMediaSession);
+ }
attachMediaControlView();
}
@@ -362,6 +389,7 @@
mSuperProvider.onDetachedFromWindow_impl();
mMediaSession.release();
mMediaSession = null;
+ mMediaController = null;
}
@Override
@@ -392,58 +420,11 @@
}
@Override
- public boolean onKeyDown_impl(int keyCode, KeyEvent event) {
- 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()) {
- mMediaController.getTransportControls().pause();
- mMediaControlView.show();
- } else {
- mMediaController.getTransportControls().play();
- mMediaControlView.hide();
- }
- return true;
- } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
- if (!mMediaPlayer.isPlaying()) {
- mMediaController.getTransportControls().play();
- mMediaControlView.hide();
- }
- return true;
- } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
- || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
- if (mMediaPlayer.isPlaying()) {
- mMediaController.getTransportControls().pause();
- mMediaControlView.show();
- }
- return true;
- } else {
- toggleMediaControlViewVisibility();
- }
- }
-
- return mSuperProvider.onKeyDown_impl(keyCode, event);
- }
-
- @Override
public void onFinishInflate_impl() {
mSuperProvider.onFinishInflate_impl();
}
@Override
- public boolean dispatchKeyEvent_impl(KeyEvent event) {
- return mSuperProvider.dispatchKeyEvent_impl(event);
- }
-
- @Override
public void setEnabled_impl(boolean enabled) {
mSuperProvider.setEnabled_impl(enabled);
}
@@ -491,7 +472,7 @@
}
mCurrentView = view;
if (mOnViewTypeChangedListener != null) {
- mOnViewTypeChangedListener.onViewTypeChanged(view.getViewType());
+ mOnViewTypeChangedListener.onViewTypeChanged(mInstance, view.getViewType());
}
if (needToStart()) {
mMediaController.getTransportControls().play();
@@ -553,12 +534,6 @@
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);
@@ -657,8 +632,12 @@
}
mStateBuilder = new PlaybackState.Builder();
mStateBuilder.setActions(playbackActions);
- mStateBuilder.addCustomAction(MediaControlView2Impl.COMMAND_SHOW_SUBTITLE, null, -1);
- mStateBuilder.addCustomAction(MediaControlView2Impl.COMMAND_HIDE_SUBTITLE, null, -1);
+
+ if (mCustomActionList != null) {
+ for (PlaybackState.CustomAction action : mCustomActionList) {
+ mStateBuilder.addCustomAction(action);
+ }
+ }
}
mStateBuilder.setState(getCorrespondingPlaybackState(),
mMediaPlayer.getCurrentPosition(), mSpeed);
@@ -751,7 +730,7 @@
}
mCurrentState = STATE_PREPARED;
if (mOnPreparedListener != null) {
- mOnPreparedListener.onPrepared();
+ mOnPreparedListener.onPrepared(mInstance);
}
if (mMediaControlView != null) {
mMediaControlView.setEnabled(true);
@@ -826,7 +805,7 @@
updatePlaybackState();
if (mOnCompletionListener != null) {
- mOnCompletionListener.onCompletion();
+ mOnCompletionListener.onCompletion(mInstance);
}
if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
mAudioManager.abandonAudioFocus(null);
@@ -838,7 +817,7 @@
new MediaPlayer.OnInfoListener() {
public boolean onInfo(MediaPlayer mp, int what, int extra) {
if (mOnInfoListener != null) {
- mOnInfoListener.onInfo(what, extra);
+ mOnInfoListener.onInfo(mInstance, what, extra);
}
return true;
}
@@ -860,7 +839,7 @@
/* If an error handler has been supplied, use it and finish. */
if (mOnErrorListener != null) {
- if (mOnErrorListener.onError(frameworkErr, implErr)) {
+ if (mOnErrorListener.onError(mInstance, frameworkErr, implErr)) {
return true;
}
}
@@ -891,7 +870,7 @@
* at least inform them that the video is over.
*/
if (mOnCompletionListener != null) {
- mOnCompletionListener.onCompletion();
+ mOnCompletionListener.onCompletion(mInstance);
}
}
})
@@ -913,65 +892,161 @@
private class MediaSessionCallback extends MediaSession.Callback {
@Override
public void onCommand(String command, Bundle args, ResultReceiver receiver) {
- switch (command) {
- case MediaControlView2Impl.COMMAND_SHOW_SUBTITLE:
- mInstance.showSubtitle();
- break;
- case MediaControlView2Impl.COMMAND_HIDE_SUBTITLE:
- mInstance.hideSubtitle();
- break;
- case MediaControlView2Impl.COMMAND_SET_FULLSCREEN:
- mInstance.setFullScreen(
- args.getBoolean(MediaControlView2Impl.ARGUMENT_KEY_FULLSCREEN));
- break;
+ if (mMediaController.getPlaybackInfo().getPlaybackType()
+ == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) {
+ mRouteSessionCallback.onCommand(command, args, receiver);
+ } else {
+ switch (command) {
+ case MediaControlView2.COMMAND_SHOW_SUBTITLE:
+ mInstance.showSubtitle();
+ break;
+ case MediaControlView2.COMMAND_HIDE_SUBTITLE:
+ mInstance.hideSubtitle();
+ break;
+ case MediaControlView2.COMMAND_SET_FULLSCREEN:
+ if (mOnFullScreenRequestListener != null) {
+ mOnFullScreenRequestListener.onFullScreenRequest(
+ mInstance,
+ args.getBoolean(MediaControlView2Impl.ARGUMENT_KEY_FULLSCREEN));
+ }
+ break;
+ }
}
}
@Override
+ public void onCustomAction(String action, Bundle extras) {
+ mOnCustomActionListener.onCustomAction(action, extras);
+ }
+
+ @Override
public void onPlay() {
- if (isInPlaybackState() && mCurrentView.hasAvailableSurface()) {
- applySpeed();
- mMediaPlayer.start();
- mCurrentState = STATE_PLAYING;
- updatePlaybackState();
- }
- mTargetState = STATE_PLAYING;
- if (DEBUG) {
- Log.d(TAG, "onPlay(). mCurrentState=" + mCurrentState
- + ", mTargetState=" + mTargetState);
+ if (mMediaController.getPlaybackInfo().getPlaybackType()
+ == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) {
+ mRouteSessionCallback.onPlay();
+ } else {
+ if (isInPlaybackState() && mCurrentView.hasAvailableSurface()) {
+ applySpeed();
+ mMediaPlayer.start();
+ mCurrentState = STATE_PLAYING;
+ updatePlaybackState();
+ }
+ mTargetState = STATE_PLAYING;
+ if (DEBUG) {
+ Log.d(TAG, "onPlay(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
}
}
@Override
public void onPause() {
- if (isInPlaybackState()) {
- if (mMediaPlayer.isPlaying()) {
- mMediaPlayer.pause();
- mCurrentState = STATE_PAUSED;
- updatePlaybackState();
+ if (mMediaController.getPlaybackInfo().getPlaybackType()
+ == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) {
+ mRouteSessionCallback.onPause();
+ } else {
+ if (isInPlaybackState()) {
+ if (mMediaPlayer.isPlaying()) {
+ mMediaPlayer.pause();
+ mCurrentState = STATE_PAUSED;
+ updatePlaybackState();
+ }
}
- }
- mTargetState = STATE_PAUSED;
- if (DEBUG) {
- Log.d(TAG, "onPause(). mCurrentState=" + mCurrentState
- + ", mTargetState=" + mTargetState);
+ mTargetState = STATE_PAUSED;
+ if (DEBUG) {
+ Log.d(TAG, "onPause(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
}
}
@Override
public void onSeekTo(long pos) {
- if (isInPlaybackState()) {
- mMediaPlayer.seekTo(pos, MediaPlayer.SEEK_PREVIOUS_SYNC);
- mSeekWhenPrepared = 0;
- updatePlaybackState();
+ if (mMediaController.getPlaybackInfo().getPlaybackType()
+ == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) {
+ mRouteSessionCallback.onSeekTo(pos);
} else {
- mSeekWhenPrepared = pos;
+ if (isInPlaybackState()) {
+ mMediaPlayer.seekTo(pos, MediaPlayer.SEEK_PREVIOUS_SYNC);
+ mSeekWhenPrepared = 0;
+ updatePlaybackState();
+ } else {
+ mSeekWhenPrepared = pos;
+ }
}
}
@Override
public void onStop() {
- resetPlayer();
+ if (mMediaController.getPlaybackInfo().getPlaybackType()
+ == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) {
+ mRouteSessionCallback.onStop();
+ } else {
+ resetPlayer();
+ }
+ }
+ }
+
+ private class RouteSessionCallback extends MediaSession.Callback {
+ RemotePlaybackClient mClient;
+
+ RemotePlaybackClient.StatusCallback mStatusCallback =
+ new RemotePlaybackClient.StatusCallback() {
+ @Override
+ public void onItemStatusChanged(Bundle data,
+ String sessionId, MediaSessionStatus sessionStatus,
+ String itemId, MediaItemStatus itemStatus) {
+ // TODO: implement this
+ }
+
+ @Override
+ public void onSessionStatusChanged(Bundle data,
+ String sessionId, MediaSessionStatus sessionStatus) {
+ // TODO: implement this
+ }
+
+ @Override
+ public void onSessionChanged(String sessionId) {
+ // TODO: implement this
+ }
+ };
+
+ @Override
+ public void onCommand(String command, Bundle args, ResultReceiver receiver) {
+ ensureClient();
+ // TODO: implement this
+ }
+
+ @Override
+ public void onPlay() {
+ ensureClient();
+ // TODO: implement this
+ }
+
+ @Override
+ public void onPause() {
+ ensureClient();
+ // TODO: implement this
+ }
+
+ @Override
+ public void onSeekTo(long pos) {
+ ensureClient();
+ // TODO: implement this
+ }
+
+ @Override
+ public void onStop() {
+ ensureClient();
+ // TODO: implement this
+ }
+
+ private void ensureClient() {
+ if (mClient == null) {
+ mClient = new RemotePlaybackClient(
+ mInstance.getContext(), mMediaRouter.getSelectedRoute());
+ mClient.setStatusCallback(mStatusCallback);
+ }
}
}
}
diff --git a/packages/MediaComponents/test/runtest.sh b/packages/MediaComponents/test/runtest.sh
index 5c0ef51..920fa96 100644
--- a/packages/MediaComponents/test/runtest.sh
+++ b/packages/MediaComponents/test/runtest.sh
@@ -129,11 +129,12 @@
${adb} root
${adb} remount
${adb} shell stop
+ ${adb} shell setprop log.tag.MediaSessionService DEBUG
${adb} sync
${adb} shell start
${adb} wait-for-device || break
# Ensure package manager is loaded.
- sleep 5
+ sleep 15
# Install apks
local install_failed="false"
diff --git a/packages/MediaComponents/test/src/android/media/MediaBrowser2Test.java b/packages/MediaComponents/test/src/android/media/MediaBrowser2Test.java
index ef75060..50e59b8 100644
--- a/packages/MediaComponents/test/src/android/media/MediaBrowser2Test.java
+++ b/packages/MediaComponents/test/src/android/media/MediaBrowser2Test.java
@@ -20,10 +20,14 @@
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
+import android.annotation.Nullable;
import android.content.Context;
import android.media.MediaBrowser2.BrowserCallback;
+import android.media.MediaSession2.Command;
import android.media.MediaSession2.CommandGroup;
+import android.media.MediaSession2.PlaylistParams;
import android.os.Bundle;
+import android.os.ResultReceiver;
import android.support.annotation.CallSuper;
import android.support.annotation.NonNull;
import android.support.test.filters.SmallTest;
@@ -49,17 +53,25 @@
@Override
TestControllerInterface onCreateController(@NonNull SessionToken2 token,
- @NonNull TestControllerCallbackInterface callback) {
+ @Nullable TestControllerCallbackInterface callback) {
+ if (callback == null) {
+ callback = new TestBrowserCallbackInterface() {};
+ }
return new TestMediaBrowser(mContext, token, new TestBrowserCallback(callback));
}
+ interface TestBrowserCallbackInterface extends TestControllerCallbackInterface {
+ // Browser specific callbacks
+ default void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {}
+ }
+
@Test
public void testGetBrowserRoot() throws InterruptedException {
final Bundle param = new Bundle();
param.putString(TAG, TAG);
final CountDownLatch latch = new CountDownLatch(1);
- final TestControllerCallbackInterface callback = new TestControllerCallbackInterface() {
+ final TestControllerCallbackInterface callback = new TestBrowserCallbackInterface() {
@Override
public void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {
assertTrue(TestUtils.equals(param, rootHints));
@@ -83,6 +95,9 @@
public final CountDownLatch disconnectLatch = new CountDownLatch(1);
TestBrowserCallback(TestControllerCallbackInterface callbackProxy) {
+ if (callbackProxy == null) {
+ throw new IllegalArgumentException("Callback proxy shouldn't be null. Test bug");
+ }
mCallbackProxy = callbackProxy;
}
@@ -101,8 +116,30 @@
}
@Override
+ public void onPlaybackStateChanged(PlaybackState2 state) {
+ super.onPlaybackStateChanged(state);
+ mCallbackProxy.onPlaybackStateChanged(state);
+ }
+
+ @Override
+ public void onPlaylistParamsChanged(PlaylistParams params) {
+ super.onPlaylistParamsChanged(params);
+ mCallbackProxy.onPlaylistParamsChanged(params);
+ }
+
+ @Override
+ public void onCustomCommand(Command command, Bundle args, ResultReceiver receiver) {
+ super.onCustomCommand(command, args, receiver);
+ mCallbackProxy.onCustomCommand(command, args, receiver);
+ }
+
+ @Override
public void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {
- mCallbackProxy.onGetRootResult(rootHints, rootMediaId, rootExtra);
+ super.onGetRootResult(rootHints, rootMediaId, rootExtra);
+ if (mCallbackProxy instanceof TestBrowserCallbackInterface) {
+ ((TestBrowserCallbackInterface) mCallbackProxy)
+ .onGetRootResult(rootHints, rootMediaId, rootExtra);
+ }
}
@Override
diff --git a/packages/MediaComponents/test/src/android/media/MediaController2Test.java b/packages/MediaComponents/test/src/android/media/MediaController2Test.java
index ae67a95..9c5aa21 100644
--- a/packages/MediaComponents/test/src/android/media/MediaController2Test.java
+++ b/packages/MediaComponents/test/src/android/media/MediaController2Test.java
@@ -16,14 +16,19 @@
package android.media;
-import android.media.MediaPlayerBase.PlaybackListener;
+import android.media.MediaController2.ControllerCallback;
+import android.media.MediaPlayerInterface.PlaybackListener;
+import android.media.MediaSession2.Command;
import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.PlaylistParams;
import android.media.MediaSession2.SessionCallback;
import android.media.TestUtils.SyncHandler;
import android.media.session.PlaybackState;
+import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
+import android.os.ResultReceiver;
import android.support.test.filters.FlakyTest;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
@@ -45,6 +50,7 @@
*/
// TODO(jaewan): Implement host-side test so controller and session can run in different processes.
// TODO(jaewan): Fix flaky failure -- see MediaController2Impl.getController()
+// TODO(jaeawn): Revisit create/close session in the sHandler. It's no longer necessary.
@RunWith(AndroidJUnit4.class)
@SmallTest
@FlakyTest
@@ -60,11 +66,10 @@
public void setUp() throws Exception {
super.setUp();
// Create this test specific MediaSession2 to use our own Handler.
- sHandler.postAndSync(()->{
- mPlayer = new MockPlayer(1);
- mSession = new MediaSession2.Builder(mContext, mPlayer).setId(TAG).build();
- });
-
+ mPlayer = new MockPlayer(1);
+ mSession = new MediaSession2.Builder(mContext, mPlayer)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback())
+ .setId(TAG).build();
mController = createController(mSession.getToken());
TestServiceRegistry.getInstance().setHandler(sHandler);
}
@@ -73,11 +78,9 @@
@Override
public void cleanUp() throws Exception {
super.cleanUp();
- sHandler.postAndSync(() -> {
- if (mSession != null) {
- mSession.close();
- }
- });
+ if (mSession != null) {
+ mSession.close();
+ }
TestServiceRegistry.getInstance().cleanUp();
}
@@ -103,7 +106,6 @@
assertTrue(mPlayer.mPauseCalled);
}
-
@Test
public void testSkipToPrevious() throws InterruptedException {
mController.skipToPrevious();
@@ -138,66 +140,146 @@
}
@Test
+ public void testPrepare() throws InterruptedException {
+ mController.prepare();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mPrepareCalled);
+ }
+
+ @Test
+ public void testFastForward() throws InterruptedException {
+ mController.fastForward();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mFastForwardCalled);
+ }
+
+ @Test
+ public void testRewind() throws InterruptedException {
+ mController.rewind();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mRewindCalled);
+ }
+
+ @Test
+ public void testSeekTo() throws InterruptedException {
+ final long seekPosition = 12125L;
+ mController.seekTo(seekPosition);
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mSeekToCalled);
+ assertEquals(seekPosition, mPlayer.mSeekPosition);
+ }
+
+ @Test
+ public void testSetCurrentPlaylistItem() throws InterruptedException {
+ final int itemIndex = 9;
+ mController.setCurrentPlaylistItem(itemIndex);
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mSetCurrentPlaylistItemCalled);
+ assertEquals(itemIndex, mPlayer.mItemIndex);
+ }
+
+ @Test
+ public void testGetSetPlaylistParams() throws Exception {
+ final PlaylistParams params = new PlaylistParams(
+ PlaylistParams.REPEAT_MODE_ALL,
+ PlaylistParams.SHUFFLE_MODE_ALL,
+ null /* PlaylistMetadata */);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final TestControllerCallbackInterface callback = new TestControllerCallbackInterface() {
+ @Override
+ public void onPlaylistParamsChanged(PlaylistParams givenParams) {
+ TestUtils.equals(params.toBundle(), givenParams.toBundle());
+ latch.countDown();
+ }
+ };
+
+ final MediaController2 controller = createController(mSession.getToken(), true, callback);
+ controller.setPlaylistParams(params);
+
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ TestUtils.equals(params.toBundle(), mSession.getPlaylistParams().toBundle());
+ TestUtils.equals(params.toBundle(), controller.getPlaylistParams().toBundle());
+ }
+
+ @Test
public void testGetPackageName() {
assertEquals(mContext.getPackageName(), mController.getSessionToken().getPackageName());
}
+ // This also tests testGetPlaybackState().
@Test
- public void testGetPlaybackState() throws InterruptedException {
- // TODO(jaewan): add equivalent test later
- /*
- final CountDownLatch latch = new CountDownLatch(1);
- final MediaPlayerBase.PlaybackListener listener = (state) -> {
- assertEquals(PlaybackState.STATE_BUFFERING, state.getState());
- latch.countDown();
- };
- assertNull(mController.getPlaybackState());
- mController.addPlaybackListener(listener, sHandler);
-
- mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_BUFFERING));
- assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- assertEquals(PlaybackState.STATE_BUFFERING, mController.getPlaybackState().getState());
- */
- }
-
- // TODO(jaewan): add equivalent test later
- /*
- @Test
- public void testAddPlaybackListener() throws InterruptedException {
+ public void testControllerCallback_onPlaybackStateChanged() throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(2);
- final MediaPlayerBase.PlaybackListener listener = (state) -> {
- switch ((int) latch.getCount()) {
- case 2:
- assertEquals(PlaybackState.STATE_PLAYING, state.getState());
- break;
- case 1:
- assertEquals(PlaybackState.STATE_PAUSED, state.getState());
- break;
+ final TestControllerCallbackInterface callback = new TestControllerCallbackInterface() {
+ @Override
+ public void onPlaybackStateChanged(PlaybackState2 state) {
+ switch ((int) latch.getCount()) {
+ case 2:
+ assertEquals(PlaybackState.STATE_PLAYING, state.getState());
+ break;
+ case 1:
+ assertEquals(PlaybackState.STATE_PAUSED, state.getState());
+ break;
+ }
+ latch.countDown();
}
- latch.countDown();
};
- mController.addPlaybackListener(listener, sHandler);
- sHandler.postAndSync(()->{
- mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
- mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PAUSED));
- });
+ mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
+ mController = createController(mSession.getToken(), true, callback);
+ mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PAUSED));
assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertEquals(PlaybackState.STATE_PAUSED, mController.getPlaybackState().getState());
}
@Test
- public void testRemovePlaybackListener() throws InterruptedException {
+ public void testSendCustomCommand() throws InterruptedException {
+ // TODO(jaewan): Need to revisit with the permission.
+ final Command testCommand = new Command(MediaSession2.COMMAND_CODE_PLAYBACK_PREPARE);
+ final Bundle testArgs = new Bundle();
+ testArgs.putString("args", "testSendCustomAction");
+
final CountDownLatch latch = new CountDownLatch(1);
- final MediaPlayerBase.PlaybackListener listener = (state) -> {
- fail();
- latch.countDown();
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onCustomCommand(ControllerInfo controller, Command customCommand,
+ Bundle args, ResultReceiver cb) {
+ super.onCustomCommand(controller, customCommand, args, cb);
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(testCommand, customCommand);
+ assertTrue(TestUtils.equals(testArgs, args));
+ assertNull(cb);
+ latch.countDown();
+ }
};
- mController.addPlaybackListener(listener, sHandler);
- mController.removePlaybackListener(listener);
- mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
- assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext, mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback).setId(TAG).build();
+ final MediaController2 controller = createController(mSession.getToken());
+ controller.sendCustomCommand(testCommand, testArgs, null);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
}
- */
@Test
public void testControllerCallback_onConnected() throws InterruptedException {
@@ -275,6 +357,7 @@
final MockPlayer player = new MockPlayer(0);
sessionHandler.postAndSync(() -> {
mSession = new MediaSession2.Builder(mContext, mPlayer)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback())
.setId("testDeadlock").build();
});
final MediaController2 controller = createController(mSession.getToken());
@@ -324,7 +407,6 @@
assertNotNull(token);
assertEquals(mContext.getPackageName(), token.getPackageName());
assertEquals(MockMediaSessionService2.ID, token.getId());
- assertNull(token.getSessionBinder());
assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
}
@@ -462,7 +544,9 @@
sHandler.postAndSync(() -> {
// Recreated session has different session stub, so previously created controller
// shouldn't be available.
- mSession = new MediaSession2.Builder(mContext, mPlayer).setId(id).build();
+ mSession = new MediaSession2.Builder(mContext, mPlayer)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback())
+ .setId(id).build();
});
testNoInteraction();
}
diff --git a/packages/MediaComponents/test/src/android/media/MediaSession2Test.java b/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
index 045dcd5..6b10ccc 100644
--- a/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
+++ b/packages/MediaComponents/test/src/android/media/MediaSession2Test.java
@@ -16,27 +16,37 @@
package android.media;
-import android.media.MediaPlayerBase.PlaybackListener;
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+
+import static android.media.TestUtils.createPlaybackState;
+
+import android.media.MediaPlayerInterface.PlaybackListener;
import android.media.MediaSession2.Builder;
+import android.media.MediaSession2.Command;
import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.PlaylistParams;
import android.media.MediaSession2.SessionCallback;
-import android.media.session.PlaybackState;
-import android.os.Process;
+import android.os.Bundle;
import android.os.Looper;
+import android.os.Process;
+import android.os.ResultReceiver;
import android.support.annotation.NonNull;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
+import android.text.TextUtils;
import java.util.ArrayList;
import org.junit.After;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
-import static android.media.TestUtils.createPlaybackState;
import static org.junit.Assert.*;
/**
@@ -54,19 +64,16 @@
@Override
public void setUp() throws Exception {
super.setUp();
- sHandler.postAndSync(() -> {
- mPlayer = new MockPlayer(0);
- mSession = new MediaSession2.Builder(mContext, mPlayer).build();
- });
+ mPlayer = new MockPlayer(0);
+ mSession = new MediaSession2.Builder(mContext, mPlayer)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback()).build();
}
@After
@Override
public void cleanUp() throws Exception {
super.cleanUp();
- sHandler.postAndSync(() -> {
- mSession.close();
- });
+ mSession.close();
}
@Test
@@ -138,9 +145,57 @@
}
@Test
+ public void testSetPlaylist() throws Exception {
+ final List<MediaItem2> playlist = new ArrayList<>();
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final TestControllerCallbackInterface callback = new TestControllerCallbackInterface() {
+ @Override
+ public void onPlaylistChanged(List<MediaItem2> givenList) {
+ assertMediaItemListEquals(playlist, givenList);
+ latch.countDown();
+ }
+ };
+
+ final MediaController2 controller = createController(mSession.getToken(), true, callback);
+ mSession.setPlaylist(playlist);
+
+ assertTrue(mPlayer.mSetPlaylistCalled);
+ assertMediaItemListEquals(playlist, mPlayer.mPlaylist);
+ assertMediaItemListEquals(playlist, mSession.getPlaylist());
+
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertMediaItemListEquals(playlist, controller.getPlaylist());
+ }
+
+ @Test
+ public void testSetPlaylistParams() throws Exception {
+ final PlaylistParams params = new PlaylistParams(
+ PlaylistParams.REPEAT_MODE_ALL,
+ PlaylistParams.SHUFFLE_MODE_ALL,
+ null /* PlaylistMetadata */);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final TestControllerCallbackInterface callback = new TestControllerCallbackInterface() {
+ @Override
+ public void onPlaylistParamsChanged(PlaylistParams givenParams) {
+ TestUtils.equals(params.toBundle(), givenParams.toBundle());
+ latch.countDown();
+ }
+ };
+
+ final MediaController2 controller = createController(mSession.getToken(), true, callback);
+ mSession.setPlaylistParams(params);
+ assertTrue(mPlayer.mSetPlaylistParamsCalled);
+ TestUtils.equals(params.toBundle(), mPlayer.mPlaylistParams.toBundle());
+ TestUtils.equals(params.toBundle(), mSession.getPlaylistParams().toBundle());
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ // TODO(jaewan): Re-enable test..
+ @Ignore
+ @Test
public void testPlaybackStateChangedListener() throws InterruptedException {
- // TODO(jaewan): Add equivalent tests again
- /*
final CountDownLatch latch = new CountDownLatch(2);
final MockPlayer player = new MockPlayer(0);
final PlaybackListener listener = (state) -> {
@@ -148,45 +203,42 @@
assertNotNull(state);
switch ((int) latch.getCount()) {
case 2:
- assertEquals(PlaybackState.STATE_PLAYING, state.getState());
+ assertEquals(PlaybackState2.STATE_PLAYING, state.getState());
break;
case 1:
- assertEquals(PlaybackState.STATE_PAUSED, state.getState());
+ assertEquals(PlaybackState2.STATE_PAUSED, state.getState());
break;
case 0:
fail();
}
latch.countDown();
};
- player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
+ player.notifyPlaybackState(createPlaybackState(PlaybackState2.STATE_PLAYING));
sHandler.postAndSync(() -> {
- mSession.addPlaybackListener(listener, sHandler);
+ mSession.addPlaybackListener(sHandlerExecutor, listener);
// When the player is set, listeners will be notified about the player's current state.
mSession.setPlayer(player);
});
- player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PAUSED));
+ player.notifyPlaybackState(createPlaybackState(PlaybackState2.STATE_PAUSED));
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
- */
}
@Test
public void testBadPlayer() throws InterruptedException {
// TODO(jaewan): Add equivalent tests again
- /*
final CountDownLatch latch = new CountDownLatch(3); // expected call + 1
final BadPlayer player = new BadPlayer(0);
sHandler.postAndSync(() -> {
- mSession.addPlaybackListener((state) -> {
+ mSession.addPlaybackListener(sHandlerExecutor, (state) -> {
// This will be called for every setPlayer() calls, but no more.
assertNull(state);
latch.countDown();
- }, sHandler);
+ });
mSession.setPlayer(player);
mSession.setPlayer(mPlayer);
});
- player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PAUSED));
+ player.notifyPlaybackState(createPlaybackState(PlaybackState2.STATE_PAUSED));
assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- */
}
private static class BadPlayer extends MockPlayer {
@@ -241,6 +293,47 @@
waitForDisconnect(controller, true);
}
+ @Test
+ public void testSendCustomAction() throws InterruptedException {
+ final Command testCommand = new Command(MediaSession2.COMMAND_CODE_PLAYBACK_PREPARE);
+ final Bundle testArgs = new Bundle();
+ testArgs.putString("args", "testSendCustomAction");
+
+ final CountDownLatch latch = new CountDownLatch(2);
+ final TestControllerCallbackInterface callback = new TestControllerCallbackInterface() {
+ @Override
+ public void onCustomCommand(Command command, Bundle args, ResultReceiver receiver) {
+ assertEquals(testCommand, command);
+ assertTrue(TestUtils.equals(testArgs, args));
+ assertNull(receiver);
+ latch.countDown();
+ }
+ };
+ final MediaController2 controller =
+ createController(mSession.getToken(), true, callback);
+ // TODO(jaewan): Test with multiple controllers
+ mSession.sendCustomCommand(testCommand, testArgs);
+
+ ControllerInfo controllerInfo = getTestControllerInfo();
+ assertNotNull(controllerInfo);
+ // TODO(jaewan): Test receivers as well.
+ mSession.sendCustomCommand(controllerInfo, testCommand, testArgs, null);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ private ControllerInfo getTestControllerInfo() {
+ List<ControllerInfo> controllers = mSession.getConnectedControllers();
+ assertNotNull(controllers);
+ final String packageName = mContext.getPackageName();
+ for (int i = 0; i < controllers.size(); i++) {
+ if (TextUtils.equals(packageName, controllers.get(i).getPackageName())) {
+ return controllers.get(i);
+ }
+ }
+ fail("Fails to get custom command");
+ return null;
+ }
+
public class MockOnConnectCallback extends SessionCallback {
@Override
public MediaSession2.CommandGroup onConnect(ControllerInfo controllerInfo) {
@@ -270,4 +363,28 @@
return true;
}
}
+
+ private static void assertMediaItemListEquals(List<MediaItem2> a, List<MediaItem2> b) {
+ if (a == null || b == null) {
+ assertEquals(a, b);
+ }
+ assertEquals(a.size(), b.size());
+
+ for (int i = 0; i < a.size(); i++) {
+ MediaItem2 aItem = a.get(i);
+ MediaItem2 bItem = b.get(i);
+
+ if (aItem == null || bItem == null) {
+ assertEquals(aItem, bItem);
+ continue;
+ }
+
+ assertEquals(aItem.getMediaId(), bItem.getMediaId());
+ assertEquals(aItem.getFlags(), bItem.getFlags());
+ TestUtils.equals(aItem.getMetadata().getBundle(), bItem.getMetadata().getBundle());
+
+ // Note: Here it does not check whether DataSourceDesc are equal,
+ // since there DataSourceDec is not comparable.
+ }
+ }
}
diff --git a/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java b/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java
index 7834a42..8e1c782 100644
--- a/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java
+++ b/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java
@@ -21,13 +21,16 @@
import android.content.Context;
import android.media.MediaController2.ControllerCallback;
+import android.media.MediaSession2.Command;
import android.media.MediaSession2.CommandGroup;
import android.os.Bundle;
import android.os.HandlerThread;
+import android.os.ResultReceiver;
import android.support.annotation.CallSuper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.test.InstrumentationRegistry;
+
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
@@ -57,10 +60,14 @@
}
interface TestControllerCallbackInterface {
- // Currently empty. Add methods in ControllerCallback/BrowserCallback that you want to test.
+ // Add methods in ControllerCallback/BrowserCallback that you want to test.
+ default void onPlaylistChanged(List<MediaItem2> playlist) {}
+ default void onPlaylistParamsChanged(MediaSession2.PlaylistParams params) {}
- // Browser specific callbacks
- default void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {}
+ // Currently empty. Add methods in ControllerCallback/BrowserCallback that you want to test.
+ default void onPlaybackStateChanged(PlaybackState2 state) { }
+
+ default void onCustomCommand(Command command, Bundle args, ResultReceiver receiver) {}
}
interface WaitForConnectionInterface {
@@ -146,7 +153,10 @@
}
TestControllerInterface onCreateController(@NonNull SessionToken2 token,
- @NonNull TestControllerCallbackInterface callback) {
+ @Nullable TestControllerCallbackInterface callback) {
+ if (callback == null) {
+ callback = new TestControllerCallbackInterface() {};
+ }
return new TestMediaController(mContext, token, new TestControllerCallback(callback));
}
@@ -156,7 +166,10 @@
public final CountDownLatch connectLatch = new CountDownLatch(1);
public final CountDownLatch disconnectLatch = new CountDownLatch(1);
- TestControllerCallback(TestControllerCallbackInterface callbackProxy) {
+ TestControllerCallback(@NonNull TestControllerCallbackInterface callbackProxy) {
+ if (callbackProxy == null) {
+ throw new IllegalArgumentException("Callback proxy shouldn't be null. Test bug");
+ }
mCallbackProxy = callbackProxy;
}
@@ -175,6 +188,18 @@
}
@Override
+ public void onPlaybackStateChanged(PlaybackState2 state) {
+ super.onPlaybackStateChanged(state);
+ mCallbackProxy.onPlaybackStateChanged(state);
+ }
+
+ @Override
+ public void onCustomCommand(Command command, Bundle args, ResultReceiver receiver) {
+ super.onCustomCommand(command, args, receiver);
+ mCallbackProxy.onCustomCommand(command, args, receiver);
+ }
+
+ @Override
public void waitForConnect(boolean expect) throws InterruptedException {
if (expect) {
assertTrue(connectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
@@ -191,6 +216,20 @@
assertFalse(disconnectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
}
+
+ @Override
+ public void onPlaylistChanged(List<MediaItem2> params) {
+ if (mCallbackProxy != null) {
+ mCallbackProxy.onPlaylistChanged(params);
+ }
+ }
+
+ @Override
+ public void onPlaylistParamsChanged(MediaSession2.PlaylistParams params) {
+ if (mCallbackProxy != null) {
+ mCallbackProxy.onPlaylistParamsChanged(params);
+ }
+ }
}
public class TestMediaController extends MediaController2 implements TestControllerInterface {
diff --git a/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java b/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java
index 192cbc2..d0106fa 100644
--- a/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java
+++ b/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java
@@ -59,9 +59,8 @@
// Specify TAG here so {@link MediaSession2.getInstance()} doesn't complaint about
// per test thread differs across the {@link MediaSession2} with the same TAG.
final MockPlayer player = new MockPlayer(1);
- sHandler.postAndSync(() -> {
- mSession = new MediaSession2.Builder(mContext, player).setId(TAG).build();
- });
+ mSession = new MediaSession2.Builder(mContext, player)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback()).setId(TAG).build();
ensureChangeInSession();
}
@@ -70,9 +69,7 @@
public void cleanUp() throws Exception {
super.cleanUp();
sHandler.removeCallbacksAndMessages(null);
- sHandler.postAndSync(() -> {
- mSession.close();
- });
+ mSession.close();
}
// TODO(jaewan): Make this host-side test to see per-user behavior.
@@ -88,7 +85,6 @@
SessionToken2 token = tokens.get(i);
if (mContext.getPackageName().equals(token.getPackageName())
&& TAG.equals(token.getId())) {
- assertNotNull(token.getSessionBinder());
assertNull(controller);
controller = createController(token);
}
@@ -166,13 +162,11 @@
&& MockMediaSessionService2.ID.equals(token.getId())) {
assertFalse(foundTestSessionService);
assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
- assertNull(token.getSessionBinder());
foundTestSessionService = true;
} else if (mContext.getPackageName().equals(token.getPackageName())
&& MockMediaLibraryService2.ID.equals(token.getId())) {
assertFalse(foundTestLibraryService);
assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, token.getType());
- assertNull(token.getSessionBinder());
foundTestLibraryService = true;
}
}
diff --git a/packages/MediaComponents/test/src/android/media/MockMediaLibraryService2.java b/packages/MediaComponents/test/src/android/media/MockMediaLibraryService2.java
index 14cf257..c1187c2 100644
--- a/packages/MediaComponents/test/src/android/media/MockMediaLibraryService2.java
+++ b/packages/MediaComponents/test/src/android/media/MockMediaLibraryService2.java
@@ -18,6 +18,8 @@
import static junit.framework.Assert.fail;
+import static org.junit.Assert.assertEquals;
+
import android.content.Context;
import android.media.MediaSession2.CommandGroup;
import android.media.MediaSession2.ControllerInfo;
@@ -70,9 +72,9 @@
public static SessionToken2 getToken(Context context) {
synchronized (MockMediaLibraryService2.class) {
if (sToken == null) {
- sToken = new SessionToken2(SessionToken2.TYPE_LIBRARY_SERVICE,
- context.getPackageName(), ID,
- MockMediaLibraryService2.class.getName(), null);
+ sToken = new SessionToken2(context, context.getPackageName(),
+ MockMediaLibraryService2.class.getName());
+ assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, sToken.getType());
}
return sToken;
}
diff --git a/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java b/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java
index b058117..5c5c7d2 100644
--- a/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java
+++ b/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java
@@ -28,6 +28,8 @@
import android.media.session.PlaybackState;
import android.os.Process;
+import java.util.concurrent.Executor;
+
/**
* Mock implementation of {@link android.media.MediaSessionService2} for testing.
*/
@@ -46,11 +48,12 @@
public MediaSession2 onCreateSession(String sessionId) {
final MockPlayer player = new MockPlayer(1);
final SyncHandler handler = (SyncHandler) TestServiceRegistry.getInstance().getHandler();
+ final Executor executor = (runnable) -> handler.post(runnable);
try {
handler.postAndSync(() -> {
mSession = new MediaSession2.Builder(MockMediaSessionService2.this, player)
- .setId(sessionId).setSessionCallback((runnable)->handler.post(runnable),
- new MySessionCallback()).build();
+ .setSessionCallback(executor, new MySessionCallback())
+ .setId(sessionId).build();
});
} catch (InterruptedException e) {
fail(e.toString());
diff --git a/packages/MediaComponents/test/src/android/media/MockPlayer.java b/packages/MediaComponents/test/src/android/media/MockPlayer.java
index ad7ba2f..1faf0f4 100644
--- a/packages/MediaComponents/test/src/android/media/MockPlayer.java
+++ b/packages/MediaComponents/test/src/android/media/MockPlayer.java
@@ -26,9 +26,9 @@
import java.util.concurrent.Executor;
/**
- * A mock implementation of {@link MediaPlayerBase} for testing.
+ * A mock implementation of {@link MediaPlayerInterface} for testing.
*/
-public class MockPlayer extends MediaPlayerBase {
+public class MockPlayer implements MediaPlayerInterface {
public final CountDownLatch mCountDownLatch;
public boolean mPlayCalled;
@@ -36,8 +36,22 @@
public boolean mStopCalled;
public boolean mSkipToPreviousCalled;
public boolean mSkipToNextCalled;
+ public boolean mPrepareCalled;
+ public boolean mFastForwardCalled;
+ public boolean mRewindCalled;
+ public boolean mSeekToCalled;
+ public long mSeekPosition;
+ public boolean mSetCurrentPlaylistItemCalled;
+ public int mItemIndex;
+ public boolean mSetPlaylistCalled;
+ public boolean mSetPlaylistParamsCalled;
+
public List<PlaybackListenerHolder> mListeners = new ArrayList<>();
+ public List<MediaItem2> mPlaylist;
+ public PlaylistParams mPlaylistParams;
+
private PlaybackState2 mLastPlaybackState;
+ private AudioAttributes mAudioAttributes;
public MockPlayer(int count) {
mCountDownLatch = (count > 0) ? new CountDownLatch(count) : null;
@@ -83,7 +97,47 @@
}
}
+ @Override
+ public void prepare() {
+ mPrepareCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+ @Override
+ public void fastForward() {
+ mFastForwardCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void rewind() {
+ mRewindCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void seekTo(long pos) {
+ mSeekToCalled = true;
+ mSeekPosition = pos;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void setCurrentPlaylistItem(int index) {
+ mSetCurrentPlaylistItemCalled = true;
+ mItemIndex = index;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
@Nullable
@Override
@@ -112,33 +166,43 @@
}
}
- // No-op. Should be added for test later.
@Override
- public void prepare() {
+ public void setPlaylistParams(PlaylistParams params) {
+ mSetPlaylistParamsCalled = true;
+ mPlaylistParams = params;
}
@Override
- public void seekTo(long pos) {
+ public void addPlaylistItem(int index, MediaItem2 item) {
}
@Override
- public void fastFoward() {
+ public void removePlaylistItem(MediaItem2 item) {
}
@Override
- public void rewind() {
+ public PlaylistParams getPlaylistParams() {
+ return mPlaylistParams;
+ }
+
+ @Override
+ public void setAudioAttributes(AudioAttributes attributes) {
+ mAudioAttributes = attributes;
}
@Override
public AudioAttributes getAudioAttributes() {
- return null;
+ return mAudioAttributes;
}
@Override
- public void setPlaylist(List<MediaItem2> item, PlaylistParams param) {
+ public void setPlaylist(List<MediaItem2> playlist) {
+ mSetPlaylistCalled = true;
+ mPlaylist = playlist;
}
@Override
- public void setCurrentPlaylistItem(int index) {
+ public List<MediaItem2> getPlaylist() {
+ return mPlaylist;
}
}
diff --git a/packages/MediaComponents/test/src/android/media/PlaybackListenerHolder.java b/packages/MediaComponents/test/src/android/media/PlaybackListenerHolder.java
index 4e19d4d..0f1644c 100644
--- a/packages/MediaComponents/test/src/android/media/PlaybackListenerHolder.java
+++ b/packages/MediaComponents/test/src/android/media/PlaybackListenerHolder.java
@@ -16,10 +16,8 @@
package android.media;
-import android.media.MediaPlayerBase.PlaybackListener;
-import android.media.session.PlaybackState;
+import android.media.MediaPlayerInterface.PlaybackListener;
import android.os.Handler;
-import android.os.Message;
import android.support.annotation.NonNull;
import java.util.List;
diff --git a/services/audioflinger/AudioFlinger.cpp b/services/audioflinger/AudioFlinger.cpp
index e362530..592273e 100644
--- a/services/audioflinger/AudioFlinger.cpp
+++ b/services/audioflinger/AudioFlinger.cpp
@@ -1976,6 +1976,43 @@
return NO_ERROR;
}
+status_t AudioFlinger::getMicrophones(std::vector<media::MicrophoneInfo> *microphones)
+{
+ // Fake data
+ size_t fakeNum = 2;
+ audio_devices_t fakeTypes[] = { AUDIO_DEVICE_IN_BUILTIN_MIC, AUDIO_DEVICE_IN_BACK_MIC };
+ for (size_t i = 0; i < fakeNum; i++) {
+ struct audio_microphone_characteristic_t characteristics;
+ sprintf(characteristics.device_id, "microphone:%zu", i);
+ characteristics.type = fakeTypes[i];
+ sprintf(characteristics.address, "");
+ characteristics.location = AUDIO_MICROPHONE_LOCATION_MAINBODY;
+ characteristics.group = 0;
+ characteristics.index_in_the_group = i;
+ characteristics.sensitivity = 1.0f;
+ characteristics.max_spl = 100.0f;
+ characteristics.min_spl = 0.0f;
+ characteristics.directionality = AUDIO_MICROPHONE_DIRECTIONALITY_OMNI;
+ characteristics.num_frequency_responses = 5 - i;
+ for (size_t j = 0; j < characteristics.num_frequency_responses; j++) {
+ characteristics.frequency_responses[0][j] = 100.0f - j;
+ characteristics.frequency_responses[1][j] = 100.0f + j;
+ }
+ for (size_t j = 0; j < AUDIO_CHANNEL_COUNT_MAX; j++) {
+ characteristics.channel_mapping[j] = AUDIO_MICROPHONE_CHANNEL_MAPPING_UNUSED;
+ }
+ characteristics.geometric_location.x = 0.1f;
+ characteristics.geometric_location.y = 0.2f;
+ characteristics.geometric_location.z = 0.3f;
+ characteristics.orientation.x = 0.0f;
+ characteristics.orientation.y = 1.0f;
+ characteristics.orientation.z = 0.0f;
+ media::MicrophoneInfo microphoneInfo = media::MicrophoneInfo(characteristics);
+ microphones->push_back(microphoneInfo);
+ }
+ return NO_ERROR;
+}
+
// setAudioHwSyncForSession_l() must be called with AudioFlinger::mLock held
void AudioFlinger::setAudioHwSyncForSession_l(PlaybackThread *thread, audio_session_t sessionId)
{
diff --git a/services/audioflinger/AudioFlinger.h b/services/audioflinger/AudioFlinger.h
index 296f092..83caca7 100644
--- a/services/audioflinger/AudioFlinger.h
+++ b/services/audioflinger/AudioFlinger.h
@@ -250,6 +250,8 @@
/* Indicate JAVA services are ready (scheduling, power management ...) */
virtual status_t systemReady();
+ virtual status_t getMicrophones(std::vector<media::MicrophoneInfo> *microphones);
+
virtual status_t onTransact(
uint32_t code,
const Parcel& data,
diff --git a/services/camera/libcameraservice/CameraService.cpp b/services/camera/libcameraservice/CameraService.cpp
index 536b54d..e7609ed 100644
--- a/services/camera/libcameraservice/CameraService.cpp
+++ b/services/camera/libcameraservice/CameraService.cpp
@@ -2054,7 +2054,8 @@
mp->prepare();
} else {
ALOGE("Failed to load CameraService sounds: %s", file);
- return NULL;
+ delete mp;
+ return nullptr;
}
return mp;
}
@@ -2066,9 +2067,19 @@
LOG1("CameraService::loadSound ref=%d", mSoundRef);
if (mSoundRef++) return;
- mSoundPlayer[SOUND_SHUTTER] = newMediaPlayer("/system/media/audio/ui/camera_click.ogg");
- mSoundPlayer[SOUND_RECORDING_START] = newMediaPlayer("/system/media/audio/ui/VideoRecord.ogg");
- mSoundPlayer[SOUND_RECORDING_STOP] = newMediaPlayer("/system/media/audio/ui/VideoStop.ogg");
+ mSoundPlayer[SOUND_SHUTTER] = newMediaPlayer("/product/media/audio/ui/camera_click.ogg");
+ if (mSoundPlayer[SOUND_SHUTTER] == nullptr) {
+ mSoundPlayer[SOUND_SHUTTER] = newMediaPlayer("/system/media/audio/ui/camera_click.ogg");
+ }
+ mSoundPlayer[SOUND_RECORDING_START] = newMediaPlayer("/product/media/audio/ui/VideoRecord.ogg");
+ if (mSoundPlayer[SOUND_RECORDING_START] == nullptr) {
+ mSoundPlayer[SOUND_RECORDING_START] =
+ newMediaPlayer("/system/media/audio/ui/VideoRecord.ogg");
+ }
+ mSoundPlayer[SOUND_RECORDING_STOP] = newMediaPlayer("/product/media/audio/ui/VideoStop.ogg");
+ if (mSoundPlayer[SOUND_RECORDING_STOP] == nullptr) {
+ mSoundPlayer[SOUND_RECORDING_STOP] = newMediaPlayer("/system/media/audio/ui/VideoStop.ogg");
+ }
}
void CameraService::releaseSound() {