org.ohthehumanity.carrie.CarrieActivity.java Source code

Java tutorial

Introduction

Here is the source code for org.ohthehumanity.carrie.CarrieActivity.java

Source

// Carrie Remote Control
// Copyright (C) 2012 Mike Elson

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package org.ohthehumanity.carrie;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.Socket;
import java.net.URLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.NetworkInterface;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.lang.InterruptedException;
import java.util.Enumeration;
import java.util.List;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Iterator;
import java.util.concurrent.ExecutionException;

import org.apache.http.conn.ConnectTimeoutException;

import android.os.Bundle;
import android.os.AsyncTask;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.content.SharedPreferences;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.widget.TextView;
import android.preference.Preference;
import android.preference.PreferenceManager;
import android.content.DialogInterface;
import android.content.ComponentName;
import android.content.Context;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.net.ConnectivityManager;

import org.ohthehumanity.carrie.CarriePreferences;

/** Main window for the Carrie application.
 **/

public class CarrieActivity extends Activity implements OnSharedPreferenceChangeListener {
    public enum Status {
        OK, INTERNAL_ERROR, NO_CONNECTION, TIMEOUT, BAD_URL, NETWORK_ERROR, SERVER_ERROR
    };

    private static final String TAG = "carrie";
    private SharedPreferences mPreferences;
    //private string connection_status;
    private String mServerName;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mServerName = "";
        setContentView(R.layout.main);

        // instantiate our preferences backend
        mPreferences = PreferenceManager.getDefaultSharedPreferences(this);

        // set callback function when settings change
        mPreferences.registerOnSharedPreferenceChangeListener(this);

