org.jared.synodroid.ds.server.SynoServer.java Source code

Java tutorial

Introduction

Here is the source code for org.jared.synodroid.ds.server.SynoServer.java

Source

/**
 * Copyright 2010 Eric Taix 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 org.jared.synodroid.ds.server;

import java.io.BufferedReader;
import java.io.EOFException;
import java.io.FileNotFoundException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.security.SecureRandom;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.List;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;

import org.jared.synodroid.ds.Synodroid;
import org.jared.synodroid.ds.protocol.DSMException;
import org.jared.synodroid.ds.protocol.DSMHandlerFactory;
import org.jared.synodroid.ds.protocol.DownloadStationNotFound;
import org.jared.synodroid.ds.protocol.ResponseHandler;
import org.jared.synodroid.ds.protocol.https.AcceptAllHostNameVerifier;
import org.jared.synodroid.ds.protocol.https.AcceptAllTrustManager;
import org.jared.synodroid.ds.utils.GenericException;
import org.jared.synodroid.ds.R;

import org.jared.synodroid.ds.action.AddTaskAction;
import org.jared.synodroid.ds.action.SynoAction;
import org.jared.synodroid.ds.data.DSMVersion;
import org.jared.synodroid.ds.data.SynoProtocol;

import android.os.Message;
import android.support.v4.app.Fragment;
import android.util.Log;

/**
 * This class represents a Synology server. It manages the connection and also the automatic refresh to retrieve the torrent list.
 * 
 * @author Eric Taix (eric.taix at gmail.com)
 */
public class SynoServer extends SimpleSynoServer {

    // The nickname of the server
    private String nickname = "";
    private String serverID = "";
    // Local connection through WIFI
    private SynoServerConnection localConnection;
    // Public connection
    private SynoServerConnection publicConnection;
    // The current connection
    private SynoServerConnection currentConn;

    // The user
    private String user;
    // The password
    private String password;
    // The sort atttribut
    private String sortAttribute;
    // Is the sort ascending
    private boolean ascending;

    // The recurrent action to execute
    private SynoAction recurrentAction = null;

    // Are we connected with the server: login+passwd?
    private boolean connected;
    // Flag to stop the server's collector
    private boolean stop;
    // Flag to pause the thread until it is interrupted
    volatile private boolean pause;
    // The data's collector thread
    private Thread collector;

    // Flag to know is the server has been interrupted while sleeping
    private boolean interrupted;

    // Binded DownloadActivity
    private ResponseHandler handler;

    private String lasterror;

    HashMap<String, String> map = new HashMap<String, String>();

