com.samsung.trailmix.multiscreen.MultiscreenManager.java Source code

Java tutorial

Introduction

Here is the source code for com.samsung.trailmix.multiscreen.MultiscreenManager.java

Source

/*******************************************************************************
 * Copyright (c) 2015 Samsung Electronics
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *******************************************************************************/

package com.samsung.trailmix.multiscreen;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.net.wifi.WifiManager;

import com.samsung.multiscreen.Channel;
import com.samsung.multiscreen.Client;
import com.samsung.multiscreen.Message;
import com.samsung.multiscreen.Result;
import com.samsung.multiscreen.Search;
import com.samsung.multiscreen.Service;
import com.samsung.multiscreen.util.JSONUtil;
import com.samsung.trailmix.App;
import com.samsung.trailmix.multiscreen.events.AppStateEvent;
import com.samsung.trailmix.multiscreen.events.ConnectionChangedEvent;
import com.samsung.trailmix.multiscreen.events.PlaybackEvent;
import com.samsung.trailmix.multiscreen.events.ServiceChangedEvent;
import com.samsung.trailmix.multiscreen.events.VideoStatusEvent;
import com.samsung.trailmix.multiscreen.model.CurrentStatus;
import com.samsung.trailmix.multiscreen.model.MetaData;
import com.samsung.trailmix.util.Util;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.HashMap;

import de.greenrobot.event.EventBus;

/**
 * Provides the Samsung MultiScreen functions.
 */
public class MultiscreenManager {
    public static final String APP_URL = "http://s3-us-west-1.amazonaws.com/dev-multiscreen-examples/examples/trailmix/tv/index.html";
    public static final String CHANNEL_ID = "com.samsung.trailmix";

    public enum ServiceType {
        // Other unknown type
        Other,
        // Samsung smart TV.
        TV,
        // Samsung smart speaker.
        Speaker
    }

    /**
     * The app state event sent from TV app. Payload is a CurrentStatus model.
     */
    public static final String EVENT_APP_STATE = "appState";

    /**
     *  TV (host) publishes to all clients to report any status changes about the video
     *  (current play head time, or play/pause state). Payload is a CurrentStatus model.
     */
    public static final String EVENT_VIDEO_STATUS = "videoStatus";
    /**
     * Published to all clients when the host begins playing a video.
     */
    public static final String EVENT_VIDEO_START = "videoStart";
    /**
     * Published to all clients when the host finishes playing a video
     */
    public static final String EVENT_VIDEO_END = "videoEnd";

    /**
     * The app state command. The EVENT_APP_STATE event should be called later with app status.
     */
    public static final String CMD_APP_STATE = "appStateRequest";
    /**
     * The command to play video.
     */
    public static final String CMD_PLAY = "play";
    /**
     * The command to pause video.
     */
    public static final String CMD_PAUSE = "pause";
    /**
     * The command to stop video.
     */
    public static final String CMD_STOP = "stop";
    /**
     * The command to resume the paused video.
     */
    public static final String CMD_RESUME = "resume";
    /**
     * The command to seek.
     */
    public static final String CMD_SEEK = "seek";
    /**
     * Request TV (host) to replay the current video from the beginning.
     */
    public static final String CMD_REPLAY = "replay";

    /**
     * An singleton instance of this class
     */
    private static MultiscreenManager instance = null;

    /**
     * A lock used to synchronize creation of this object and access to the service map.
     */
    protected static final Object lock = new Object();

    /**
     * The Search object which is going to run discovery service.
     */
    private Search search = null;

    /**
     * Multiscreen TV service
     */
    private com.samsung.multiscreen.Service service;

    /**
     * Multiscreen TV application
     */
    private com.samsung.multiscreen.Application multiscreenApp;

    /**
     * The array list to hold TV services.
     */
    private ArrayList<Service> serviceList = new ArrayList<>();

    /**
     * Returns the instance.
     *
     * @return
     */
    public static MultiscreenManager getInstance() {
        if (instance == null) {
            synchronized (lock) {
                if (instance == null) {
                    instance = new MultiscreenManager();
                }
            }
        }
        return instance;
    }

    private MultiscreenManager() {
        // Register Wifi state listener.
        registerWiFiStateListener();
    }

    /**
     * Clean up the service.
     */
    public void release() {
        // Unregister the WiFi state listener.
        try {
            App.getInstance().unregisterReceiver(mWifiStateChangedReceiver);
        } catch (Exception e) {//ignore if there is error.
        }

        // Disconnect TV if it is connected.
        disconnect();

        service = null;
    }

    /**
     * Check if discovery process is running.
     *
     * @return true discovery is running otherwise false.
     */
    public boolean isDiscovering() {
        return (search != null && search.isSearching());
    }