        if (mPreferences.getString("server", null) == null) {
            setStatus("Server not set");
        } else if (mPreferences.getString("port", null) == null) {
            setStatus("Port not configured");
        }

        ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
        if (cm.getActiveNetworkInfo().getType() != ConnectivityManager.TYPE_WIFI) {

            AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this);
            dlgAlert.setTitle("WiFi not active");
            dlgAlert.setMessage(
                    "This application is usually used on a local network over a WiFi. Open WiFi settings?");
            dlgAlert.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int which) {
                    switch (which) {
                    case DialogInterface.BUTTON_POSITIVE:
                        //Yes button clicked
                        final Intent intent = new Intent(Intent.ACTION_MAIN, null);
                        intent.addCategory(Intent.CATEGORY_LAUNCHER);
                        final ComponentName cn = new ComponentName("com.android.settings",
                                "com.android.settings.wifi.WifiSettings");
                        intent.setComponent(cn);
                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                        startActivity(intent);
                        break;

                    case DialogInterface.BUTTON_NEGATIVE:
                        //Log.i(TAG, "Not opening wifi");
                        //No button clicked
                        break;
                    }
                }
            });

            dlgAlert.setNegativeButton("No", null);
            dlgAlert.setCancelable(true);
            dlgAlert.create().show();
        }

        updateTitle();
        updateSkipLabels();
        updateServerName();
    }

    /** ABC to send a single network command in a separate Task thread
     **/

    private abstract class HTTPTask extends AsyncTask<String, Integer, Void> {
        // TBD split no connection into NO_ROUTE and NO_SERVER
        protected CarrieActivity.Status status;

        protected String response;

        protected String url;

        protected void retrieve(String request) {
            url = request;
            URL urlobj;
            try {
                urlobj = new URL(request);
            } catch (MalformedURLException e) {
                status = CarrieActivity.Status.BAD_URL;
                return;
            }
            URLConnection conn;
            try {
                conn = urlobj.openConnection();
            } catch (IOException e) {
                status = CarrieActivity.Status.NO_CONNECTION;
                return;
            }
            //Log.i(TAG, "Opening URL " + request);

            HttpURLConnection httpConn = (HttpURLConnection) conn;
            httpConn.setAllowUserInteraction(false);
            httpConn.setInstanceFollowRedirects(true);
            try {
                httpConn.setRequestMethod("GET");
            } catch (ProtocolException e) {
                status = CarrieActivity.Status.INTERNAL_ERROR;
                return;
            }
            try {
                httpConn.connect();
            } catch (ConnectTimeoutException e) {
                //Log.i(TAG, "timeout");
                status = CarrieActivity.Status.TIMEOUT;
                return;
            } catch (IOException e) {
                //Log.i(TAG, "ioexception");
                status = CarrieActivity.Status.NO_CONNECTION;
                return;
            }
            //Log.i(TAG, "Sending request");

            int response_code;
            try {
                response_code = httpConn.getResponseCode();
            } catch (IOException e) {
                status = CarrieActivity.Status.INTERNAL_ERROR;
                //Log.e(TAG, "Exception reading response code");
                return;
            }
            if (response_code != HttpURLConnection.HTTP_OK) {
                status = CarrieActivity.Status.SERVER_ERROR;
                //Log.e(TAG, "Server returned " + response_code);
                return;
            }
            //Log.i(TAG, "Got response code " + response_code);
            InputStream in;
            try {
                in = httpConn.getInputStream();
            } catch (IOException e) {
                status = CarrieActivity.Status.INTERNAL_ERROR;
                return;
            }

            InputStreamReader isr = new InputStreamReader(in);
            int charRead;
            int BUFFER_SIZE = 2000;
            char[] inputBuffer = new char[BUFFER_SIZE];
            response = "";
            try {
                while ((charRead = isr.read(inputBuffer)) > 0) {
                    String readString = String.copyValueOf(inputBuffer, 0, charRead);
                    response += readString;
                    inputBuffer = new char[BUFFER_SIZE];
                }
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
                status = CarrieActivity.Status.NETWORK_ERROR;
                return;
            }
            status = CarrieActivity.Status.OK;
            //Log.i(TAG, "Server response was " + response);
        }

        protected String statusString() {
            switch (status) {
            case NO_CONNECTION:
                return "Cannot connect";
            case INTERNAL_ERROR:
                return "Internal error";
            case TIMEOUT:
                return "Timeout";
            case BAD_URL:
                return "Bad url: " + url;
            case NETWORK_ERROR:
                return "Network error";
            case SERVER_ERROR:
                return "Server error";
            default:
                return "ok";
            }
        }
    }

    /** Request the server name from a Carrie server and update the status
       bar **/

    private class GetServerNameTask extends HTTPTask {
        protected Void doInBackground(String... url) {
            //Log.d(TAG, "GetServerNameTask.doInBackground");
            String target = mPreferences.getString("server", null) + ":" + mPreferences.getString("port", null);
            //Log.i(TAG, "Calling retrieve");
            retrieve("http://" + target + "/carrie/hello");
            //Log.i(TAG, "Done retrieve, status is " + status);
            return null;
        }

        protected void onPostExecute(Void dummy) {
            //Log.i(TAG, "GetServerName.onPostExecute");
            if (status == CarrieActivity.Status.OK) {
                setStatus("Connected to " + response);
                mServerName = response;
            } else {
                setStatus(statusString() + " " + mPreferences.getString("server", null));
            }
            updateTitle();
        }
    }

    /** Send a command to the server and show the status
     **/

    private class SendCommandTask extends HTTPTask {
        protected Void doInBackground(String... url) {
            String target = mPreferences.getString("server", null) + ":" + mPreferences.getString("port", null);
            //Log.i(TAG, "Calling retrieve");
            retrieve("http://" + target + "/" + url[0]);
            //Log.i(TAG, "Done retrieve, status is " + status);
            return null;
        }

        protected void onPostExecute(Void dummy) {
            //Log.i(TAG, "onPostExecute");
            if (status == CarrieActivity.Status.OK) {
                setStatus(response);
            } else {
                setStatus(statusString());
            }
        }
    }

    private class ScanServersTask extends AsyncTask<Void, String, LinkedList<String>> {
        protected LinkedList<String> mServers;
        //protected Object mLock;
        protected CarrieActivity mActivity;

        public ScanServersTask(CarrieActivity activity) {
            super();
            mActivity = activity;
        }

        private class Scanner extends Thread {
            String mBaseAddr;
            int mOffsetAddr;
            int mPort;
            int mTimeout;
            ScanServersTask mTask;

            public Scanner(String baseAddr, int offsetAddr, int port, int timeout, ScanServersTask task) {
                mBaseAddr = baseAddr;
                mOffsetAddr = offsetAddr;
                mPort = port;
                mTask = task;
            }

            public void run() {
                //Log.i(TAG, "threadstart");
                Socket socket = new Socket();
                String server = mBaseAddr + mOffsetAddr;
                try {
                    socket.connect(new InetSocketAddress(InetAddress.getByName(server), mPort), mTimeout);
                } catch (IOException e) {
                    //Log.i(TAG, "    cannot connect");
                    try {
                        socket.close();
                    } catch (IOException e2) {
                        // ignore anything thrown by close()
                    }
                    return;
                }
                try {
                    socket.close();
                } catch (IOException e) {
                }
                //Log.i(TAG, "    connection on " + server);
                mTask.callback(server);
            }
        }

        /** As each thread completes and exits, call this function to record the new server
           if found
         **/

        public void callback(String addr) {
            //Log.i(TAG, "Callback " + addr);
            //publishProgress(addr);
            synchronized (this) {
                mServers.add(addr);
            }
        }

        /** Wrapper to update the status area of the main activity
         **/

        protected void setStatus(String message) {
            final String inner_message = new String(message);
            runOnUiThread(new Runnable() {
                public void run() {
                    TextView updateView = (TextView) findViewById(R.id.status);
                    updateView.setText(inner_message);
                }
            });
        }

        /** Initiate scan.
           Each request is sent by a seperate thread so all 256 possible
           IP addresses on the local /8 subnet can be tested in parallel
        **/

        protected LinkedList<String> doInBackground(Void... params) {
            mServers = new LinkedList<String>();
            String full_ip = getLocalIpAddress();
            int index = full_ip.lastIndexOf(".");
            if (index == -1) {
                //Log.e(TAG, "Cannot decode IP " + full_ip);
                return mServers;
            }
            String subnet = full_ip.substring(0, index + 1);
            //Log.i(TAG, "Begin scan of " + subnet);
            setStatus("Scanning " + subnet + "* ...");
            int timeout = 500; // ms
            int port = Integer.parseInt(mPreferences.getString("port", null));
            LinkedList<Scanner> scanners = new LinkedList<Scanner>();
            for (int i = 0; i <= 255; i++) {
                Scanner t = new Scanner(subnet, i, port, timeout, this);
                scanners.add(t);
                t.start();
            }
            //Log.i(TAG, "End scan initiation, waiting for threads to exit");
            for (Iterator<Scanner> i = scanners.iterator(); i.hasNext();) {
                try {
                    i.next().join();
                } catch (InterruptedException e) {
                }
            }
            //Log.i(TAG, "Threads finished, found " + mServers.size() + " servers");
            return mServers;
        }

        /** After scan completed update the UI
         **/

        protected void onPostExecute(LinkedList<String> servers) {
            //Log.d(TAG, "ScanServersTask.onPostExecute");
            setStatus("Found " + mServers.size() + " servers");
            switch (mServers.size()) {
            case 0:
                // no servers found
                return;
            case 1:
                // single server found, automatically connect
                //Log.d(TAG, "Updating preferences");
                setServer(servers.getFirst());
                return;
            default:
                // multiple servers found, present the user with a choice dialog
                AlertDialog.Builder alert = new AlertDialog.Builder(mActivity);
                alert.setTitle("Choose server");
                final String[] inner_servers = new String[servers.size()];
                servers.toArray(inner_servers);
                alert.setItems(inner_servers, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int item) {
                        setServer(inner_servers[item]);
                    }
                });
                alert.show();
                return;
            }
        }
    }

    /** Write a new string into our `server` preference and ping it to check
       for the hostname
    **/

    public void setServer(String server) {
        SharedPreferences.Editor editor = mPreferences.edit();
        editor.putString("server", server);
        editor.commit();
        updateServerName();
    }

    public String getLocalIpAddress() {
        try {
            for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en
                    .hasMoreElements();) {

                NetworkInterface intf = en.nextElement();
                for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements();) {

                    InetAddress inetAddress = enumIpAddr.nextElement();
                    if (!inetAddress.isLoopbackAddress()) {
                        return inetAddress.getHostAddress();
                    }
                }
            }
        } catch (SocketException ex) {
            //Log.e(TAG, ex.toString());
        }
        return "";
    }

    /** Set windoww status field
     **/

    public void setStatus(String message) {
        TextView updateView = (TextView) findViewById(R.id.status);
        if (updateView == null) {
            //Log.e(TAG, "R.id.status is null");
        } else {
            updateView.setText(message);
        }
    }

    /** Tell our worker thread to send a command over the network
     **/

    public void command(String message) {
        if (mPreferences.getString("server", null) == null || mPreferences.getString("port", null) == null) {
            startActivity(new Intent(this, CarriePreferences.class));
        } else {
            //Log.i(TAG, "command");
            setStatus("Connecting...");
            new SendCommandTask().execute(message);
        }
    }

    /** Handle click on Play/Pause button
     **/

    public void onPlay(View view) {
        setStatus("on play");
        //Log.i(TAG, "onPlay");
        //String res = send("play");
        command("pause");
    }

    /** Handle click on Fullscreen button
     **/

    public void onFullscreen(View view) {
        setStatus("on fullscreen");
        command("fullscreen");
    }

    /** Handle click on little backwards button
     **/

    public void onBackwards(View view) {
        command("backward/" + mPreferences.getString("small_skip", "7"));
    }

    /** Handle click on little forwards button
     **/

    public void onForwards(View view) {
        command("forward/" + mPreferences.getString("small_skip", "7"));
    }

    public void onBBackwards(View view) {
        command("backward/" + mPreferences.getString("large_skip", "60"));
    }

    public void onFForwards(View view) {
        command("forward/" + mPreferences.getString("large_skip", "60"));
    }

    public void onOSDOn(View view) {
        command("osdon");
    }

    public void onOSDOff(View view) {
        command("osdoff");
    }

    public void onMute(View view) {
        command("mute");
    }

    public void onVoldown(View view) {
        command("voldown");
    }

    public void onVolup(View view) {
        command("volup");
    }

    public void onSub(View view) {
        command("sub");
    }

    public void onSubLang(View view) {
        //new ScanServersTask().execute();
        command("sublang");
    }

    public void onAudLang(View view) {
        command("audlang");
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        //Log.i(TAG, "onCreateOptionsMenu");
        //startActivity(new Intent(this, CarriePreferences.class));
        startActivityForResult(new Intent(this, CarriePreferences.class), 1);
        return false;
    }

    /** Automatically called when an activity (our preferences) that was started with
       startActivityForResult exits **/

    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        //if (data == null) {
        //Log.i(TAG, "Preferences closed no scan requested");
        //} else {
        //Log.i(TAG, "Preferences closed scan is " + data.getBooleanExtra("scan", false));
        //}

        if (data != null && data.getBooleanExtra("scan", false) == true) {
            //Log.i(TAG, "Begin scan in correct thread");
            new ScanServersTask(this).execute();
        } else {
            //Log.d(TAG, "Updating server name");
            updateServerName();
        }
    }

    /** Callback when a preference is changed, either through code or the preferences screen
     **/

    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        mServerName = "";
        // Change the title bar to show target address and port
        updateTitle();
        // Change the labels showing how far jumps go
        updateSkipLabels();
        // Test the network connection
        //setStatus("Requesting server name");
        //new GetServerNameTask().execute();
    }

    /** Update the window title and status area to show the target server name
       as returned by a check on url http://<servername>:<port>/carrie/hello
     **/

    private void updateServerName() {
        if (mPreferences.getString("server", null) == null) {
            setStatus("Server not set");
        } else {
            setStatus("Requesting server name");
            new GetServerNameTask().execute();
        }
    }

    /** Update the window title bar to show server location
     **/

    private void updateTitle() {
        // vvv
        String prelude = "Remote Control - ";
        if (mPreferences.getString("server", null) == null) {
            setTitle(prelude + "server not set");
        } else if (mServerName.equals("")) {
            setTitle(prelude + mPreferences.getString("server", "") + ":" + mPreferences.getString("port", "5505"));
        } else {
            setTitle(prelude + mServerName);
        }
    }

    /** On startup and after changing settings update the labels in between
       the skip back/forwards buttons to show the current skip distances
       in seconds
    **/

    private void updateSkipLabels() {
        ((TextView) findViewById(R.id.nudge_seconds)).setText(mPreferences.getString("small_skip", "7") + "s");
        ((TextView) findViewById(R.id.skip_seconds)).setText(mPreferences.getString("large_skip", "60") + "s");
    }
}