/*
ChromeCastMediaPlayerProfile.java
Copyright (c) 2014 NTT DOCOMO,INC.
Released under the MIT license
http://opensource.org/licenses/mit-license.php
*/
package org.deviceconnect.android.deviceplugin.chromecast.profile;
import android.Manifest;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import com.google.android.gms.cast.MediaStatus;
import org.deviceconnect.android.activity.PermissionUtility;
import org.deviceconnect.android.deviceplugin.chromecast.ChromeCastService;
import org.deviceconnect.android.deviceplugin.chromecast.core.ChromeCastDiscovery;
import org.deviceconnect.android.deviceplugin.chromecast.core.ChromeCastHttpServer;
import org.deviceconnect.android.deviceplugin.chromecast.core.ChromeCastMediaPlayer;
import org.deviceconnect.android.deviceplugin.chromecast.core.MediaFile;
import org.deviceconnect.android.event.EventError;
import org.deviceconnect.android.event.EventManager;
import org.deviceconnect.android.message.MessageUtils;
import org.deviceconnect.android.profile.MediaPlayerProfile;
import org.deviceconnect.android.profile.api.DConnectApi;
import org.deviceconnect.android.profile.api.DeleteApi;
import org.deviceconnect.android.profile.api.GetApi;
import org.deviceconnect.android.profile.api.PutApi;
import org.deviceconnect.message.DConnectMessage;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* MediaPlayer プロファイル (Chromecast).
* <p>
* Chromecastのメディア操作を提供する
* </p>
* @author NTT DOCOMO, INC.
*/
public class ChromeCastMediaPlayerProfile extends MediaPlayerProfile {
/** イベントの登録・解除に失敗したときのエラーコード. */
private static final int ERROR_VALUE_IS_NULL = 100;
private static final String SAMPLE_URL = "https://github.com/DeviceConnect/DeviceConnect-Android/wiki/sphero_demo.MOV";
/** 1ミリ秒. */
private static final int MILLISECOND = 1000;
/** Chromecastの再生状態がバッファリング中であることを表す. */
private static final String MESSAGE_BUFFERING = "buffering";
/** Chromecastの再生状態がstop状態であることを表す. */
private static final String MESSAGE_STOP = "stop";
/** Chromecastの再生状態がpause状態であることを表す. */
private static final String MESSAGE_PAUSED = "pause";
/** Chromecastの再生状態がplay状態であることを表す. */
private static final String MESSAGE_PALYING = "play";
/** Chromecastの再生状態がunknownであることを表す. */
private static final String MESSAGE_UNKNOWN = "unknown";
/** Chromecastが有効になっていない時のエラーメッセージ. */
private static final String ERROR_MESSAGE_DEVICE_NOT_ENABLE = "Device is not enable";
/** Chromecastに再生するメディアファイルが設定されていない時のエラーメッセージ. */
private static final String ERROR_MESSAGE_MEDIA_NOT_SELECTED = "Media is not selected";
/** Chromecastの再生状態がない場合のエラーメッセージ. */
private static final String ERROR_MESSAGE_PLAYSTATE_IS_NOT = "Playstate is not";
/** メディアファイルのPlay時のエラーメッセージ. */
private static final String ERROR_MESSAGE_MEDIA_PLAY = ERROR_MESSAGE_PLAYSTATE_IS_NOT
+ " " + MESSAGE_STOP + " or " + MESSAGE_PAUSED;
/** メディアファイルのResume時のエラーメッセージ. */
private static final String ERROR_MESSAGE_MEDIA_RESUME = ERROR_MESSAGE_PLAYSTATE_IS_NOT
+ " " + MESSAGE_PAUSED;
/** メディアファイルのStop時のエラーメッセージ. */
private static final String ERROR_MESSAGE_MEDIA_STOP = ERROR_MESSAGE_PLAYSTATE_IS_NOT
+ " " + MESSAGE_PALYING + " or " + MESSAGE_PAUSED + " or " + MESSAGE_BUFFERING;
/** メディアファイルのPause時のエラーメッセージ. */
private static final String ERROR_MESSAGE_MEDIA_PAUSE = ERROR_MESSAGE_PLAYSTATE_IS_NOT
+ " " + MESSAGE_PALYING;
/** メディアファイルがMute設定時のエラーメッセージ. */
private static final String ERROR_MESSAGE_MEDIA_MUTE = ERROR_MESSAGE_PLAYSTATE_IS_NOT
+ " " + MESSAGE_PALYING + " or " + MESSAGE_PAUSED;
/** メディアファイルのVolume変更時のエラーメッセージ. */
private static final String ERROR_MESSAGE_MEDIA_VOLUME = ERROR_MESSAGE_MEDIA_MUTE;
/** メディアファイルのSeek変更時のエラーメッセージ. */
private static final String ERROR_MESSAGE_MEDIA_SEEK = ERROR_MESSAGE_MEDIA_MUTE;
/** メディア情報の{@link java.util.Comparator}のマップ. */
private static final Map<String, ParamComparator> COMPARATORS = new HashMap<String, ParamComparator>();
/** 比較をしない{@link java.util.Comparator}. */
private static final Comparator<Bundle> NOT_COMPARATOR = new Comparator<Bundle>() {
@Override
public int compare(final Bundle a, final Bundle b) {
return 0;
}
};
static {
COMPARATORS.put(PARAM_TYPE, new ParamComparator(PARAM_TYPE));
COMPARATORS.put(PARAM_LANGUAGE, new ParamComparator(PARAM_LANGUAGE));
COMPARATORS.put(PARAM_MEDIA_ID, new ParamComparator(PARAM_MEDIA_ID));
COMPARATORS.put(PARAM_MIME_TYPE, new ParamComparator(PARAM_MIME_TYPE));
COMPARATORS.put(PARAM_TITLE, new ParamComparator(PARAM_TITLE));
COMPARATORS.put(PARAM_DURATION, new ParamComparator(PARAM_DURATION));
}
public ChromeCastMediaPlayerProfile() {
addApi(mGetMediaApi);
addApi(mPutMediaApi);
addApi(mGetSeekApi);
addApi(mPutSeekApi);
addApi(mGetVolumeApi);
addApi(mPutMuteApi);
addApi(mGetMuteApi);
addApi(mDeleteMuteApi);
addApi(mPutPauseApi);
addApi(mPutPlayApi);
addApi(mPutResumeApi);
addApi(mPutStopApi);
addApi(mPutVolumeApi);
addApi(mPutOnStatusChangeApi);
addApi(mDeleteOnStatusChangeApi);
addApi(mGetMediaListApi);
addApi(mGetPlayStatusApi);
}
/**
* 指定したパラメータに対する{@link java.util.Comparator}を返す.
*
* @param paramName パラメータ名
* @param isAsc 昇順である場合は<code>true</code>、そうでなければ<code>false</code>
* @return {@link java.util.Comparator}
*/
private static Comparator<Bundle> findComparator(final String paramName, final boolean isAsc) {
ParamComparator comparator = COMPARATORS.get(paramName);
if (comparator != null) {
comparator.setOrder(isAsc);
return comparator;
}
return NOT_COMPARATOR;
}
/**
* 再生状態を文字列に変換する.
*
* @param playState 再生状態
* @return 再生状態の文字列を返す
*/
public String getPlayStatus(final int playState) {
switch (playState) {
case MediaStatus.PLAYER_STATE_BUFFERING:
return MESSAGE_BUFFERING;
case MediaStatus.PLAYER_STATE_IDLE:
return MESSAGE_STOP;
case MediaStatus.PLAYER_STATE_PAUSED:
return MESSAGE_PAUSED;
case MediaStatus.PLAYER_STATE_PLAYING:
return MESSAGE_PALYING;
case MediaStatus.PLAYER_STATE_UNKNOWN:
default:
return MESSAGE_UNKNOWN;
}
}
/**
* サービスからChromeCastMediaPlayerを取得する.
* @return ChromeCastMediaPlayer
*/
private ChromeCastMediaPlayer getChromeCastMediaPlayer() {
return ((ChromeCastService) getContext()).getChromeCastMediaPlayer();
}
/**
* サービスからChromeCastDiscoveryrを取得する.
* @return ChromeCastDiscovery
*/
private ChromeCastDiscovery getChromeCastDiscovery() {
return ((ChromeCastService) getContext()).getChromeCastDiscovery();
}
/**
* デバイスが有効か否かを返す<br>.
* デバイスが無効の場合、レスポンスにエラーを設定する
*
* @param response レスポンス
* @param app ChromeCastMediaPlayer
* @return デバイスが有効か否か(有効: true, 無効: false)
*/
private boolean isDeviceEnable(final Intent response, final ChromeCastMediaPlayer app) {
if (!app.isDeviceEnable()) {
MessageUtils.setIllegalDeviceStateError(response, ERROR_MESSAGE_DEVICE_NOT_ENABLE);
return false;
}
return true;
}
/**
* メディアの状態を取得する.
* @param response レスポンス
* @param app ChromeCastMediaPlayer
* @return デバイスが有効か否か(有効: {@link MediaStatus}, 無効: <code>null</code>)
*/
private MediaStatus getMediaStatus(final Intent response, final ChromeCastMediaPlayer app) {
MediaStatus status = app.getMediaStatus();
if (status == null) {
MessageUtils.setIllegalDeviceStateError(response, ERROR_MESSAGE_MEDIA_NOT_SELECTED);
}
return status;
}
private final DConnectApi mPutPlayApi = new PutApi() {
@Override
public String getAttribute() {
return MediaPlayerProfile.ATTRIBUTE_PLAY;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final String serviceId = getServiceID(request);
((ChromeCastService) getContext()).connectChromeCast(serviceId,
new ChromeCastService.Callback() {
@Override
public void onResponse(final boolean connected) {
if (!connected) {
MessageUtils.setIllegalDeviceStateError(response, "The chromecast is not in local network.");
sendResponse(response);
return;
}
ChromeCastMediaPlayer app = getChromeCastMediaPlayer();
if (!isDeviceEnable(response, app)) {
sendResponse(response);
return;
}
MediaStatus status = getMediaStatus(response, app);
if (status == null) {
sendResponse(response);
return;
}
if (status.getPlayerState() == MediaStatus.PLAYER_STATE_PLAYING) {
setResult(response, DConnectMessage.RESULT_OK);
sendResponse(response);
return;
}
if (status.getPlayerState() == MediaStatus.PLAYER_STATE_IDLE) {
app.play(response);
} else if (status.getPlayerState() == MediaStatus.PLAYER_STATE_PAUSED) {
app.resume(response);
} else {
MessageUtils.setIllegalDeviceStateError(response, ERROR_MESSAGE_MEDIA_PLAY);
sendResponse(response);
}
}
});
return false;
}
};
private final DConnectApi mPutResumeApi = new PutApi() {
@Override
public String getAttribute() {
return MediaPlayerProfile.ATTRIBUTE_RESUME;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final String serviceId = getServiceID(request);
((ChromeCastService) getContext()).connectChromeCast(serviceId, new ChromeCastService.Callback() {
@Override
public void onResponse(final boolean connected) {
if (!connected) {
MessageUtils.setIllegalDeviceStateError(response, "The chromecast is not in local network.");
sendResponse(response);
return;
} ChromeCastMediaPlayer app = getChromeCastMediaPlayer();
if (!isDeviceEnable(response, app)) {
sendResponse(response);
return;
}
MediaStatus status = getMediaStatus(response, app);
if (status == null) {
sendResponse(response);
return;
}
if (status.getPlayerState() == MediaStatus.PLAYER_STATE_PLAYING) {
setResult(response, DConnectMessage.RESULT_OK);
sendResponse(response);
return;
}
if (status.getPlayerState() == MediaStatus.PLAYER_STATE_PAUSED) {
app.resume(response);
} else {
MessageUtils.setIllegalDeviceStateError(response, ERROR_MESSAGE_MEDIA_RESUME);
sendResponse(response);
}
}
});
return false;
}
};
private final DConnectApi mPutStopApi = new PutApi() {
@Override
public String getAttribute() {
return MediaPlayerProfile.ATTRIBUTE_STOP;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final String serviceId = getServiceID(request);
((ChromeCastService) getContext()).connectChromeCast(serviceId, new ChromeCastService.Callback() {
@Override
public void onResponse(final boolean connected) {
if (!connected) {
MessageUtils.setIllegalDeviceStateError(response, "The chromecast is not in local network.");
sendResponse(response);
return;
} ChromeCastMediaPlayer app = getChromeCastMediaPlayer();
if (!isDeviceEnable(response, app)) {
sendResponse(response);
return;
}
MediaStatus status = getMediaStatus(response, app);
if (status == null) {
sendResponse(response);
return;
}
if (status.getPlayerState() == MediaStatus.PLAYER_STATE_IDLE) {
setResult(response, DConnectMessage.RESULT_OK);
sendResponse(response);
return;
}
if (status.getPlayerState() == MediaStatus.PLAYER_STATE_PLAYING
|| status.getPlayerState() == MediaStatus.PLAYER_STATE_PAUSED
|| status.getPlayerState() == MediaStatus.PLAYER_STATE_BUFFERING) {
app.stop(response);
} else {
MessageUtils.setIllegalDeviceStateError(response, ERROR_MESSAGE_MEDIA_STOP);
sendResponse(response);
}
}
});
return false;
}
};
private final DConnectApi mPutPauseApi = new PutApi() {
@Override
public String getAttribute() {
return MediaPlayerProfile.ATTRIBUTE_PAUSE;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final String serviceId = getServiceID(request);
((ChromeCastService) getContext()).connectChromeCast(serviceId, new ChromeCastService.Callback() {
@Override
public void onResponse(final boolean connected) {
if (!connected) {
MessageUtils.setIllegalDeviceStateError(response, "The chromecast is not in local network.");
sendResponse(response);
return;
} ChromeCastMediaPlayer app = getChromeCastMediaPlayer();
if (!isDeviceEnable(response, app)) {
sendResponse(response);
return;
}
MediaStatus status = getMediaStatus(response, app);
if (status == null) {
sendResponse(response);
return;
}
if (status.getPlayerState() == MediaStatus.PLAYER_STATE_PAUSED) {
setResult(response, DConnectMessage.RESULT_OK);
sendResponse(response);
return;
}
app.pause(response);
}
});
return false;
}
};
/**
* メディアをミュートする<br/>.
* エラーの場合、レスポンスにエラーを設定する
*
* @param request リクエスト
* @param response レスポンス
* @param serviceId サービスID
* @param mute ミュートするか否か(true: ミュートON, false: ミュートOFF)
* @return result 結果を返す(true: 成功, false: 失敗)
*/
private boolean setMute(final Intent request, final Intent response,
final String serviceId, final boolean mute) {
((ChromeCastService) getContext()).connectChromeCast(serviceId, new ChromeCastService.Callback() {
@Override
public void onResponse(final boolean connected) {
if (!connected) {
MessageUtils.setIllegalDeviceStateError(response, "The chromecast is not in local network.");
sendResponse(response);
return;
}
ChromeCastMediaPlayer app = getChromeCastMediaPlayer();
if (!isDeviceEnable(response, app)) {
sendResponse(response);
return;
}
MediaStatus status = getMediaStatus(response, app);
if (status == null) {
sendResponse(response);
return;
}
app.setMute(response, mute);
}
});
return false;
}
private final DConnectApi mPutMuteApi = new PutApi() {
@Override
public String getAttribute() {
return MediaPlayerProfile.ATTRIBUTE_MUTE;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final String serviceId = getServiceID(request);
return setMute(request, response, serviceId, true);
}
};
private final DConnectApi mDeleteMuteApi = new DeleteApi() {
@Override
public String getAttribute() {
return MediaPlayerProfile.ATTRIBUTE_MUTE;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final String serviceId = getServiceID(request);
return setMute(request, response, serviceId, false);
}
};
private final DConnectApi mGetMuteApi = new GetApi() {
@Override
public String getAttribute() {
return MediaPlayerProfile.ATTRIBUTE_MUTE;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final String serviceId = getServiceID(request);
((ChromeCastService) getContext()).connectChromeCast(serviceId, new ChromeCastService.Callback() {
@Override
public void onResponse(final boolean connected) {
if (!connected) {
MessageUtils.setIllegalDeviceStateError(response, "The chromecast is not in local network.");
sendResponse(response);
return;
}
ChromeCastMediaPlayer app = getChromeCastMediaPlayer();
if (!isDeviceEnable(response, app)) {
sendResponse(response);
return;
}
if (getMediaStatus(response, app) == null) {
sendResponse(response);
return;
}
int mute = app.getMute(response);
if (mute == 1) {
setMute(response, true);
setResult(response, DConnectMessage.RESULT_OK);
sendResponse(response);
} else if (mute == 0) {
setMute(response, false);
setResult(response, DConnectMessage.RESULT_OK);
sendResponse(response);
}
}
});
return false;
}
};
private final DConnectApi mPutVolumeApi = new PutApi() {
@Override
public String getAttribute() {
return MediaPlayerProfile.ATTRIBUTE_VOLUME;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final String serviceId = getServiceID(request);
final Double volume = MediaPlayerProfile.getVolume(request);
((ChromeCastService) getContext()).connectChromeCast(serviceId,
new ChromeCastService.Callback() {
@Override
public void onResponse(final boolean connected) {
if (!connected) {
MessageUtils.setIllegalDeviceStateError(response, "The chromecast is not in local network.");
sendResponse(response);
return;
}
ChromeCastMediaPlayer app = getChromeCastMediaPlayer();
if (!isDeviceEnable(response, app)) {
sendResponse(response);
return;
}
MediaStatus status = getMediaStatus(response, app);
if (status == null) {
sendResponse(response);
return;
}
if (volume == null) {
MessageUtils.setInvalidRequestParameterError(response);
} else if (0.0 > volume || volume > 1.0) {
MessageUtils.setInvalidRequestParameterError(response);
} else {
app.setVolume(response, volume);
return;
}
sendResponse(response);
}
});
return false;
}
};
private final DConnectApi mGetVolumeApi = new GetApi() {
@Override
public String getAttribute() {
return MediaPlayerProfile.ATTRIBUTE_VOLUME;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final String serviceId = getServiceID(request);
((ChromeCastService) getContext()).connectChromeCast(serviceId,
new ChromeCastService.Callback() {
@Override
public void onResponse(final boolean connected) {
if (!connected) {
MessageUtils.setIllegalDeviceStateError(response, "The chromecast is not in local network.");
sendResponse(response);
return;
}
ChromeCastMediaPlayer app = getChromeCastMediaPlayer();
if (!isDeviceEnable(response, app)) {
sendResponse(response);
return;
}
if (getMediaStatus(response, app) == null) {
sendResponse(response);
return;
}
double volume = app.getVolume(response);
if (volume >= 0) {
setVolume(response, volume);
setResult(response, DConnectMessage.RESULT_OK);
sendResponse(response);
} else {
MessageUtils.setIllegalDeviceStateError(response);
sendResponse(response);
}
}
});
return false;
}
};
private final DConnectApi mPutSeekApi = new PutApi() {
@Override
public String getAttribute() {
return MediaPlayerProfile.ATTRIBUTE_SEEK;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final String serviceId = getServiceID(request);
final Integer pos = MediaPlayerProfile.getPos(request);
((ChromeCastService) getContext()).connectChromeCast(serviceId, new ChromeCastService.Callback() {
@Override
public void onResponse(final boolean connected) {
if (!connected) {
MessageUtils.setIllegalDeviceStateError(response, "The chromecast is not in local network.");
sendResponse(response);
return;
}
ChromeCastMediaPlayer app = getChromeCastMediaPlayer();
if (!isDeviceEnable(response, app)) {
sendResponse(response);
return;
}
if (pos == null) {
MessageUtils.setInvalidRequestParameterError(response);
sendResponse(response);
} else {
MediaStatus status = getMediaStatus(response, app);
if (status == null) {
sendResponse(response);
return;
}
long posMillisecond = pos * MILLISECOND;
if (0 > posMillisecond || posMillisecond > status.getMediaInfo().getStreamDuration()) {
MessageUtils.setInvalidRequestParameterError(response);
sendResponse(response);
return;
}
app.setSeek(response, posMillisecond);
}
}
});
return false;
}
};
private final DConnectApi mGetSeekApi = new GetApi() {
@Override
public String getAttribute() {
return MediaPlayerProfile.ATTRIBUTE_SEEK;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final String serviceId = getServiceID(request);
((ChromeCastService) getContext()).connectChromeCast(serviceId,
new ChromeCastService.Callback() {
@Override
public void onResponse(final boolean connected) {
if (!connected) {
MessageUtils.setIllegalDeviceStateError(response, "The chromecast is not in local network.");
sendResponse(response);
return;
}
ChromeCastMediaPlayer app = getChromeCastMediaPlayer();
if (!isDeviceEnable(response, app)) {
sendResponse(response);
return;
}
MediaStatus status = getMediaStatus(response, app);
if (status == null) {
sendResponse(response);
return;
}
long streamPosition = status.getStreamPosition();
long posSecond = app.getSeek(response) / MILLISECOND;
if (posSecond > 0) {
setPos(response, (int) posSecond);
} else if (streamPosition > 0) {
setPos(response, (int) streamPosition);
} else {
setPos(response, 0);
}
setResult(response, DConnectMessage.RESULT_OK);
sendResponse(response);
}
});
return false;
}
};
private final DConnectApi mGetPlayStatusApi = new GetApi() {
@Override
public String getAttribute() {
return MediaPlayerProfile.ATTRIBUTE_PLAY_STATUS;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final String serviceId = getServiceID(request);
((ChromeCastService) getContext()).connectChromeCast(serviceId,
new ChromeCastService.Callback() {
@Override
public void onResponse(final boolean connected) {
if (!connected) {
MessageUtils.setIllegalDeviceStateError(response, "The chromecast is not in local network.");
sendResponse(response);
return;
}
ChromeCastMediaPlayer app = getChromeCastMediaPlayer();
if (!isDeviceEnable(response, app)) {
sendResponse(response);
return;
}
MediaStatus status = getMediaStatus(response, app);
if (status == null) {
sendResponse(response);
return;
}
String playStatus = getPlayStatus(status.getPlayerState());
response.putExtra(MediaPlayerProfile.PARAM_STATUS, playStatus);
setResult(response, DConnectMessage.RESULT_OK);
sendResponse(response);
}
});
return false;
}
};
private final DConnectApi mGetMediaApi = new GetApi() {
@Override
public String getAttribute() {
return MediaPlayerProfile.ATTRIBUTE_MEDIA;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final String serviceId = getServiceID(request);
final String mediaId = MediaPlayerProfile.getMediaId(request);
((ChromeCastService) getContext()).connectChromeCast(serviceId,
new ChromeCastService.Callback() {
@Override
public void onResponse(final boolean connected) {
if (!connected) {
MessageUtils.setIllegalDeviceStateError(response, "The chromecast is not in local network.");
sendResponse(response);
return;
}
if (mediaId == null) {
MessageUtils.setInvalidRequestParameterError(response, "mediaId is null.");
sendResponse(response);
return;
}
if (mediaId.equals("")) {
MessageUtils.setInvalidRequestParameterError(response, "mediaId is empty.");
sendResponse(response);
return;
}
Bundle media = getMedia(mediaId);
for (String key : media.keySet()) {
Object value = media.get(key);
if (value instanceof String) {
response.putExtra(key, (String) value);
} else if (value instanceof String[]) {
response.putExtra(key, (String[]) value);
} else if (value instanceof Long) {
response.putExtra(key, (Long) value);
} else if (value instanceof Bundle) {
response.putExtra(key, (Bundle) value);
}
}
setResult(response, DConnectMessage.RESULT_OK);
sendResponse(response);
}
});
return false;
}
};
/**
* 指定したURIからkeyとvalueに基づき、Cursorを取得する.
*
* @param uri URI
* @param key キー
* @param value バリュー
* @return cursor カーソル
*/
private Cursor getCursorFrom(final Uri uri, final String key, final String value) {
Cursor cursor = getContext().getApplicationContext()
.getContentResolver()
.query(uri, null, key + "=" + value + "", null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
if (cursor.getCount() == 1) {
return cursor;
}
cursor.close();
}
}
return null;
}
/**
* 指定したメディアをローカルサーバ上で公開する.
*
* @param mediaId メディアID
* @return dummyUrl ダミーURL
*/
private String exposeMedia(final int mediaId) {
Uri targetUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
String path = getPathFromUri(ContentUris.withAppendedId(targetUri,
Long.valueOf(mediaId)));
if (path == null) {
return null;
}
ChromeCastHttpServer server = ((ChromeCastService) getContext())
.getChromeCastHttpServer();
return server.exposeFile(new MediaFile(new File(path), null));
}
private final DConnectApi mPutMediaApi = new PutApi() {
@Override
public String getAttribute() {
return MediaPlayerProfile.ATTRIBUTE_MEDIA;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final String serviceId = getServiceID(request);
final String mediaId = MediaPlayerProfile.getMediaId(request);
((ChromeCastService) getContext()).connectChromeCast(serviceId,
new ChromeCastService.Callback() {
@Override
public void onResponse(final boolean connected) {
if (!connected) {
MessageUtils.setIllegalDeviceStateError(response, "The chromecast is not in local network.");
sendResponse(response);
return;
}
if (mediaId == null) {
MessageUtils.setInvalidRequestParameterError(response, "mediaId is null.");
sendResponse(response);
return;
}
if (mediaId.equals("")) {
MessageUtils.setInvalidRequestParameterError(response, "mediaId is empty.");
sendResponse(response);
return;
}
if (!hasMedia(mediaId)) {
MessageUtils.setInvalidRequestParameterError(response, "media is not found.");
sendResponse(response);
return;
}
ChromeCastMediaPlayer app = getChromeCastMediaPlayer();
if (!isDeviceEnable(response, app)) {
sendResponse(response);
return;
}
String url = null;
String title = null;
Integer mId = -1;
try {
mId = Integer.parseInt(mediaId);
} catch (NumberFormatException e) {
url = mediaId;
}
if (url == null) {
Cursor cursor = getCursorFrom(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
MediaStore.Video.Media._ID, mediaId);
try {
if (cursor == null) {
response.putExtra(DConnectMessage.EXTRA_VALUE, "mediaId is not exist");
setResult(response, DConnectMessage.RESULT_ERROR);
sendResponse(response);
return;
} else {
title = cursor.getString(cursor
.getColumnIndex(MediaStore.Video.Media.TITLE));
url = exposeMedia(mId);
if (url == null) {
response.putExtra(DConnectMessage.EXTRA_VALUE, "url is null");
setResult(response, DConnectMessage.RESULT_ERROR);
sendResponse(response);
return;
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
}
if (title == null) {
title = "TITLE";
}
app.load(response, url, title);
}
});
return false;
}
};
/**
* Uriからパスを取得する.
*
* @param mUri Uri
* @return パス
*/
private String getPathFromUri(final Uri mUri) {
String filename = null;
Cursor c = this.getContext().getContentResolver().query(mUri, null, null, null, null);
if (c != null) {
c.moveToFirst();
filename = c.getString(c.getColumnIndex(MediaStore.MediaColumns.DATA));
c.close();
}
return filename;
}
/**
* bundleにメディア情報を設定する.
*
* @param mediaType メディアタイプ
* @param bundle バンドル
* @param cursor カーソル
*/
private void setMediaInformation(final String mediaType, final Bundle bundle,
final Cursor cursor) {
if (mediaType.equals("Video")) {
setType(bundle, mediaType);
setLanguage(bundle, cursor.getString(cursor
.getColumnIndex(MediaStore.Video.Media.LANGUAGE)));
setMediaId(bundle, cursor.getString(cursor
.getColumnIndex(MediaStore.Video.Media._ID)));
setMIMEType(bundle, cursor.getString(cursor
.getColumnIndex(MediaStore.Video.Media.MIME_TYPE)));
setTitle(bundle, cursor.getString(cursor
.getColumnIndex(MediaStore.Video.Media.TITLE)));
int duration = cursor.getInt(cursor
.getColumnIndex(MediaStore.Video.Media.DURATION));
if (duration < 0) {
duration = 0;
}
setDuration(bundle, duration);
Bundle creator = new Bundle();
setCreator(creator, cursor.getString(cursor
.getColumnIndex(MediaStore.Video.Media.ARTIST)));
setCreators(bundle, new Bundle[] {creator});
} else if (mediaType.equals("Audio")) {
setType(bundle, "Music");
setMediaId(bundle, cursor.getString(cursor
.getColumnIndex(MediaStore.Audio.Media._ID)));
setMIMEType(bundle, cursor.getString(cursor
.getColumnIndex(MediaStore.Audio.Media.MIME_TYPE)));
setTitle(bundle, cursor.getString(cursor
.getColumnIndex(MediaStore.Audio.Media.TITLE)));
int duration = cursor.getInt(cursor
.getColumnIndex(MediaStore.Video.Media.DURATION));
if (duration < 0) {
duration = 0;
}
setDuration(bundle, duration);
Bundle creator = new Bundle();
setCreator(creator, cursor.getString(cursor
.getColumnIndex(MediaStore.Audio.Media.ARTIST)));
setRole(creator, cursor.getString(cursor
.getColumnIndex(MediaStore.Audio.Media.COMPOSER)));
setCreators(bundle, new Bundle[] {creator});
} else if (mediaType.equals("Image")) {
setType(bundle, mediaType);
setMediaId(bundle, cursor.getString(cursor
.getColumnIndex(MediaStore.Images.Media._ID)));
setMIMEType(bundle, cursor.getString(cursor
.getColumnIndex(MediaStore.Images.Media.MIME_TYPE)));
setTitle(bundle, cursor.getString(cursor
.getColumnIndex(MediaStore.Images.Media.TITLE)));
Bundle creator = new Bundle();
setCreators(bundle, new Bundle[] {creator});
} else {
setType(bundle, mediaType);
}
}
/**
* メディア情報をリストアップする.
*
* @param mediaType メディアタイプ
* @param list リスト
* @param filter フィルター
* @param orderBy ソートオーダー
*/
private void listupMedia(final String mediaType, final List<Bundle> list, final String filter,
final String orderBy) {
Uri uri = null;
if (mediaType.equals("Video")) {
uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if (mediaType.equals("Audio")) {
uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
} else if (mediaType.equals("Image")) {
uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
}
ContentResolver resolver = this.getContext().getApplicationContext().getContentResolver();
Cursor cursor = resolver.query(uri, null, filter, null, orderBy);
if (cursor != null) {
if (cursor.moveToFirst()) {
do {
Bundle medium = new Bundle();
setMediaInformation(mediaType, medium, cursor);
list.add(medium);
} while (cursor.moveToNext());
}
cursor.close();
}
}
/**
* メディア情報をリストアップする.
*
* @param mediaType メディアタイプ
* @param list リスト
* @param query クエリー
* @param mimeType MIMEタイプ
* @param orders ソートオーダー
*/
private void listupMedia(final String mediaType, final List<Bundle> list, final String query,
final String mimeType, final String[] orders) {
String filter = "";
String orderBy;
if (mimeType != null) {
String key = null;
if (mediaType.equals("Video")) {
key = MediaStore.Video.Media.MIME_TYPE;
} else if (mediaType.equals("Audio")) {
key = MediaStore.Audio.Media.MIME_TYPE;
} else if (mediaType.equals("Image")) {
key = MediaStore.Images.Media.MIME_TYPE;
}
filter = "" + key + "='" + mimeType + "'";
}
if (query != null) {
if (!filter.equals("")) {
filter += " AND ";
}
if (mediaType.equals("Video")) {
filter += "(" + MediaStore.Video.Media.TITLE + " LIKE '%" + query + "%')";
} else if (mediaType.equals("Audio")) {
filter += "(" + MediaStore.Audio.Media.TITLE + " LIKE '%" + query + "%'";
filter += " OR " + MediaStore.Audio.Media.COMPOSER + " LIKE '%" + query + "%')";
} else if (mediaType.equals("Image")) {
filter += "(" + MediaStore.Images.Media.TITLE + " LIKE '%" + query + "%')";
}
}
if (orders != null) {
orderBy = orders[0] + " " + orders[1];
} else {
orderBy = "title asc";
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
final String filt = filter;
final String order = orderBy;
PermissionUtility.requestPermissions(getContext(), new Handler(Looper.getMainLooper()),
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
new PermissionUtility.PermissionRequestCallback() {
@Override
public void onSuccess() {
listupMedia(mediaType, list, filt, order);
}
@Override
public void onFail(@NonNull String deniedPermission) {
}
});
} else {
listupMedia(mediaType, list, filter, orderBy);
}
}
/**
* デバイス内のメディアを全検索する.
* @param query 検索するタイトル
* @param mimeType マイムタイプ
* @param orders ソート
* @return 検索結果
*/
private List<Bundle> findAllMedia(final String query, final String mimeType,
final String[] orders) {
List<Bundle> list = new ArrayList<Bundle>();
listupMedia("Video", list, query, mimeType, orders);
Bundle medium = new Bundle();
setType(medium, "Video");
setLanguage(medium, "Language");
setMediaId(medium, SAMPLE_URL);
setMIMEType(medium, "video/quicktime");
setTitle(medium, "Title: Sample");
setDuration(medium, 9999);
Bundle creatorVideo = new Bundle();
setCreator(creatorVideo, "Creator: Sample");
setCreators(medium, new Bundle[]{creatorVideo});
list.add(medium);
return list;
}
/**
* 指定したメディアの情報を取得する.
* @param mediaId メディアID
* @return メディア情報
*/
private Bundle getMedia(final String mediaId) {
List<Bundle> list = findAllMedia(null, null, null);
for (Bundle b : list) {
String id = b.getString(PARAM_MEDIA_ID);
if (id.equals(mediaId)) {
return b;
}
}
return null;
}
/**
* 指定したメディアを保有しているかどうかをチェックする.
* @param mediaId メディアID
* @return 保有している場合は<code>true</code>、そうでない場合は<code>false</code>
*/
private boolean hasMedia(final String mediaId) {
return getMedia(mediaId) != null;
}
private final DConnectApi mGetMediaListApi = new GetApi() {
@Override
public String getAttribute() {
return MediaPlayerProfile.ATTRIBUTE_MEDIA_LIST;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final String serviceId = getServiceID(request);
final String query = MediaPlayerProfile.getQuery(request);
final String mimeType = MediaPlayerProfile.getMIMEType(request);
final String[] orders = MediaPlayerProfile.getOrder(request);
final Integer offset = MediaPlayerProfile.getOffset(request);
final Integer limit = MediaPlayerProfile.getLimit(request);
((ChromeCastService) getContext()).connectChromeCast(serviceId,
new ChromeCastService.Callback() {
@Override
public void onResponse(final boolean connected) {
if (!connected) {
MessageUtils.setIllegalDeviceStateError(response, "The chromecast is not in local network.");
sendResponse(response);
return;
}
// パラメータの型チェック
Bundle b = request.getExtras();
if (b.getString(PARAM_LIMIT) != null) {
if (parseInteger(b.get(PARAM_LIMIT)) == null) {
MessageUtils.setInvalidRequestParameterError(response);
sendResponse(response);
return;
}
}
if (b.getString(PARAM_OFFSET) != null) {
if (parseInteger(b.get(PARAM_OFFSET)) == null) {
MessageUtils.setInvalidRequestParameterError(response);
sendResponse(response);
return;
}
}
// パラメータの範囲チェック
if (orders != null && orders.length != 2) {
MessageUtils.setInvalidRequestParameterError(response, "order is invalid.");
sendResponse(response);
return;
}
final int offsetValue;
if (offset != null) {
if (offset >= 0) {
offsetValue = offset;
} else {
MessageUtils.setInvalidRequestParameterError(response, "offset is negative.");
sendResponse(response);
return;
}
} else {
offsetValue = 0;
}
if (limit != null && limit < 0) {
MessageUtils.setInvalidRequestParameterError(response, "limit is negative.");
sendResponse(response);
return;
}
Comparator<Bundle> comparator = null;
if (orders != null) {
boolean isAsc;
Order o = Order.getInstance(orders[1]);
switch (o) {
case ASC:
isAsc = true;
break;
case DSEC:
isAsc = false;
break;
default:
MessageUtils.setInvalidRequestParameterError(response, "order is invalid.");
sendResponse(response);
return;
}
comparator = findComparator(orders[0], isAsc);
}
// メディアリストのソート
List<Bundle> foundMedia = findAllMedia(query, mimeType, orders);
if (comparator != null) {
Collections.sort(foundMedia, comparator);
}
final int limitValue = limit != null ? limit : foundMedia.size();
int endIndex = offsetValue + limitValue;
if (endIndex > foundMedia.size()) {
endIndex = foundMedia.size();
}
List<Bundle> result;
if (offsetValue < foundMedia.size()) {
result = foundMedia.subList(offsetValue, endIndex);
} else {
result = new ArrayList<Bundle>();
}
setCount(response, result.size());
setMedia(response, result.toArray(new Bundle[result.size()]));
setResult(response, DConnectMessage.RESULT_OK);
sendResponse(response);
}
});
return false;
}
};
private final DConnectApi mPutOnStatusChangeApi = new PutApi() {
@Override
public String getAttribute() {
return MediaPlayerProfile.ATTRIBUTE_ON_STATUS_CHANGE;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final String serviceId = getServiceID(request);
((ChromeCastService) getContext()).connectChromeCast(serviceId,
new ChromeCastService.Callback() {
@Override
public void onResponse(final boolean connected) {
if (!connected) {
MessageUtils.setIllegalDeviceStateError(response, "The chromecast is not in local network.");
sendResponse(response);
return;
}
EventError error = EventManager.INSTANCE.addEvent(request);
if (error == EventError.NONE) {
((ChromeCastService) getContext()).registerOnStatusChange(response,
serviceId);
} else {
MessageUtils.setError(response, ERROR_VALUE_IS_NULL, "Can not register event.");
sendResponse(response);
}
}
});
return false;
}
};
private final DConnectApi mDeleteOnStatusChangeApi = new DeleteApi() {
@Override
public String getAttribute() {
return MediaPlayerProfile.ATTRIBUTE_ON_STATUS_CHANGE;
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
final String serviceId = getServiceID(request);
((ChromeCastService) getContext()).connectChromeCast(serviceId,
new ChromeCastService.Callback() {
@Override
public void onResponse(final boolean connected) {
if (!connected) {
MessageUtils.setIllegalDeviceStateError(response, "The chromecast is not in local network.");
sendResponse(response);
return;
}
EventError error = EventManager.INSTANCE.removeEvent(request);
if (error == EventError.NONE) {
((ChromeCastService) getContext()).unregisterOnStatusChange(response);
} else {
MessageUtils.setError(response, ERROR_VALUE_IS_NULL, "Can not unregister event.");
sendResponse(response);
}
}
});
return false;
}
};
/**
* パラメータ比較クラス.
*/
private static class ParamComparator implements Comparator<Bundle> {
/** 比較するパラメータ名. */
private final String mParamName;
/** 昇順であることを示すフラグ. */
private boolean mIsAsc;
/**
* コンストラクタ.
*
* @param paramName パラメータ名
*/
ParamComparator(final String paramName) {
mParamName = paramName;
}
/**
* ソート順を設定する.
* @param isAsc 昇順である場合は<code>true</code>、そうでなければ<code>false</code>
*/
public void setOrder(final boolean isAsc) {
mIsAsc = isAsc;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public int compare(final Bundle a, final Bundle b) {
Object va = a.get(mParamName);
Object vb = b.get(mParamName);
if (!mIsAsc) {
Object vt = va;
va = vb;
vb = vt;
}
if (va == null && vb == null) {
return 0;
}
if (va != null && vb == null) {
return -1;
}
if (va == null && vb != null) {
return 1;
}
if (va.getClass() != vb.getClass()) {
return 0;
}
if (va instanceof Comparable && !(vb instanceof Comparable)) {
return -1;
}
if (!(va instanceof Comparable) && vb instanceof Comparable) {
return 1;
}
if (!(va instanceof Comparable) && !(vb instanceof Comparable)) {
return 0;
}
if (va instanceof Comparable && !(vb instanceof Comparable)) {
return -1;
}
if (!(va instanceof Comparable) && vb instanceof Comparable) {
return 1;
}
return ((Comparable) va).compareTo(vb);
}
}
}