    /**
     * Check if the discovery process is stopping.
     * @return true if it is stopping.
     */
    public boolean isStoppingDiscovery() {
        return (search != null && search.isStopping());
    }

    /**
     * start TV discovery.
     */
    public void startDiscovery() {
        Util.d("startDiscovery");

        // Create the search object if it is null.
        if (search == null) {
            search = Service.search(App.getInstance());
            search.setOnServiceFoundListener(mOnServiceFoundListener);
            search.setOnServiceLostListener(mOnServiceLostListener);
        }

        // When WiFi is connected and search process is not running.
        if (Util.isWiFiConnected() && !isDiscovering()) {

            if (!search.isStarting()) {
                // Clear the TV list.
                removeAllServices();

                // Start discovery.
                search.start();
            }
        }
    }

    /**
     * Stop TV discovery.
     */
    public void stopDiscovery() {
        Util.d("stopDiscovery");

        // Stop discovery if search object it not null and it is search.
        if (isDiscovering()) {
            if (!search.isStopping()) {
                try {
                    search.stop();
                } catch (Exception e) { //ignore any error during stop search.
                }
            }
        }
    }

    /**
     * restart a new discovery.
     */
    public void restartDiscovery() {
        if (search == null) {
            startDiscovery();
            return;
        }

        if (isDiscovering()) {
            // Set the listener. Called when search is completely stopped.
            setDiscoveryOnStopListener(new Search.OnStopListener() {
                @Override
                public void onStop() {

                    //Clear the onStopListener.
                    search.setOnStopListener(null);

                    //Start a new discovery.
                    startDiscovery();
                }
            });

            // Start to stop discovery.
            stopDiscovery();
        } else {
            // There is no search process, start a new discovery.
            startDiscovery();
        }
    }

    private Search.OnServiceFoundListener mOnServiceFoundListener = new Search.OnServiceFoundListener() {
        @Override
        public void onFound(Service service) {
            Util.d("Service onFound: " + service);

            // TV is found, update the service list.
            updateService(service);
        }
    };
    private Search.OnServiceLostListener mOnServiceLostListener = new Search.OnServiceLostListener() {
        @Override
        public void onLost(Service service) {
            // Remove the TV from TV list.
            removeService(service);
        }
    };

    /**
     * Get the services found during discovery.
     */
    public ArrayList<Service> getServiceList() {
        return serviceList;
    }

    /**
     * Remove all the services.
     */
    public void removeAllServices() {
        serviceList.clear();

        // Notify UI to update cast icon.
        EventBus.getDefault().post(new ServiceChangedEvent(null, false));
    }

    /**
     * Update the service. If the service has existed, replace it. Otherwise add a new service.
     * @param service the new service.
     */
    public void updateService(Service service) {
        if (service == null) {
            return;
        }

        // Get the service position.
        int position = serviceList.indexOf(service);

        // Check if position is valid.
        if (position >= 0) {
            serviceList.set(position, service);

            // Notify new service is added.
            EventBus.getDefault().post(new ServiceChangedEvent(service, true));
        } else {
            addService(service);
        }
    }

    /**
     * Add a new service into service list.
     * @param service
     */
    public void addService(Service service) {
        if (service == null) {
            return;
        }

        serviceList.add(service);

        // Notify new service is added.
        EventBus.getDefault().post(new ServiceChangedEvent(service, true));
    }

    /**
     * Remove TV from the TV list.
     *
     * @param service
     */
    private void removeService(Service service) {
        if (service == null) {
            return;
        }

        // Remove the service if it exists.
        if (serviceList.remove(service)) {

            // Notify new service is added.
            EventBus.getDefault().post(new ServiceChangedEvent(service, true));
        }
    }

    /**
     * Connect to given service.
     *
     * @param service the service to be connected. disconnect current service if it is null.
     */
    public void connectToService(final Service service) {
        if (service == null) {
            disconnect();
            return;
        }

        if (this.service != null) {

            // Launch the TV App directly if we already got the TV service.
            if (this.service.equals(service)) {

                // Launch the TV app if it is not connected, otherwise do nothing.
                if (!multiscreenApp.isConnected()) {
                    launchApplication();
                }

            } else {

                // If different TV is selected, disconnect the previous application.
                if (multiscreenApp != null && multiscreenApp.isConnected()) {
                    multiscreenApp.disconnect(true, new Result<Client>() {
                        @Override
                        public void onSuccess(Client client) {
                            // disconnect onSuccess, update service.
                            updateServiceAndConnect(service);
                        }

                        @Override
                        public void onError(com.samsung.multiscreen.Error error) {
                            // disconnect failed.
                            Util.e("disconnect onError: " + error.getMessage());

                            // Update service.
                            updateServiceAndConnect(service);
                        }
                    });
                } else {
                    updateServiceAndConnect(service);
                }
            }
        } else {
            //connect to a new TV.
            updateServiceAndConnect(service);
        }
    }

