package se.chalmers.pd.playlistmanager; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.content.Context; import android.support.v4.app.FragmentActivity; import android.util.Log; import android.widget.Toast; import java.util.ArrayList; /** * This class controls the main flow of the application. It forwards connect requests and messages * to the other classes of the application. A lot of the work that is done is executed * asynchronously so it implements callbacks from the mqtt client and the dialogs * the controller can display. */ public class ApplicationController implements MqttWorker.Callback, DialogFactory.Callback { /** * Callbacks to the owner of the controller that triggers when messages are received. */ public interface Callback { /** * Triggered when the client has received a message. * * @param action the action to perform */ public void onMessageAction(Action action); /** * Triggered when the client has received a message. * * @param action the action to perform * @param t the data that the actions needs to complete its action. This differs as follows; * add: Track t * add_all: ArrayList<Track> t * seek: float t */ public <T extends Object> void onMessageAction(Action action, T t); } private static final String TYPE_DATA = "data"; private static final String TOPIC_PLAYLIST = "/playlist"; private static final String TOPIC_PRIVATE = "/playlist/playlistmanager"; private static final String TOPIC_SENSOR = "/sensor/infotainment"; private static final String TAG = "ApplicationController"; private static final String TRACK_URI = "uri"; private static final String TRACK_NAME = "track"; private static final String TRACK_ARTIST = "artist"; private static final String TRACK_LENGTH = "tracklength"; private String brokerUrl = ""; private MqttWorker mqttWorker; private Context context; private Callback callback; private LoadingDialogFragment connectingDialog; /** * Sets up the basic controls of the class. * * @param context the context that the class is executed in. Used to show dialogs. * @param callback the class that needs information about incoming messages. */ public ApplicationController(Context context, Callback callback) { this.context = context; this.callback = callback; mqttWorker = new MqttWorker(this); } /** * Builds a connect dialog which shows a connecting message to the user. It then * connects the mqtt client to the url. * * @param url the url to connect to */ public synchronized void connect(String url) { connectingDialog = DialogFactory.buildConnectingDialog(context, url); connectingDialog.show(((FragmentActivity) context).getFragmentManager(), "connectingDialog"); brokerUrl = url; mqttWorker.disconnect(); mqttWorker.interrupt(); mqttWorker = new MqttWorker(this); mqttWorker.setUrl(url); mqttWorker.start(); } /** * Called from the mqtt client when the connection has been established or failed. If connection * has failed, it shows a connect dialog to the user. * * @param connected true if connected, false if not. */ @Override public void onConnected(boolean connected) { if (connected) { mqttWorker.subscribe(TOPIC_PLAYLIST); mqttWorker.subscribe(TOPIC_PRIVATE); mqttWorker.subscribe(TOPIC_SENSOR); mqttWorker.publish(TOPIC_PLAYLIST, getAllJsonMessage()); connectingDialog.dismiss(); Log.d(TAG, "Now subscribing to " + TOPIC_PLAYLIST + ", " + TOPIC_PRIVATE + ", " + TOPIC_SENSOR); } else { ((MainActivity) context).runOnUiThread(new Runnable() { @Override public void run() { DialogFactory.buildConnectToUrlDialog(context, ApplicationController.this, brokerUrl, R.string.reconnect_dialog_message).show(); } }); } } /** * Helper method that builds a "get_all" json message which is published * by the mqtt client when the application starts. * * @return a string representation of the message. */ private String getAllJsonMessage() { JSONObject message = new JSONObject(); try { message.put(Action.action.toString(), Action.get_all.toString()); message.put(TYPE_DATA, TOPIC_PRIVATE); } catch (JSONException e) { Log.d(TAG, "Could not create get_all message: " + e.getMessage()); } return message.toString(); } /** * Executed when the user has given an answer in the connect dialog. If the result * is positive, a new connection attempt will be made with the new broker url. If * not, the dialog is simply dismissed. * * @param result true if the user wants to connect, false otherwise * @param newBrokerUrl the new url to connect to */ @Override public void onConnectDialogAnswer(boolean result, String newBrokerUrl) { if (result) { connect(newBrokerUrl); } else { if (connectingDialog != null && connectingDialog.isVisible()) { connectingDialog.dismiss(); } } } /** * Executed from the mqtt client when a message has been received. It checks what * kind of message has come in and triggers callbacks to the Callback implementer. * The actions have different datatypes that are required for their actions and * this data is constructed here. * * @param topic the topic of the imcoming message * @param payload the message body as a json string */ @Override public void onMessage(String topic, String payload) { try { JSONObject json = new JSONObject(payload); String actionString = json.getString(Action.action.toString()); Action action = Action.valueOf(actionString); switch (action) { case add: Track track = jsonToTrack(json); callback.onMessageAction(action, track); break; case add_all: JSONArray trackArray = json.getJSONArray(TYPE_DATA); callback.onMessageAction(action, jsonArrayToTrackList(trackArray)); break; case seek: float position = Float.parseFloat(json.getString(TYPE_DATA)); callback.onMessageAction(action, position); break; default: callback.onMessageAction(action); break; } } catch (JSONException e) { Log.e(TAG, "Could not create json object from payload " + payload + " with error: " + e.getMessage()); } } /** * Converts a json array of tracks to an array list of tracks. * * @param trackArray the json array of tracks * @return a list of Track objects * @throws JSONException if the json object does not meet the Track object required fields. */ private ArrayList<Track> jsonArrayToTrackList(JSONArray trackArray) throws JSONException { ArrayList<Track> playlist = new ArrayList<Track>(); for (int i = 0; i < trackArray.length(); i++) { JSONObject jsonTrack = trackArray.getJSONObject(i); Track track = jsonToTrack(jsonTrack); playlist.add(track); } return playlist; } /** * Converts a json object of a track to a real Track object. * * @param jsonTrack the json object to convert * @return the Track object that has been created * @throws JSONException if the json object does not meet the Track object required fields. */ private Track jsonToTrack(JSONObject jsonTrack) throws JSONException { return new Track(jsonTrack.getString(TRACK_NAME), jsonTrack.getString(TRACK_ARTIST), jsonTrack.optString(TRACK_URI), jsonTrack.optInt(TRACK_LENGTH)); } /** * Called when the user has selected a track from the search results and wish to add it to * the playlist. This method creates the json object and ask's the mqtt client to publish it. * * @param track the track to add to the playlist */ public void addTrack(Track track) { if (mqttWorker != null) { JSONObject message = new JSONObject(); try { message.put(Action.action.toString(), Action.add.toString()); message.put(TRACK_ARTIST, track.getArtist()); message.put(TRACK_NAME, track.getName()); message.put(TRACK_URI, track.getUri()); message.put(TRACK_LENGTH, track.getLength()); mqttWorker.publish(TOPIC_PLAYLIST, message.toString()); } catch (JSONException e) { Log.e(TAG, "Could not create and send json object from track " + track.toString() + " with error: " + e.getMessage()); } } else { Toast.makeText(context, R.string.cant_add_tracks_connect_to_broker, Toast.LENGTH_LONG).show(); } } /** * Tells the mqtt client to perfom an action. These actions are any of; * play, pause, prev, next. * * @param action the action to publish */ public void performAction(Action action) { switch (action) { case play: case pause: case prev: case next: mqttWorker.publish(TOPIC_PLAYLIST, getJsonActionMessage(action.toString())); break; default: break; } } /** * Helper method to build a json message with a given action. * * @param action the action to add to the 'action' field * @return a string representation of the json object */ private String getJsonActionMessage(String action) { JSONObject json = new JSONObject(); try { json.put(Action.action.toString(), action); } catch (JSONException e) { Log.e(TAG, "Could not create and send json object from action " + action + " with error: " + e.getMessage()); } return json.toString(); } /** * Fetches the current brokerUrl and returns it. * * @return the current broker url. */ public String getBrokerUrl() { return brokerUrl; } /** * Called when the user has triggered a 'seek' action. It asks the mqtt client to publish a seek * message with the new position. * * @param position the position to seek to as a fraction number (ex. 0.73) */ public void seek(float position) { JSONObject json = new JSONObject(); try { json.put(Action.action.toString(), Action.seek); json.put(TYPE_DATA, String.valueOf(position)); } catch (JSONException e) { e.printStackTrace(); } mqttWorker.publish(TOPIC_PLAYLIST, json.toString()); } }