it.mb.whatshare.MainActivity.java Source code

Java tutorial

Introduction

Here is the source code for it.mb.whatshare.MainActivity.java

Source

/**
 * MainActivity.java Created on 10 Mar 2013
 * 
 * Copyright 2013 Michele Bonazza <emmepuntobi@gmail.com>
 * 
 * This file is part of WhatsHare.
 * 
 * WhatsHare 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.
 * 
 * Foobar 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
 * WhatsHare. If not, see <http://www.gnu.org/licenses/>.
 */
package it.mb.whatshare;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OptionalDataException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import org.json.JSONException;

import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.text.Html;
import android.util.Pair;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnLongClickListener;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
import android.widget.TextView;
import android.widget.Toast;

import com.google.analytics.tracking.android.EasyTracker;
import com.google.analytics.tracking.android.ExceptionReporter;
import com.google.analytics.tracking.android.GoogleAnalytics;
import com.google.analytics.tracking.android.Tracker;

/**
 * The only activity: it can be created when tapping on a notification (and an
 * Intent is routed to this activity, which in turn creates a new Whatsapp
 * activity) or by tapping the app's icon, in which case the pairing screen is
 * displayed.
 * 
 * @author Michele Bonazza
 * 
 */
public class MainActivity extends FragmentActivity {

    /**
     * The file name of the list containing all inbound devices.
     */
    public static final String INBOUND_DEVICES_FILENAME = "inbound";

    /**
     * If set, all network requests are routed to localhost (on port 80) upon
     * failure (to have a log of all requests that failed). An HTTP server must
     * be running (for instance, using netcat or the echo_server.py script in
     * the extras_not_in_apk folder)
     */
    public static final boolean DEBUG_FAILED_REQUESTS = false;

    /**
     * The IP of the server that logs failed requests in case
     * {@link #DEBUG_FAILED_REQUESTS} is set.
     */
    public static final String DEBUG_FAILED_REQUESTS_SERVER = "http://192.168.0.8";
    /**
     * The key used to set the intent type according to which content is either
     * shared directly through the paired device's WhatsApp or after showing an
     * app picker dialog.
     */
    public static final String INTENT_TYPE_EXTRA = "type";
    /**
     * String added to the message sent through GCM in case the content should
     * be shared directly via Whatsapp bypassing the app choice dialog on the
     * receiver's side.
     */
    public static final String SHARE_VIA_WHATSAPP_EXTRA = "w";

    /**
     * A device paired with this one to share stuff on whatsapp.
     * 
     * @author Michele Bonazza
     */
    static class PairedDevice implements Serializable {

        /**
         * 
         */
        private static final long serialVersionUID = 7662420062946826108L;
        /**
         * The name set for the device by the user.
         */
        String name;
        /**
         * The device's type as specified by the device itself (e.g. 'Chrome
         * Whatshare Extension').
         */
        final String type;

        /**
         * The unique ID for the device
         */
        final String id;

        /**
         * Creates a new device.
         * 
         * @param ID
         *            the ID for the device
         * @param name
         *            the name set for the device by the user
         * @param type
         *            the device's type as specified by the device itself (e.g.
         *            'Chrome Whatshare Extension')
         */
        PairedDevice(String ID, String name, String type) {
            this.id = ID;
            this.name = name;
            this.type = type;
        }

        /**
         * Returns a new value to be used as ID for a device.
         * 
         * @return an ID to be used to identify a device
         */
        static String getNextID() {
            return String.valueOf(System.currentTimeMillis());
        }

        public String toString() {
            return new StringBuilder(name).append(", type(").append(type).append(")").toString();
        }

        /**
         * Renames this device.
         * 
         * @param newName
         *            the new name for this device
         */
        public void rename(String newName) {
            this.name = newName;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((id == null) ? 0 : id.hashCode());
            result = prime * result + ((name == null) ? 0 : name.hashCode());
            result = prime * result + ((type == null) ? 0 : type.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            PairedDevice other = (PairedDevice) obj;
            if (id == null) {
                if (other.id != null)
                    return false;
            } else if (!id.equals(other.id))
                return false;
            if (name == null) {
                if (other.name != null)
                    return false;
            } else if (!name.equals(other.name))
                return false;
            if (type == null) {
                if (other.type != null)
                    return false;
            } else if (!type.equals(other.type))
                return false;
            return true;
        }

    }

    /**
     * The name of Whatsapp's package.
     */
    static final String WHATSAPP_PACKAGE = "com.whatsapp";
    /**
     * Where Google shortener is at.
     */
    static final String SHORTENER_URL = "https://www.googleapis.com/urlshortener/v1/url?key=%s";
    /**
     * The number of random keys used to encrypt messages with.
     */
    static final int SHARED_SECRET_SIZE = 4;
    /**
     * The regex used to filter names chosen by the user for inbound devices.
     */
    static final String VALID_DEVICE_NAME = "[A-Za-z0-9\\-\\_\\s]{1,30}";
    /**
     * The code used by {@link #onActivityResult(int, int, Intent)} to detect
     * incoming replies from the QR code capture activity.
     */
    static final int QR_CODE_SCANNED = 0;