    public Service getConnectedService() {
        return service;
    }

    /**
     * Update the current service and start to launch TV application.
     *
     * @param service the new TV service.
     */
    private void updateServiceAndConnect(Service service) {
        this.service = service;

        //Start to launch TV application.
        if (this.service != null) {
            launchApplication();
        }
    }

    /**
     * Check if TV is connected already.
     *
     * @return true if TV is connected otherwise false.
     */
    public boolean isTVConnected() {
        return service != null && multiscreenApp != null && multiscreenApp.isConnected();
    }

    /**
     * Return the service type of the connected service.
     * @return
     */
    public ServiceType getConnectedServiceType() {
        return getServiceType(this.service);
    }

    /**
     * Return the service type of given service.
     * @param service
     * @return
     */
    public ServiceType getServiceType(Service service) {
        if (service == null) {
            return ServiceType.Other;
        }

        String type = service.getType();
        if (type.toLowerCase().endsWith("speaker")) {
            return ServiceType.Speaker;
        }

        return ServiceType.TV;
    }

    /**
     * Makes connection to the TV and start the application on the TV
     * if the current service is available.
     */
    public void launchApplication() {
        if (service == null) {
            return;
        }

        // Parse Application Url.
        Uri url = Uri.parse(APP_URL);

        // Get an instance of Application.
        multiscreenApp = service.createApplication(url, CHANNEL_ID);

        // Set the connection timeout to 20 seconds.
        // When the TV is unavailable after 20 seconds, onDisconnect event is called.
        multiscreenApp.setConnectionTimeout(20000);

        // Listen for the disconnect event.
        multiscreenApp.setOnDisconnectListener(new Channel.OnDisconnectListener() {
            @Override
            public void onDisconnect(Client client) {
                if (client != null) {

                    // Notify service change listeners.
                    EventBus.getDefault().post(new ConnectionChangedEvent(null));
                }
            }
        });

        // Listen for the connect event
        multiscreenApp.setOnConnectListener(new Channel.OnConnectListener() {
            @Override
            public void onConnect(Client client) {

                // Notify to update UI.
                EventBus.getDefault().post(new ConnectionChangedEvent(null));
            }
        });

        // Listen for the errors.
        multiscreenApp.setOnErrorListener(new Channel.OnErrorListener() {
            @Override
            public void onError(com.samsung.multiscreen.Error error) {
                Util.e("setOnErrorListener: " + error.toString());
                EventBus.getDefault().post(new ConnectionChangedEvent(error.getMessage()));
            }
        });

        // Add message listeners.
        multiscreenApp.addOnMessageListener(EVENT_APP_STATE, onAppStateListener);
        multiscreenApp.addOnMessageListener(EVENT_VIDEO_STATUS, onVideoStatusListener);
        multiscreenApp.addOnMessageListener(EVENT_VIDEO_START, onVideoStartListener);
        multiscreenApp.addOnMessageListener(EVENT_VIDEO_END, onVideoEndListener);

        // Connect and launch the TV application.
        // The timeout is 30 seconds.
        multiscreenApp.connect(null, 30000, new Result<Client>() {

            @Override
            public void onSuccess(Client client) {
            }

            @Override
            public void onError(com.samsung.multiscreen.Error error) {
                Util.e("connect onError: " + error.toString());

                // failed to launch TV application. Notify TV service changes.
                EventBus.getDefault().post(new ConnectionChangedEvent(error.getMessage()));
            }
        });
    }

    /**
     * Disconnect the multiscreen web application.
     */
    public void disconnect() {
        if (isTVConnected()) {
            multiscreenApp.removeOnMessageListeners();
            multiscreenApp.disconnect(true, null);
            service = null;
        }
    }

    /**
     * Set the discovery stop listener called when discovery is stopped.
     *
     * @param listener the listener.
     */
    public void setDiscoveryOnStopListener(Search.OnStopListener listener) {
        if (search != null) {
            search.setOnStopListener(listener);
        }
    }

    /**
     * Get the clients amount.
     * @return
     */
    public int getClientCount() {
        int count = 0;

        if (isTVConnected()) {
            count = multiscreenApp.getClients().size();
        }

        return count;
    }

    //===============================Protocol related code======================================

    /**
     * Send the app state request.
     */
    public void requestAppState() {
        sendToTV(CMD_APP_STATE, null, Message.TARGET_HOST);
    }

    /**
     * Pause the playing video.
     */
    public void pause() {
        sendToTV(CMD_PAUSE, null, Message.TARGET_HOST);
    }

    /**
     * Play the video from beginning.
     */
    public void play(MetaData metaData) {
        if (metaData != null) {
            sendToTV(CMD_PLAY, metaData.getJsonObject(), Message.TARGET_HOST);
        }
    }