    /**
     * Static intialization of the SSL factory to accept each certificate, even if a certificate is self signed
     */
    static {
        SSLContext sc;
        try {
            sc = SSLContext.getInstance("TLS");
            sc.init(null, new TrustManager[] { new AcceptAllTrustManager() }, new SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
            HttpsURLConnection.setDefaultHostnameVerifier(new AcceptAllHostNameVerifier());
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Constructor which set all server's informations. No connection are made when calling the constructor.
     */
    public SynoServer(String nicknameP, SynoServerConnection localConP, SynoServerConnection publicConnP,
            String userP, String passwordP, boolean debug, boolean autoDetectP, String ID) {
        nickname = nicknameP;
        serverID = ID;
        localConnection = localConP;
        publicConnection = publicConnP;
        user = userP;
        password = passwordP;
        DEBUG = debug;
        autoDetect = autoDetectP;

        // Create the appropriated factory
        dsmFactory = DSMHandlerFactory.getFactory(dsmVersion, this, DEBUG, autoDetect);
        map = ErrorMap.initMap();
    }

    /**
     * Set a new recurrent action. The collector thread is interrupted to executed the new recurrent action immediatly
     * 
     * @param handlerP
     *            The handler which will receive the response
     * @param recurrentActionP
     *            the recurrentAction to set
     */
    public void setRecurrentAction(ResponseHandler handlerP, SynoAction recurrentActionP) {
        bindResponseHandler(handlerP);
        recurrentAction = recurrentActionP;
    }

    /**
     * Connect to the server. It is a requirement to connect to the NAS server before any attempt to call a method of this class.
     * 
     * @return
     * @throws DSMException
     */
    public void connect(final ResponseHandler handlerP, final List<SynoAction> actionQueueP, boolean publicP) {
        // Set the connection according to the public or local parameter
        if (publicP) {
            currentConn = publicConnection;
        } else {
            currentConn = localConnection;
        }

        bindResponseHandler(handlerP);
        // If we are not already connected
        if (!connected) {
            // Everything is OK, so launch the thread
            Runnable runnable = new Runnable() {
                public void run() {
                    try {
                        doConnection(false);
                        // If the action's queue is not empty
                        if (actionQueueP != null) {
                            if (DEBUG)
                                Log.v(Synodroid.DS_TAG, "There are items to execute in the queue...");
                            for (SynoAction taskAction : actionQueueP) {
                                executeAsynchronousAction(handler, taskAction, false);
                            }
                        }
                        // If everything is fine then start to collect informations
                        boolean silentMode = false;
                        while (connected && !stop) {
                            try {
                                // Update the progressbar
                                fireMessage(SynoServer.this.handler, ResponseHandler.MSG_OPERATION_PENDING);
                                // Execute the recurrent action
                                SynoAction toDo = null;
                                synchronized (this) {
                                    if (recurrentAction != null) {
                                        toDo = recurrentAction;
                                    }
                                }
                                if (toDo != null) {
                                    toDo.execute(SynoServer.this.handler, SynoServer.this);
                                }
                                // In case we are disconnected before the response is
                                // received
                                if (connected) {
                                    // If auto refresh
                                    setInterrupted(false);
                                    synchronized (this) {
                                        if (currentConn.autoRefresh) {
                                            // Sleep
                                            wait(currentConn.refreshInterval * 1000);
                                        } else {
                                            wait();
                                        }
                                    }
                                    // If the thread is paused
                                    if (pause) {
                                        silentMode = true;
                                        synchronized (this) {
                                            wait();
                                        }
                                    }
                                }
                            }
                            // Nothing to do. It may be a force refresh after an action!
                            catch (InterruptedException iex) {
                                if (DEBUG)
                                    Log.v(Synodroid.DS_TAG, "Been interrupted while sleeping...");
                                setInterrupted(true);
                            }
                            // All others exceptions
                            catch (Exception ex) {
                                // If not in Silent mode and throws it again
                                if (silentMode) {
                                    doConnection(silentMode);
                                    silentMode = false;
                                } else {
                                    throw ex;
                                }
                            }
                        }
                    }
                    // Connection error
                    catch (DSMException e) {
                        if (DEBUG)
                            Log.e(Synodroid.DS_TAG, "DSMException occured", e);
                        try {
                            fireMessage(SynoServer.this.handler, ResponseHandler.MSG_ERROR,
                                    translateError(SynoServer.this.handler, e));
                        } catch (Exception err) {
                        }
                    }
                    // Download Station not running error
                    catch (DownloadStationNotFound e) {
                        if (DEBUG)
                            Log.e(Synodroid.DS_TAG, "Download Station Not Found Error", e);
                        try {
                            fireMessage(handlerP, ResponseHandler.MSG_ERROR,
                                    SynoServer.this.handler.getString(R.string.download_station_not_found));
                        } catch (Exception err) {
                        }
                    }
                    // Programmation exception
                    catch (Exception e) {
                        if (DEBUG)
                            Log.e(Synodroid.DS_TAG, "Exception occured", e);
                        // This is most likely a connection timeout
                        DSMException ex = new DSMException(e);
                        try {
                            fireMessage(SynoServer.this.handler, ResponseHandler.MSG_ERROR,
                                    translateError(SynoServer.this.handler, ex));
                        } catch (Exception err) {
                        }
                    }
                    // Set the connection to null to force connection next time
                    finally {
                        synchronized (this) {
                            connected = false;
                        }
                        if (DEBUG)
                            Log.v(Synodroid.DS_TAG, "Server forced to reconnect.");
                    }
                }
            };
            collector = new Thread(runnable, "Synodroid DS collector");
            collector.start();
        }
    }

    /**
     * Fo the connection
     * 
     * @throws Exception
     */
    private void doConnection(boolean silentModeP) throws Exception {
        // Send a connecting message
        if (!silentModeP) {
            fireMessage(handler, ResponseHandler.MSG_CONNECTING);
        }
        // Connect: try to...
        boolean need_shared = !dsmFactory.connect();
        // Send a connected message
        if (!silentModeP) {
            fireMessage(SynoServer.this.handler, ResponseHandler.MSG_CONNECTED);
        }
        if (need_shared) {
            fireMessage(SynoServer.this.handler, ResponseHandler.MSG_SHARED_NOT_SET);
        }
    }

    /**
     * Bind an activity with this current server
     * 
     * @param activityP
     */
    public void bindResponseHandler(ResponseHandler handlerP) {
        handler = handlerP;
    }

    /**
     * Disconnect from the server
     */
    synchronized public void disconnect() {
        connected = false;
        stop = true;
        collector.interrupt();
        if (DEBUG)
            Log.v(Synodroid.DS_TAG, "Server disconnected.");
    }

    /**
     * Saves the last error for future retrieval
     */
    synchronized public void setLastError(String error) {
        lasterror = error;
    }

    /**
     * Disconnect from the server
     */
    public String getLastError() {
        return lasterror;
    }

    /**
     * Disconnect from the server
     */
    public String getCookies() {
        return cookies;
    }

    public void setCookie(String cookieP) {
        cookies = cookieP;
    }

    /**
     * Send a message
     */
    public void fireMessage(ResponseHandler handlerP, int msgP) {
        fireMessage(handlerP, msgP, null);
    }

    /**
     * Send a message
     */
    public void fireMessage(ResponseHandler handlerP, int msgP, Object objP) {
        // Send the connecting message
        Message msg = new Message();
        msg.what = msgP;
        msg.obj = objP;
        handlerP.handleReponse(msg);

    }

    /**
     * Translate an error (JSON or technical exception) to a end-user message
     * 
     * @param Log
     */
    private String translateError(ResponseHandler handlerP, DSMException dsmExP) {
        String msg = "Can't display error";
        msg = handlerP.getString(R.string.unknow_reason);
        // Get the reason
        String jsoReason = dsmExP.getJsonReason();
        // If no JSON reason, try to find the reason in the root DSMException
        if (jsoReason == null && dsmExP.getRootException() != null
                && dsmExP.getRootException() instanceof DSMException) {
            jsoReason = ((DSMException) dsmExP.getRootException()).getJsonReason();
        }

        // If there's is a wellknown reason
        if (jsoReason != null) {
            // Wrong user or password
            if (jsoReason.equals("error_cantlogin")) {
                msg = handlerP.getString(R.string.connect_wrong_userpassword);
            } else if (jsoReason.equals("error_interrupt")) {
                msg = handlerP.getString(R.string.connect_already_connected);
            } else if (jsoReason.equals("error_noprivilege")) {
                msg = handlerP.getString(R.string.connect_cant);
            } else {
                String mapMessage = map.get(jsoReason);
                if (mapMessage == null) {
                    msg += ": " + jsoReason;
                    if (DEBUG)
                        Log.e(Synodroid.DS_TAG, "JSON's error not trapped: " + jsoReason);
                } else {
                    msg = "DSM Error: " + mapMessage;
                }
            }
        }
        // Or if there's a wellknown exception
        else if (dsmExP.getRootException() != null) {
            if (dsmExP.getRootException() instanceof SocketException) {
                msg = handlerP.getString(R.string.connect_nohost);
            } else if (dsmExP.getRootException() instanceof SSLException) {
                try {
                    msg = MessageFormat.format(handlerP.getString(R.string.connect_ssl_error),
                            new Object[] { dsmExP.getCause().getMessage() });
                } catch (Exception e) {
                    msg = handlerP.getString(R.string.port_mismatch);
                }
            } else if (dsmExP.getRootException() instanceof EOFException) {
                msg = handlerP.getString(R.string.port_mismatch);
            } else if (dsmExP.getRootException() instanceof GenericException) {
                msg = handlerP.getString(R.string.failed_response);
            } else if (dsmExP.getRootException() instanceof SocketTimeoutException) {
                msg = handlerP.getString(R.string.connect_nohost);
            } else if (dsmExP.getRootException() instanceof FileNotFoundException) {
                msg = handlerP.getString(R.string.file_not_found);
            } else {
                String m = dsmExP.getRootException().getMessage();
                if (m != null) {
                    msg = m;
                }
            }
        } else if (jsoReason == null && dsmExP.getRootException() == null && dsmExP.isIDException) {
            msg = handlerP.getString(dsmExP.getExceptionID());
        }

        // Return the message
        return msg;
    }

    /**
     * Return the string representation of a Synology server
     */
    @Override
    public String toString() {
        return (currentConn.protocol.name() + "://" + currentConn.host + ":" + currentConn.port).toLowerCase();
    }

    /**
     * Return the handler factory
     * 
     * @return
     */
    public DSMHandlerFactory getDSMHandlerFactory() {
        return dsmFactory;
    }

    /**
     * @return the user
     */
    public String getUser() {
        return user;
    }

    /**
     * @return the password
     */
    public String getPassword() {
        return password;
    }

    /**
     * @return the url
     */
    public String getUrl() {
        return (currentConn.protocol.name() + "://" + currentConn.host + ":" + currentConn.port).toLowerCase();
    }

    /**
     * @return the nickname
     */
    public String getNickname() {
        return nickname;
    }

    /**
     * @return the server ID
     */
    public String getID() {
        return serverID;
    }

    /**
     * @return the protocol
     */
    public SynoProtocol getProtocol() {
        return currentConn.protocol;
    }

    /**
     * @return the host
     */
    public String getHost() {
        return currentConn.host;
    }

    /**
     * @return the port
     */
    public Integer getPort() {
        return currentConn.port;
    }

    /**
     * @return the refreshInterval
     */
    public int getRefreshInterval() {
        return currentConn.refreshInterval;
    }

    /**
     * @return the dsmVersion
     */
    public DSMVersion getDsmVersion() {
        return dsmVersion;
    }

    /**
     * @param nickname
     *            the nickname to set
     */
    synchronized public void setNickname(String nickname) {
        this.nickname = nickname;
        connected = false;
        if (DEBUG)
            Log.v(Synodroid.DS_TAG, "Server nickname updated.");
    }

    /**
     * @param user
     *            the user to set
     */
    synchronized public void setUser(String user) {
        this.user = user;
        connected = false;
        if (DEBUG)
            Log.v(Synodroid.DS_TAG, "Username updated on server.");
    }

    /**
     * @param password
     *            the password to set
     */
    synchronized public void setPassword(String password) {
        this.password = password;
        connected = false;
        if (DEBUG)
            Log.v(Synodroid.DS_TAG, "Password updated on server.");
    }

    /**
     * @param dsmVersion
     *            the dsmVersion to set
     */
    synchronized public void setDsmVersion(DSMVersion dsmVersion, boolean reconnect) {
        if (reconnect)
            connected = false;

        if (!this.dsmVersion.equals(dsmVersion)) {
            if (DEBUG)
                Log.i(Synodroid.DS_TAG, "DSM Handler switching from " + this.dsmVersion.getTitle() + " to "
                        + dsmVersion.getTitle());
            this.dsmVersion = dsmVersion;
            // Create the appropriated factory
            dsmFactory = DSMHandlerFactory.getFactory(dsmVersion, this, DEBUG, autoDetect);
        }
    }

    /**
     * @param sortAttribute
     *            the sortAttribute to set
     */
    synchronized public void setSortAttribute(String sortAttribute) {
        this.sortAttribute = sortAttribute;
    }

    /**
     * @param ascending
     *            the ascending to set
     */
    synchronized public void setAscending(boolean ascending) {
        this.ascending = ascending;
    }

    /**
     * @return the sortAttribute
     */
    public String getSortAttribute() {
        return sortAttribute;
    }

    /**
     * @return the ascending
     */
    public boolean isAscending() {
        return ascending;
    }

    /**
     * Execute an asynchronous action on this server
     * 
     * @param actionP
     *            The action to execute
     * @param forceRefreshP
     *            Flag to set if a refresh is needed after the completion of the action
     */
    public void executeAsynchronousAction(final ResponseHandler handlerP, final SynoAction actionP,
            final boolean forceRefreshP) {
        executeAsynchronousAction(handlerP, actionP, forceRefreshP, true);
    }

    /**
     * Execute an asynchronous action on this server
     * 
     * @param actionP
     *            The action to execute
     * @param forceRefreshP
     *            Flag to set if a refresh is needed after the completion of the action
     */
    public void executeAsynchronousAction(final ResponseHandler handlerP, final SynoAction actionP,
            final boolean forceRefreshP, final boolean showToast) {
        Runnable runnable = new Runnable() {
            public void run() {
                // An operation is pending
                fireMessage(handlerP, ResponseHandler.MSG_OPERATION_PENDING);
                if (DEBUG)
                    Log.v(Synodroid.DS_TAG, "Executing action: " + actionP.getName());

                //Kill toast if it is a safe addTaskAction...
                try {
                    ((AddTaskAction) actionP).checkToast(SynoServer.this);
                } catch (Exception e) {
                }

                try {
                    if (actionP instanceof AddTaskAction
                            && SynoServer.this.getDsmVersion().smallerThen(DSMVersion.VERSION3_1)
                            && ((AddTaskAction) actionP).getUriString().startsWith("magnet")) {
                        if (DEBUG)
                            Log.w(Synodroid.DS_TAG, "Task dismissed. Magnet links only works on DSM 3.1 and up.");
                        fireMessage(handlerP, ResponseHandler.MSG_TOAST,
                                ((Fragment) handlerP).getText(R.string.magnet));
                    } else {
                        // If a Toast must be shown
                        if (actionP.isToastable() && showToast) {
                            int resId = actionP.getToastId();
                            String fileName = (actionP.getTask() != null ? actionP.getTask().fileName : "");
                            String text = handlerP.getString(resId, new Object[] { fileName });
                            fireMessage(handlerP, ResponseHandler.MSG_TOAST, text);
                        }
                        actionP.execute(handlerP, SynoServer.this);
                    }
                } catch (DSMException ex) {
                    if (DEBUG)
                        Log.e(Synodroid.DS_TAG, "Unexpected DSM error", ex);
                    try {
                        fireMessage(handlerP, ResponseHandler.MSG_ERROR,
                                SynoServer.this.translateError(SynoServer.this.handler, ex));
                    } catch (Exception err) {
                    }
                } catch (DownloadStationNotFound e) {
                    if (DEBUG)
                        Log.e(Synodroid.DS_TAG, "Download Station Not Found Error", e);
                    try {
                        fireMessage(handlerP, ResponseHandler.MSG_ERROR,
                                SynoServer.this.handler.getString(R.string.download_station_not_found));
                    } catch (Exception err) {
                    }
                } catch (Exception e) {
                    if (DEBUG)
                        Log.e(Synodroid.DS_TAG, "Unexpected error", e);
                    DSMException ex = new DSMException(e);
                    try {
                        fireMessage(handlerP, ResponseHandler.MSG_ERROR,
                                SynoServer.this.translateError(SynoServer.this.handler, ex));
                    } catch (Exception err) {
                    }
                } finally {
                    fireMessage(handlerP, ResponseHandler.MSG_OPERATION_DONE);
                    // Interrup the collector's thread so it will refresh
                    // immediatelty
                    if (forceRefreshP) {
                        collector.interrupt();
                    }
                }
            }
        };
        new Thread(runnable, "Synodroid DS action").start();
    }

    public StringBuffer download(String uriP, String requestP) throws Exception {
        HttpURLConnection con = null;
        try {
            // Create the connection
            con = createConnection(uriP, requestP, "GET", true);
            // Add the parameters
            OutputStreamWriter wr = new OutputStreamWriter(con.getOutputStream());
            wr.write(requestP);
            // Send the request
            wr.flush();

            // Try to retrieve the session cookie
            String newCookie = con.getHeaderField("set-cookie");
            if (newCookie != null) {
                synchronized (this) {
                    cookies = newCookie;
                }
                if (DEBUG)
                    Log.v(Synodroid.DS_TAG, "Retreived cookies: " + cookies);
            }
            // Now read the reponse and build a string with it
            BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream()));
            StringBuffer sb = new StringBuffer();
            String line;
            try {
                while ((line = br.readLine()) != null) {
                    sb.append(line);
                }
            } catch (OutOfMemoryError e) {
                sb = null;
            }

            br.close();
            return sb;
        }
        // Unexpected exception
        catch (Exception ex) {
            if (DEBUG)
                Log.e(Synodroid.DS_TAG, "Unexpected error", ex);
            throw ex;
        }
        // Finally close everything
        finally {
            if (con != null) {
                con.disconnect();
            }
        }
    }

    /**
     * @return the connected
     */
    public boolean isConnected() {
        return connected;
    }

    /**
     * Set server connection flag
     */
    synchronized public void setConnected(boolean status) {
        connected = status;
    }

    /**
     * Force a refresh by interrupting the sleep
     */
    synchronized public void forceRefresh() {
        if (collector != null) {
            collector.interrupt();
        }
    }

    /**
     * Pause the server's thread
     */
    public void pause() {
        pause = true;
    }

    /**
     * Resume the server's thread
     */
    public void resume() {
        pause = false;
        collector.interrupt();
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((nickname == null) ? 0 : nickname.hashCode());
        return result;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        SynoServer other = (SynoServer) obj;
        if (nickname == null) {
            if (other.nickname != null)
                return false;
        } else if (!nickname.equals(other.nickname))
            return false;
        return true;
    }

    /**
     * Does the collector thread is alive
     * 
     * @return
     */
    public boolean isAlive() {
        if (collector != null) {
            return collector.isAlive() && !stop;
        }
        return false;
    }

    /**
     * @return the interrupted
     */
    public boolean isInterrupted() {
        return interrupted;
    }

    /**
     * @param interruptedP
     *            the interrupted to set
     */
    synchronized public void setInterrupted(boolean interruptedP) {
        interrupted = interruptedP;
    }

    /**
     * Return the current connection or null if there's no connection currentlty
     * 
     * @return
     */
    public SynoServerConnection getConnection() {
        return currentConn;
    }

    /**
     * @return the localConnection
     */
    public SynoServerConnection getLocalConnection() {
        return localConnection;
    }

    /**
     * @param localConnectionP
     *            the localConnection to set
     */
    synchronized public void setLocalConnection(SynoServerConnection localConnectionP) {
        localConnection = localConnectionP;
    }

    /**
     * @return the publicConnection
     */
    public SynoServerConnection getPublicConnection() {
        return publicConnection;
    }

    /**
     * @param publicConnectionP
     *            the publicConnection to set
     */
    synchronized public void setPublicConnection(SynoServerConnection publicConnectionP) {
        publicConnection = publicConnectionP;
    }

    public ResponseHandler getResponseHandler() {
        return handler;
    }

    public void setDebugLvl(boolean debug) {
        DEBUG = debug;
    }

}