    private PairedDevice outboundDevice, deviceSelectedContextMenu;
    private List<PairedDevice> inboundDevices = new ArrayList<PairedDevice>();
    private ArrayAdapter<String> adapter;
    private Tracker tracker;
    private GoogleAnalytics analytics;

    /*
     * (non-Javadoc)
     * 
     * @see android.app.Activity#onCreateContextMenu(android.view.ContextMenu,
     * android.view.View, android.view.ContextMenu.ContextMenuInfo)
     */
    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.context_menu_devices, menu);
        super.onCreateContextMenu(menu, v, menuInfo);
        AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
        deviceSelectedContextMenu = inboundDevices.get(info.position);
    }

    /*
     * (non-Javadoc)
     * 
     * @see android.app.Activity#onResume()
     */
    @Override
    protected void onResume() {
        super.onResume();
        updateLayout();
    }

    /*
     * (non-Javadoc)
     * 
     * @see android.app.Activity#onContextItemSelected(android.view.MenuItem)
     */
    @Override
    public boolean onContextItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case R.id.context_devices_rename:
            tracker.sendEvent("ui", "button_press", "rename", 0L);
            Dialogs.promptForNewDeviceName(deviceSelectedContextMenu, this);
            break;
        case R.id.context_devices_unpair:
            tracker.sendEvent("ui", "button_press", "unpair", 0L);
            Dialogs.confirmUnpairInbound(deviceSelectedContextMenu, this);
            break;
        }
        return super.onContextItemSelected(item);
    }

    /**
     * Removes the currently selected device from the list of inbound devices.
     */
    void removePaired() {
        if (deviceSelectedContextMenu != null) {
            Utils.debug("removePaired(): removing %s... success? %s", deviceSelectedContextMenu.name,
                    inboundDevices.remove(deviceSelectedContextMenu));
            deviceSelectedContextMenu = null;
            writePairedInboundFile(inboundDevices);
            BaseAdapter listAdapter = getListAdapter();
            listAdapter.notifyDataSetChanged();
        } else {
            Utils.debug("removePaired(): no device is currently set to be unpaired");
        }
    }

    /**
     * Renames the currently selected device.
     */
    void onSelectedDeviceRenamed() {
        if (deviceSelectedContextMenu != null) {
            Utils.debug("renamePaired(): renamed %s to %s", deviceSelectedContextMenu.type,
                    deviceSelectedContextMenu.name);
            deviceSelectedContextMenu = null;
            writePairedInboundFile(inboundDevices);
            BaseAdapter listAdapter = getListAdapter();
            listAdapter.notifyDataSetChanged();
        } else {
            Utils.debug("renamePaired(): no device is currently set to be renamed");
        }
    }

    /**
     * Refreshes the layout of this activity, also reloading configured devices
     * from files.
     */
    private void updateLayout() {
        if (outboundDevice == null) {
            try {
                Pair<PairedDevice, String> paired = SendToGCMActivity.loadOutboundPairing(this);
                if (paired != null)
                    outboundDevice = paired.first;
            } catch (OptionalDataException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (FileNotFoundException e) {
                // it's ok
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        final TextView outboundView = (TextView) findViewById(R.id.outbound_device);
        if (outboundDevice != null) {
            outboundView.setText(outboundDevice.type);
        } else {
            outboundView.setText(R.string.no_device);
        }

        final boolean outboundConfigured = outboundDevice != null;
        outboundView.setOnLongClickListener(new OnLongClickListener() {

            @Override
            public boolean onLongClick(View v) {
                if (outboundConfigured) {
                    Dialogs.confirmRemoveOutbound(outboundDevice, MainActivity.this);
                } else {
                    showOutboundConfiguration();
                }
                return true;
            }
        });
    }

    /**
     * Removes the currently configured outbound device and updates both the
     * current layout and the configuration file.
     */
    void deleteOutboundDevice() {
        outboundDevice = null;
        try {
            PairOutboundActivity.savePairing(null, this);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (JSONException e) {
            e.printStackTrace();
        }
        updateLayout();
    }

    /**
     * Returns the number of currently paired inbound devices.
     * 
     * @return the number of currently paired inbound devices
     */
    int getInboundDevicesCount() {
        return adapter.getCount();
    }

    /**
     * Called when the add new inbound device button is pressed.
     * 
     * @param v
     *            the button
     */
    public void onAddInboundClicked(View v) {
        onNewInboundDevicePressed();
    }

    /**
     * Called when the add new outbound device button is pressed.
     * 
     * @param v
     *            the button
     */
    public void onAddOutboundClicked(View v) {
        onNewOutboundDevicePressed();
    }

    /*
     * (non-Javadoc)
     * 
     * @see android.app.Activity#onOptionsItemSelected(android.view.MenuItem)
     */
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case R.id.menu_delete_saved_inbound:
            onDeleteSavedInboundPressed();
            break;
        case R.id.menu_tell_friends:
            tracker.sendEvent("ui", "button_press", "tell_friends", 0L);
            onTellFriendsSelected();
            break;
        case R.id.about:
            tracker.sendEvent("ui", "button_press", "about", 0L);
            Dialogs.showAbout(this);
            break;
        }
        return true;
    }

    /*
     * (non-Javadoc)
     * 
     * @see android.app.Activity#onCreateOptionsMenu(android.view.Menu)
     */
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.activity_main, menu);
        return true;
    }

    private void onDeleteSavedInboundPressed() {
        tracker.sendEvent("ui", "button_press", "delete_all_inbound", 0L);
        Dialogs.confirmUnpairAllInbound(this);
    }

    private void onTellFriendsSelected() {
        Intent intent = new Intent(Intent.ACTION_SEND);
        intent.setType("text/html");
        intent.putExtra(Intent.EXTRA_SUBJECT, getResources().getString(R.string.email_subject));
        intent.putExtra(Intent.EXTRA_TEXT, Html.fromHtml(getResources().getString(R.string.email_body)));
        startActivity(Intent.createChooser(intent, getResources().getString(R.string.email_intent_msg)));
    }

    /**
     * Removes all inbound paired devices.
     */
    public void deleteAllInbound() {
        if (!getApplicationContext().deleteFile(INBOUND_DEVICES_FILENAME)) {
            if (!inboundDevices.isEmpty()) {
                Toast.makeText(this, R.string.delete_all_inbound_fail_notification, Toast.LENGTH_SHORT).show();
                return;
            }
        }

        inboundDevices = new ArrayList<PairedDevice>();
        BaseAdapter listAdapter = getListAdapter();
        listAdapter.notifyDataSetChanged();
        Toast.makeText(this, R.string.delete_all_inbound_notification, Toast.LENGTH_SHORT).show();
    }

    private void showOutboundConfiguration() {
        Intent i = new Intent(this, PairOutboundActivity.class);
        startActivity(i);
    }

    /**
     * Called when the pair new device menu item (inbound) is pressed.
     */
    public void onNewInboundDevicePressed() {
        if (Utils.isConnectedToTheInternet(this)) {
            tracker.sendEvent("ui", "button_press", "add_inbound", 0L);
            Dialogs.pairInboundInstructions(this);
        } else {
            tracker.sendEvent("ui", "button_press", "add_inbound", -1L);
            Dialogs.noInternetConnection(this, R.string.no_internet_pairing, false);
        }
    }

    /**
     * Called when the pair new device menu item (outbound) is pressed.
     */
    public void onNewOutboundDevicePressed() {
        if (Utils.isConnectedToTheInternet(this)) {
            tracker.sendEvent("ui", "button_press", "add_outbound", 0L);
            showOutboundConfiguration();
        } else {
            tracker.sendEvent("ui", "button_press", "add_outbound", -1L);
            Dialogs.noInternetConnection(this, R.string.no_internet_pairing, false);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Utils.checkDebug(this);
        analytics = GoogleAnalytics.getInstance(this);
        tracker = analytics.getTracker(getResources().getString(R.string.ga_trackingId));

        Thread.UncaughtExceptionHandler uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
        if (uncaughtExceptionHandler instanceof ExceptionReporter) {
            ExceptionReporter exceptionReporter = (ExceptionReporter) uncaughtExceptionHandler;
            exceptionReporter.setExceptionParser(new AnalyticsExceptionParser());
        }

        analytics.setDefaultTracker(tracker);
        // start the registration process if needed
        GCMIntentService.registerWithGCM(this);
        View menu = getLayoutInflater().inflate(R.layout.menu, null);
        setContentView(menu);
    }

    /**
     * Checks whether Whatsapp is installed on this device.
     * 
     * @param activity
     *            the calling activity
     * @return <code>true</code> if a whatsapp installation is found locally
     */
    static boolean isWhatsappInstalled(Context activity) {
        boolean installed = false;
        try {
            activity.getPackageManager().getPackageInfo(WHATSAPP_PACKAGE, PackageManager.GET_ACTIVITIES);
            installed = true;
        } catch (PackageManager.NameNotFoundException e) {
            // not installed
        }
        return installed;
    }

    /*
     * (non-Javadoc)
     * 
     * @see android.app.Activity#onStart()
     */
    @Override
    protected void onStart() {
        super.onStart();
        EasyTracker.getInstance().activityStart(this);
    }

    /*
     * (non-Javadoc)
     * 
     * @see android.app.Activity#onStop()
     */
    @Override
    protected void onStop() {
        super.onStop();
        EasyTracker.getInstance().activityStop(this);
    }

    /*
     * (non-Javadoc)
     * 
     * @see android.app.Activity#onActivityResult(int, int,
     * android.content.Intent)
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == QR_CODE_SCANNED) {
            if (resultCode == RESULT_OK) {
                String result = data.getStringExtra("SCAN_RESULT");
                try {
                    String[] keys = result.split(" ");

                    if (keys.length < SHARED_SECRET_SIZE)
                        throw new NumberFormatException();

                    int[] sharedSecret = new int[SHARED_SECRET_SIZE];
                    for (int i = 0; i < SHARED_SECRET_SIZE; i++) {
                        sharedSecret[i] = Integer.valueOf(keys[i]);
                    }

                    String space = "";
                    StringBuilder deviceName = new StringBuilder();
                    for (int i = SHARED_SECRET_SIZE; i < keys.length; i++) {
                        deviceName.append(space);
                        deviceName.append(keys[i]);
                        space = " ";
                    }

                    tracker.sendEvent("qr", "result", "scan_ok", 0L);
                    Dialogs.promptForInboundName(deviceName.toString(), sharedSecret, this);
                } catch (NumberFormatException e) {
                    tracker.sendEvent("qr", "result", "scan_fail", 0L);
                    Dialogs.onQRFail(this);
                }
            } else if (resultCode == RESULT_CANCELED) {
                tracker.sendEvent("qr", "result", "scan_canceled", 0L);
            }
        }
    }

    /**
     * Returns whether the argument <tt>deviceID</tt> is valid.
     * 
     * @param deviceID
     *            the ID to be checked
     * @return <code>true</code> if <tt>deviceID</tt> is a non-empty string that
     *         matches {@link #VALID_DEVICE_NAME} and is not in use for any
     *         other inbound device.
     */
    public boolean isValidChoice(String deviceID) {
        if (deviceID == null || deviceID.isEmpty() || !deviceID.matches(VALID_DEVICE_NAME))
            return false;

        int hashed = deviceID.hashCode();

        for (PairedDevice device : inboundDevices) {
            if (hashed == device.name.hashCode()) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns a list of all devices currently configured as inbound devices by
     * loading it from the configuration file.
     * 
     * @return a (potentially empty) list of currently paired inbound devices
     */
    @SuppressWarnings("unchecked")
    List<PairedDevice> loadInboundPairing() {
        FileInputStream fis = null;
        try {
            fis = openFileInput(INBOUND_DEVICES_FILENAME);
            ObjectInputStream ois = new ObjectInputStream(fis);
            Object read = ois.readObject();
            return (ArrayList<PairedDevice>) read;
        } catch (FileNotFoundException e) {
            // it's ok, no inbound device has been configured
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (fis != null)
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }
        return new ArrayList<PairedDevice>();

    }

    /**
     * Reloads the list of configured inbound devices from file and updates the
     * list in the UI.
     */
    void refreshInboundDevicesList() {
        runOnUiThread(new Runnable() {

            @Override
            public void run() {
                getListAdapter().notifyDataSetChanged();
            }
        });
    }

    /**
     * Loads the list of configured inbound devices from the configuration file
     * and creates an {@link ArrayAdapter} wrapping them.
     * 
     * @return a new {@link ArrayAdapter} that contains all currently configured
     *         inbound devices (potentially none)
     */
    ArrayAdapter<String> getListAdapter() {
        List<String> deviceNames = new ArrayList<String>();
        inboundDevices = loadInboundPairing();
        Utils.debug("%d device(s)", inboundDevices.size());

        for (PairedDevice device : inboundDevices) {
            deviceNames.add(device.name);
        }

        if (adapter == null) {
            adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, deviceNames);
        } else {
            adapter.clear();
            for (String deviceName : deviceNames) {
                adapter.add(deviceName);
            }
        }
        return adapter;
    }

    /**
     * Overwrites the current configuration file for outbound devices storing
     * the argument list of devices.
     * 
     * @param pairedDevices
     *            the new list of devices to be saved to disk
     */
    void writePairedInboundFile(List<PairedDevice> pairedDevices) {
        FileOutputStream fos = null;
        try {
            fos = openFileOutput(INBOUND_DEVICES_FILENAME, Context.MODE_PRIVATE);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(pairedDevices);
        } catch (IOException e) {
            // TODO should notify the user
            e.printStackTrace();
        } finally {
            if (fos != null)
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }
    }

}