    /**
     * Play the video from with given start time.
     * @param time the start time in seconds.
     */
    public void play(MetaData metaData, float time, String state) {
        if (metaData != null) {
            JSONObject jo = metaData.getJsonObject();
            try {
                jo.put("time", time);
                jo.put("state", state);
            } catch (JSONException e) {
            }
            sendToTV(CMD_PLAY, jo, Message.TARGET_HOST);
        }
    }

    /**
     * stop the current playing video.
     */
    public void stop() {
        sendToTV(CMD_STOP, null, Message.TARGET_HOST);
    }

    /**
     * resume the video.
     */
    public void resume() {
        sendToTV(CMD_RESUME, null, Message.TARGET_HOST);
    }

    /**
     * replay the video from beginning.
     */
    public void replay() {
        sendToTV(CMD_REPLAY, null, Message.TARGET_HOST);
    }

    /**
     * seek the current playing video.
     */
    public void seek(float position) {
        Util.d("Seek function is called with position:" + position);
        sendToTV(CMD_SEEK, position, Message.TARGET_HOST);
    }

    /**
     * Receive the response data of app state request.
     */
    private Channel.OnMessageListener onAppStateListener = new Channel.OnMessageListener() {
        @Override
        public void onMessage(Message message) {
            Util.d("onAppStateListener = " + message.toString());

            if (message != null && message.getData() != null) {
                HashMap map = (HashMap) message.getData();
                if (map.containsKey("currentStatus")) {

                    // Read the json string from message map.
                    String jsonString = JSONUtil.toJSONString((HashMap) map.get("currentStatus"));

                    // Broadcast app state event.
                    EventBus.getDefault()
                            .post(new AppStateEvent(CurrentStatus.parse(jsonString, CurrentStatus.class)));
                }
            }
        }
    };

    /**
     * Receive the update of track status.
     */
    private Channel.OnMessageListener onVideoStatusListener = new Channel.OnMessageListener() {
        @Override
        public void onMessage(Message message) {
            Util.d("onVideoStatusListener: " + message.toString());

            if (message != null && message.getData() != null) {

                if (message.getData() instanceof HashMap) {
                    String jsonString = JSONUtil.toJSONString((HashMap) message.getData());

                    EventBus.getDefault()
                            .post(new VideoStatusEvent(CurrentStatus.parse(jsonString, CurrentStatus.class)));
                }
            }
        }
    };

    /**
     * Receive the track start event.
     */
    private Channel.OnMessageListener onVideoStartListener = new Channel.OnMessageListener() {
        @Override
        public void onMessage(Message message) {
            if (message != null && message.getData() != null) {
                EventBus.getDefault().post(new PlaybackEvent(message.getData().toString(), message.getEvent()));
            }
        }
    };

    /**
     * Receive the track end event.
     */
    private Channel.OnMessageListener onVideoEndListener = new Channel.OnMessageListener() {
        @Override
        public void onMessage(Message message) {
            if (message != null && message.getData() != null) {
                EventBus.getDefault().post(new PlaybackEvent(message.getData().toString(), message.getEvent()));
            }
        }
    };

    /**
     * Sent the data to TV.
     *
     * @param event  the channel event.
     * @param data   the object to sent to TV.
     * @param target the target to receive message.
     */
    private void sendToTV(String event, Object data, String target) {
        if (multiscreenApp != null && multiscreenApp.isConnected()) {
            multiscreenApp.publish(event, data, target);
        }
    }

    //===================================WiFi State Monitor===============================

    /**
     * Register network change listeners.
     */
    private void registerWiFiStateListener() {
        IntentFilter mWiFiStateFilter = new IntentFilter();
        mWiFiStateFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
        mWiFiStateFilter.addAction(android.net.ConnectivityManager.CONNECTIVITY_ACTION);
        App.getInstance().registerReceiver(mWifiStateChangedReceiver, mWiFiStateFilter);
    }

    /**
     * Broadcast receiver for network changes.
     */
    BroadcastReceiver mWifiStateChangedReceiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
            if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(intent.getAction())) {
                int extraWifiState = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE,
                        WifiManager.WIFI_STATE_UNKNOWN);

                switch (extraWifiState) {
                case WifiManager.WIFI_STATE_DISABLED:
                case WifiManager.WIFI_STATE_DISABLING:
                    // WiFi is not available.
                    stopDiscovery();

                    // Clear the TV list.
                    removeAllServices();
                    break;
                case WifiManager.WIFI_STATE_ENABLED:
                    // Use ConnectivityManager.CONNECTIVITY_ACTION instead.
                    break;
                }
            } else if (android.net.ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
                // WiFi is connected
                if (Util.isWiFiConnected()) {
                    startDiscovery();
                }
            }
        }
    };
}