/*
* Copyright (C) 2014 Saravan Pantham
*
* 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.jams.music.player.GMusicHelpers;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import org.apache.http.client.HttpClient;
import org.apache.http.cookie.Cookie;
import org.apache.http.entity.ByteArrayEntity;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.Context;
import android.text.TextUtils;
import com.loopj.android.http.PersistentCookieStore;
import com.loopj.android.http.RequestParams;
/*******************************************************************************
* This class contains the MobileClient (preferred) and WebClient (deprecated)
* endpoints and method calls for Google Play Music. Retrieving the song stream
* URL and reordering songs in a playlist are the only operations that should
* be carried out using the WebClient protocol.
*
* @author Saravan Pantham
*******************************************************************************/
public class GMusicClientCalls {
public static Context mContext;
private static PersistentCookieStore mCookieStore;
public static GMusicHttpClient mHttpClient;
private static GMusicClientCalls mInstance;
private static String mWebClientUserAgent;
private static String mAuthToken;
private static String mMobileClientUserAgent = "Android-Music/1301 (t03g JDQ39); gzip";
public static JSONArray mPlaylistEntriesMutationsArray = new JSONArray();
public static void createInstance(Context context) {
getInstance(context);
}
public static GMusicClientCalls getInstance(Context context) {
if(mInstance == null)
mInstance = new GMusicClientCalls(context);
return mInstance;
}
private GMusicClientCalls(Context context) {
mHttpClient = new GMusicHttpClient();
mContext = context;
mCookieStore = new PersistentCookieStore(context.getApplicationContext());
mHttpClient.setCookieStore(mCookieStore);
mHttpClient.setUserAgent("");
}
/*************************************************
* Returns the raw HTTP client behind the custom
* GMusicHTTPClient implementation.
* @return The raw HTTP client object.
*************************************************/
public static final HttpClient getRawHttpClient() {
return mHttpClient.getHttpClient();
}
/******************************************************************
* Sets the HTTP client's authorization header using the specified
* authentication token. The header will be in the following form:
*
* "Authorization", "GoogleLogin auth=xxxxxxxxxxxxxx",
*
* where "xxxxxxxxxxxxxxx" is the authentication token.
*
* @param authToken The authentication token that will be used to
* set the header.
******************************************************************/
public static final void setAuthorizationHeader(String authToken) {
mAuthToken = authToken;
mHttpClient.addHeader("Authorization", "GoogleLogin auth=" + mAuthToken);
}
/************************************************************************
* Resets the current HTTP client object by shutting it down and then
* reinstantiating it. The old cookie store, user agent and authorization
* header will be reused. This process will be done on a separate thread.
************************************************************************/
public static void resetHttpClient() {
//Reset the HTTP Client on a separate thread.
Thread thread = new Thread() {
@Override
public void run() {
if (mHttpClient!=null) {
mHttpClient.getHttpClient().getConnectionManager().shutdown();
mHttpClient = new GMusicHttpClient();
mHttpClient.setCookieStore(mCookieStore);
mHttpClient.setUserAgent(mWebClientUserAgent);
mHttpClient.addHeader("Authorization", "GoogleLogin auth=" + mAuthToken);
}
}
};
thread.start();
}
/*********************************************************
* Sets the user agent for webclient calls.
*
* @param userAgent
*********************************************************/
public static final void setWebClientUserAgent(String userAgent) {
mWebClientUserAgent = userAgent;
mHttpClient.setUserAgent(mWebClientUserAgent);
}
public static final GMusicHttpClient getHttpClient() {
return mHttpClient;
}
/*************************************************
* Loops through the HTTP client's cookie store
* and returns the value of the "xt" cookie.
*
* @return Value of the "xt" cookie.
*************************************************/
private static final String getXtCookieValue() {
for(Cookie cookie : mCookieStore.getCookies()) {
if(cookie.getName().equals("xt"))
return cookie.getValue();
}
return null;
}
/*******************************************************************************
* Attempts to log the user into the "sj" (SkyJam) service using the provided
* authentication token. The authentication token is unique for each session
* and user account. It can be obtained via the GoogleAuthUtil.getToken()
* method. See AsyncGoogleMusicAuthenticationTask.java for the current
* implementation of this process. This method will return true if the login
* process succeeded. Returns false for any other type of failure.
*
* @param context The context that will be used for the login process.
* @param authToken The authentication token that will be used to login.
*******************************************************************************/
public static final boolean login(Context context, String authToken) {
if(!TextUtils.isEmpty(authToken))
{
JSONForm form = new JSONForm().close();
GMusicClientCalls.setAuthorizationHeader(authToken);
String response = mHttpClient.post(context,
"https://play.google.com/music/listen?hl=en&u=0",
new ByteArrayEntity(form.toString().getBytes()),
form.getContentType());
//Check if the required paramters are null.
if (response!=null) {
if (getXtCookieValue()!=null) {
return true;
} else {
return false;
}
} else {
return false;
}
} else {
return false;
}
}
/*****************************************************************
* Gets the URI of the song stream using the specified song Id.
* The URI is dynamically generated by Google's servers and
* expires after one minute of no usage. This method uses the
* WebClient endpoint.
*
* @param songId The id of the song that needs to be streamed.
* @return Returns the URI of the song stream.
* @throws JSONException
* @throws URISyntaxException
******************************************************************/
public static final URI getSongStream(String songId)
throws JSONException, URISyntaxException {
RequestParams params = new RequestParams();
params.put("u", "0");
params.put("songid", songId);
params.put("pt", "e");
String response = mHttpClient.get("https://play.google.com/music/play", params);
if (response!=null) {
JSONObject jsonObject = new JSONObject(response);
return new URI(jsonObject.optString("url", null));
}
return null;
}
/***************************************************************************************
* @deprecated The use of this method is highly discouraged as it sends/fetches large
* amounts of data from Google's servers. All of this data is readily available via the
* Google Play Music app's public ContentProvider. This method uses the WebClient
* endpoint and is a helper method for getSongs().
*
* @param context The context to use during the download process.
* @return
* @throws JSONException
***************************************************************************************/
public static final ArrayList<WebClientSongsSchema> getAllSongs(Context context)
throws JSONException {
return getSongs(context, "");
}
/***************************************************************************************
* <p>
* Queries Google's servers for a list of all songs in the current Google account's
* music library.
* </p>
*
* @deprecated The use of this method is highly discouraged as it sends/fetches large
* amounts of data from Google's servers. All of this data is readily available via the
* Google Play Music app's public ContentProvider. This method uses the WebClient
* endpoint.
*
* @param context The context to use during the download process.
* @param continuationToken The token that will return the next set of songs (only 1000
* songs are returned per request).
* @return
* @throws JSONException
***************************************************************************************/
public static final ArrayList<WebClientSongsSchema> getSongs(Context context,
String continuationToken)
throws JSONException {
JSONForm form = new JSONForm();
form.addField("json", "{\"continuationToken\":\"" + continuationToken + "\"}");
form.close();
String response = mHttpClient.post(context,
"https://play.google.com/music/services/loadalltracks?u=0&xt=" + getXtCookieValue(),
new ByteArrayEntity(form.toString().getBytes()),
form.getContentType());
JSONObject jsonObject = new JSONObject(response);
WebClientPlaylistsSchema playlist = new WebClientPlaylistsSchema().fromJsonObject(jsonObject);
ArrayList<WebClientSongsSchema> chunkedSongList = new ArrayList<WebClientSongsSchema>();
chunkedSongList.addAll(playlist.getPlaylist());
if(!TextUtils.isEmpty(playlist.getContinuationToken())) {
chunkedSongList.addAll(getSongs(context, playlist.getContinuationToken()));
}
return chunkedSongList;
}
/****************************************************************************
* Creates a new, user generated playlist. This method only creates the
* playlist; it does not add songs to the playlist.
*
* @param context The context to use while creating the new playlist.
* @param playlistName The name of the new playlist.
* @return Returns the playlistId of the newly created playlist.
* @throws JSONException
* @throws IllegalArgumentException
****************************************************************************/
public final static String createPlaylist(Context context, String playlistName)
throws JSONException, IllegalArgumentException {
JSONObject jsonParam = new JSONObject();
JSONArray mutationsArray = new JSONArray();
JSONObject createObject = new JSONObject();
createObject.put("lastModifiedTimestamp", "0");
createObject.put("name", playlistName);
createObject.put("creationTimestamp", "-1");
createObject.put("type", "USER_GENERATED");
createObject.put("deleted", false);
mutationsArray.put(new JSONObject().put("create", createObject));
jsonParam.put("mutations", mutationsArray);
mHttpClient.setUserAgent(mMobileClientUserAgent);
String result = mHttpClient.post(context,
"https://www.googleapis.com/sj/v1.1/playlistbatch?alt=json&hl=en_US",
new ByteArrayEntity(jsonParam.toString().getBytes()),
"application/json");
mHttpClient.setUserAgent(mWebClientUserAgent);
return new JSONObject(result).optJSONArray("mutate_response")
.getJSONObject(0).optString("id");
}
/*****************************************************************************
* Creates a JSONObject object that contains the delete command for the
* specified playlist and adds it to the JSONArray that will pass the the
* command on to Google's servers.
*
* @param context The context to use while deleting the playlist.
* @param playlistId The playlistId of the playlist to delete.
* @throws JSONException
* @throws IllegalArgumentException
*****************************************************************************/
public static final String deletePlaylist(Context context, String playlistId)
throws JSONException, IllegalArgumentException {
JSONObject jsonParam = new JSONObject();
JSONArray mutationsArray = new JSONArray();
mutationsArray.put(new JSONObject().put("delete", playlistId));
jsonParam.put("mutations", mutationsArray);
mHttpClient.setUserAgent(mMobileClientUserAgent);
String result = mHttpClient.post(context,
"https://www.googleapis.com/sj/v1.1/playlistbatch?alt=json&hl=en_US",
new ByteArrayEntity(jsonParam.toString().getBytes()),
"application/json");
mHttpClient.setUserAgent(mWebClientUserAgent);
return result;
}
/*****************************************************************************
* Creates a JSONObject object that contains the delete command for the
* specified playlist entry.The object will be added to the JSONArray that
* will be passed on to Google's servers.
*
* @param playlistId The playlistId of the playlist to delete.
* @throws JSONException
* @throws IllegalArgumentException
*****************************************************************************/
public static final void putDeletePlaylistEntryRequest(String playlistEntryId)
throws JSONException, IllegalArgumentException {
JSONObject deleteObject = new JSONObject();
deleteObject.put("delete", playlistEntryId);
mPlaylistEntriesMutationsArray.put(deleteObject);
}
/*****************************************************************************
* Adds the specified JSONObject to mPlaylistEntriesMutationsArray. The added
* JSONObject will be placed under the "create" key. The JSONObject should
* contain valid info about the new playlist entry (song) that will be created.
*
* @param createObject The JSONObject that contains the new playlist entry's
* info and will be placed under the "create" key.
*****************************************************************************/
public static final void putCreatePlaylistEntryRequest(JSONObject createObject)
throws JSONException {
mPlaylistEntriesMutationsArray.put(new JSONObject().put("create", createObject));
}
/*****************************************************************************
* Adds the specified JSONObject to mPlaylistEntriesMutationsArray. The added
* JSONObject will be placed under the "update" key. The JSONObject should
* contain valid info about the playlist entry that is being updated.
*
* @param updateObject The JSONObject that contains the updated playlist entry's
* info and will be placed under the "update" key.
*****************************************************************************/
public static final void putUpdatePlaylistEntryRequest(JSONObject updateObject)
throws JSONException {
mPlaylistEntriesMutationsArray.put(new JSONObject().put("update", updateObject));
}
/******************************************************************************************
* Executes a single/batch modification operation on a playlist's entry(ies). This method
* is a general purpose method that simply hits the MobileClient endpoints using
* mPlaylistEntriesMutationsArray. Supported mutation operations include "create",
* "delete", and "update".
*
* @param context The context to use while carrying out the modification operation.
* @param mutationsArray The JSONArray that contains the mutations command to be
* carried out.
* @return The JSON response as a String.
* @throws JSONException
* @throws IllegalArgumentException
******************************************************************************************/
public static final String modifyPlaylist(Context context)
throws JSONException, IllegalArgumentException {
JSONObject jsonParam = new JSONObject();
jsonParam.put("mutations", mPlaylistEntriesMutationsArray);
mHttpClient.setUserAgent(mMobileClientUserAgent);
String result = mHttpClient.post(context,
"https://www.googleapis.com/sj/v1.1/plentriesbatch?alt=json&hl=en_US",
new ByteArrayEntity(jsonParam.toString().getBytes()),
"application/json");
mHttpClient.setUserAgent(mWebClientUserAgent);
//Clear out and reset the mutationsArray now that we're done using it.
mPlaylistEntriesMutationsArray = null;
mPlaylistEntriesMutationsArray = new JSONArray();
return result;
}
/*******************************************************************************************
* Returns the number of elements in mPlaylistEntriesMutationsArray. Used to check if a
* POST request should be sent to Google's servers.
*******************************************************************************************/
public static int getQueuedMutationsCount() {
return mPlaylistEntriesMutationsArray.length();
}
/*******************************************************************************************
* Sends a POST request to Google's servers and retrieves a JSONArray with all user
* playlists. The JSONArray contains the fields of the playlist such as "id", "name",
* "type", etc. (for a list of all response fields, see MobileClientPlaylistsSchema.java).
*
* @return A JSONArray object that contains all user playlists and their fields.
* @param context The context to use while retrieving user playlists.
*******************************************************************************************/
public static final JSONArray getUserPlaylistsMobileClient(Context context)
throws JSONException, IllegalArgumentException {
JSONObject jsonRequestParams = new JSONObject();
JSONArray playlistsJSONArray = new JSONArray();
jsonRequestParams.put("max-results", 250);
jsonRequestParams.put("start-token", "0");
mHttpClient.setUserAgent(mMobileClientUserAgent);
String result = mHttpClient.post(context,
"https://www.googleapis.com/sj/v1.1/playlistfeed?alt=json&hl=en_US&tier=basic",
new ByteArrayEntity(jsonRequestParams.toString().getBytes()),
"application/json");
JSONObject resultJSONObject = new JSONObject(result);
JSONObject dataJSONObject = new JSONObject();
if (resultJSONObject!=null) {
dataJSONObject = resultJSONObject.optJSONObject("data");
}
if (dataJSONObject!=null) {
playlistsJSONArray = dataJSONObject.getJSONArray("items");
}
return playlistsJSONArray;
}
/******************************************************************************************
* Retrieves a JSONAray with all songs in <i><b>every</b></i> playlist. The JSONArray
* contains the fields of the songs such as "id", "clientId", "trackId", etc. (for a list
* of all fields, see MobileClientPlaylistEntriesSchema.java).
*
* @deprecated This method is fully functional. However, there are issues with retrieving
* the correct playlist entryIds. Specifically, the entryIds do not seem to work with
* reordering playlists via the MobileClient mutations protocol.
*
* @return A JSONArray object that contains all songs and their fields within every playlist.
* @param context The context to use while retrieving songs from the playlist.
******************************************************************************************/
public static final JSONArray getPlaylistEntriesMobileClient(Context context)
throws JSONException, IllegalArgumentException {
JSONArray playlistEntriesJSONArray = new JSONArray();
JSONObject jsonRequestParams = new JSONObject();
jsonRequestParams.put("max-results", 10000);
jsonRequestParams.put("start-token", "0");
mHttpClient.setUserAgent(mMobileClientUserAgent);
String result = mHttpClient.post(context,
"https://www.googleapis.com/sj/v1.1/plentryfeed?alt=json&hl=en_US&tier=basic",
new ByteArrayEntity(jsonRequestParams.toString().getBytes()),
"application/json");
JSONObject resultJSONObject = new JSONObject(result);
JSONObject dataJSONObject = new JSONObject();
if (resultJSONObject!=null) {
dataJSONObject = resultJSONObject.optJSONObject("data");
}
if (dataJSONObject!=null) {
playlistEntriesJSONArray = dataJSONObject.getJSONArray("items");
}
return playlistEntriesJSONArray;
}
/**************************************************************************************************
* Retrieves a JSONAray with all songs within the <b><i>specified</b></i> playlist. The JSONArray
* contains the fields of the songs such as "id", "clientId", "trackId", etc. (for a list
* of all fields, see WebClientSongsSchema.java). Uses the WebClient endpoint.
*
* @return A JSONArray object that contains the songs and their fields within the specified playlist.
* @param context The context to use while retrieving songs from the playlist.
* @param playlistId The id of the playlist we need to fetch the songs from.
**************************************************************************************************/
public static final JSONArray getPlaylistEntriesWebClient(Context context, String playlistId)
throws JSONException, IllegalArgumentException {
JSONObject jsonParam = new JSONObject();
jsonParam.putOpt("id", playlistId);
JSONForm form = new JSONForm();
form.addField("json", jsonParam.toString());
form.close();
mHttpClient.setUserAgent(mMobileClientUserAgent);
String result = mHttpClient.post(context,
"https://play.google.com/music/services/loadplaylist?u=0&xt=" + getXtCookieValue(),
new ByteArrayEntity(form.toString().getBytes()),
form.getContentType());
JSONArray jsonArray = new JSONArray();
JSONObject jsonObject = new JSONObject(result);
if (jsonObject!=null) {
jsonArray = jsonObject.getJSONArray("playlist");
}
return jsonArray;
}
/**************************************************************************************
* Reorders the specified song (within the specified playlist) to a new position.
*
* @param context The context to use during the reordering process.
* @param playlistId The id of the playlist which contains the song to be reordered.
* @param movedSongId The id of the song that is being reordered.
* @param movedEntryId The entryId of the song that is being reordered.
* @param afterEntryId The entryId of the song that is before the new position.
* @param beforeEntryId The entryId of the song that is after the new position.
* @return Returns the JSON response of the reorder task.
* @throws JSONException
**************************************************************************************/
public static final String reorderPlaylistEntryWebClient(Context context,
String playlistId,
ArrayList<String> movedSongId,
ArrayList<String> movedEntryId,
String afterEntryId,
String beforeEntryId) throws JSONException {
JSONObject jsonParam = new JSONObject();
jsonParam.put("playlistId", playlistId);
jsonParam.put("movedSongIds", movedSongId);
jsonParam.put("movedEntryIds", movedEntryId);
jsonParam.put("afterEntryId", afterEntryId);
jsonParam.put("beforeEntryId", beforeEntryId);
String jsonParamString = jsonParam.toString();
jsonParamString = jsonParamString.replace("\"[", "[\"");
jsonParamString = jsonParamString.replace("]\"", "\"]");
JSONForm form = new JSONForm();
form.addField("json", jsonParamString);
form.close();
String result = mHttpClient.post(context,
"https://play.google.com/music/services/changeplaylistorder?u=0&xt=" + getXtCookieValue(),
new ByteArrayEntity(form.toString().getBytes()),
form.getContentType());
return result;
}
}