Java tutorial
/** * 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(); } } } }