/**
* PairOutboundActivity.java Created on 16 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 static it.mb.whatshare.CallGooGlInbound.CHARACTERS;
import static it.mb.whatshare.CallGooGlInbound.CHAR_MAP;
import it.mb.whatshare.MainActivity.PairedDevice;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OptionalDataException;
import java.io.PrintStream;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.text.InputFilter;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.util.Pair;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import com.google.analytics.tracking.android.EasyTracker;
import com.google.analytics.tracking.android.GoogleAnalytics;
import com.google.analytics.tracking.android.Tracker;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
/**
* Activity displayed when pairing this device so that it can share stuff to a
* paired Android device on which Whatsapp is installed.
*
* @author Michele Bonazza
*
*/
public class PairOutboundActivity extends FragmentActivity {
/**
* The name of the file that keeps reference of the device (which has
* Whatsapp installed) used to send messages to.
*/
public static final String PAIRING_FILE_NAME = "pairing";
/**
* An asynchronous task to call Google's URL shortener service in order to
* retrieve information stored within the expanded URL by the outbound
* device (the one where Whatsapp is installed).
*
* @author Michele Bonazza
*/
private class CallGooGlOutbound extends AsyncTask<String, Void, Void> {
private ProgressDialog dialog;
private PairedDevice paired;
/*
* (non-Javadoc)
*
* @see android.os.AsyncTask#onPreExecute()
*/
@Override
protected void onPreExecute() {
dialog = ProgressDialog.show(PairOutboundActivity.this,
getResources().getString(R.string.please_wait),
getResources().getString(R.string.wait_message));
}
/*
* (non-Javadoc)
*
* @see android.os.AsyncTask#onPostExecute(java.lang.Object)
*/
@Override
protected void onPostExecute(Void result) {
super.onPostExecute(result);
dialog.dismiss();
Dialogs.onPairingOutbound(paired, PairOutboundActivity.this);
}
/*
* (non-Javadoc)
*
* @see android.os.AsyncTask#doInBackground(Params[])
*/
@Override
protected Void doInBackground(String... params) {
paired = expand(params[0]);
return null;
}
private PairedDevice expand(String url) {
HttpGet get = new HttpGet(String.format(
EXPANDER_URL,
PairOutboundActivity.this.getResources().getString(
R.string.android_shortener_key), url));
String response;
try {
response = new DefaultHttpClient().execute(get,
new BasicResponseHandler());
String longUrl = new JSONObject(response).getString("longUrl");
Utils.debug("response is %s", longUrl);
Matcher matcher = EXPANDED_URL.matcher(longUrl);
if (matcher.matches()) {
String domain = matcher.group(1);
int sum = Integer.parseInt(matcher.group(2));
String model = matcher.group(3);
String assignedID = matcher.group(4);
String id = matcher.group(5);
Utils.debug(
"domain=%s, sum=%d, model=%s, assignedID=%s, id=%s",
domain, sum, model, assignedID, id);
if (checksum(domain, sum)) {
Utils.debug("Checksum ok!");
tracker.sendEvent("googl", "parse_expanded", "ok", 0L);
return new PairedDevice(decodeId(assignedID),
decodeId(id), URLDecoder.decode(model, "UTF-8"));
} else {
tracker.sendEvent("googl", "parse_expanded",
"bad_checksum", 0L);
Utils.debug("Checksum bad");
}
} else {
tracker.sendEvent("googl", "parse_expanded", "wrong_url",
0L);
Utils.debug("wrong URL");
}
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
tracker.sendEvent("googl", "parse_expanded",
"ioexception " + e.getMessage(), 0L);
e.printStackTrace();
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
private boolean checksum(String toCheck, int toMatch) {
int sum = 0;
for (int i = 0; i < toCheck.length(); i++) {
sum += CHAR_MAP.get(toCheck.charAt(i));
}
return sum == toMatch;
}
private String decodeId(String id) {
StringBuilder decoded = new StringBuilder();
for (int i = 0; i < id.length(); i++) {
int c = CHAR_MAP.get(id.charAt(i));
int index = (c - randomSeed.get(i % randomSeed.size()))
% CHARACTERS.length;
if (index < 0) {
index = CHARACTERS.length + index;
}
decoded.append(CHARACTERS[index]);
}
return decoded.toString();
}
}
private static final String EXPANDER_URL = MainActivity.SHORTENER_URL
+ "&shortUrl=http://goo.gl/%s";
private static final Pattern EXPANDED_URL = Pattern
.compile("http://([^/]+)/(\\d+)\\?model\\=([^&]+)&yourid=([a-zA-Z0-9\\-\\_]+)&id=([a-zA-Z0-9\\-\\_]+)");
private static final int MAX_SHORTENED_URL_LENGTH = 6;
private static String assignedID;
private EditText inputCode;
private List<Integer> randomSeed;
private Tracker tracker;
private boolean keepKeyboardVisible;
/**
* Returns the ID that the currently paired outbound device has given to
* this device and must thus be included in messages sent to that device.
*
* @param context
* the current activity to access file with
* @return the ID to be included in all messages sent to the currently
* paired outbound device, <code>null</code> if no device is
* currently paired in outbound
*/
static String getAssignedID(ContextWrapper context) {
if (assignedID == null) {
try {
Pair<PairedDevice, String> paired = SendToGCMActivity
.loadOutboundPairing(context);
assignedID = paired.second;
} catch (OptionalDataException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return assignedID;
}
/*
* (non-Javadoc)
*
* @see android.app.Activity#onCreate(android.os.Bundle)
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Utils.checkDebug(this);
onNewIntent(getIntent());
}
/*
* (non-Javadoc)
*
* @see android.app.Activity#onNewIntent(android.content.Intent)
*/
@Override
protected void onNewIntent(Intent intent) {
tracker = GoogleAnalytics.getInstance(this).getDefaultTracker();
showPairingLayout();
}
private void showPairingLayout() {
View view = getLayoutInflater().inflate(R.layout.activity_qrcode, null);
setContentView(view);
String paired = getOutboundPaired();
if (paired != null) {
((TextView) findViewById(R.id.qr_instructions)).setText(getString(
R.string.new_outbound_instructions, paired));
}
inputCode = (EditText) findViewById(R.id.inputCode);
inputCode.setFilters(new InputFilter[] { new InputFilter() {
/*
* (non- Javadoc )
*
* @see android .text. InputFilter # filter( java .lang.
* CharSequence , int, int, android .text. Spanned , int, int)
*/
@Override
public CharSequence filter(CharSequence source, int start, int end,
Spanned dest, int dstart, int dend) {
if (source instanceof SpannableStringBuilder) {
SpannableStringBuilder sourceAsSpannableBuilder = (SpannableStringBuilder) source;
for (int i = end - 1; i >= start; i--) {
char currentChar = source.charAt(i);
if (!Character.isLetterOrDigit(currentChar)) {
sourceAsSpannableBuilder.delete(i, i + 1);
}
}
return source;
} else {
StringBuilder filteredStringBuilder = new StringBuilder();
for (int i = 0; i < end; i++) {
char currentChar = source.charAt(i);
if (Character.isLetterOrDigit(currentChar)) {
filteredStringBuilder.append(currentChar);
}
}
return filteredStringBuilder.toString();
}
}
}, new InputFilter.LengthFilter(MAX_SHORTENED_URL_LENGTH) });
inputCode.setOnEditorActionListener(new OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId,
KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
onSubmitPressed(null);
return keepKeyboardVisible;
}
return false;
}
});
final ImageView qrWrapper = (ImageView) findViewById(R.id.qr_code);
qrWrapper.getViewTreeObserver().addOnGlobalLayoutListener(
new OnGlobalLayoutListener() {
private boolean createdQRCode = false;
@Override
public void onGlobalLayout() {
if (!createdQRCode) {
try {
Bitmap qrCode = generateQRCode(
generateRandomSeed(),
getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ? qrWrapper
.getHeight() : qrWrapper
.getWidth());
if (qrCode != null) {
qrWrapper.setImageBitmap(qrCode);
}
createdQRCode = true;
} catch (WriterException e) {
e.printStackTrace();
}
}
}
});
}
/*
* (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);
}
private String getOutboundPaired() {
try {
Pair<PairedDevice, String> paired = SendToGCMActivity
.loadOutboundPairing(this);
if (paired != null) {
assignedID = paired.second;
return paired.first.type;
}
} catch (OptionalDataException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (FileNotFoundException e) {
// it's ok
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
/**
* Saves the argument <tt>device</tt> as the (only) configured outbound
* device.
*
* <p>
* If <tt>device</tt> is <code>null</code>, the currently configured device
* is deleted.
*
* @param device
* the device to be stored, or <code>null</code> if the current
* association must be discarded
* @param context
* the application's context (used to open the association file
* with)
* @throws IOException
* in case something is wrong with the file
* @throws JSONException
* in case something is wrong with the argument <tt>device</tt>
*/
public static void savePairing(PairedDevice device, Context context)
throws IOException, JSONException {
if (device == null) {
Utils.debug("deleting outbound device... %s",
context.deleteFile(PAIRING_FILE_NAME) ? "success" : "fail");
} else {
FileOutputStream fos = context.openFileOutput(PAIRING_FILE_NAME,
Context.MODE_PRIVATE);
// @formatter:off
JSONObject json = new JSONObject()
.put("name", device.name)
.put("type", device.type)
.put("assignedID", device.id);
// @formatter:on
PrintStream writer = new PrintStream(fos);
writer.append(json.toString());
writer.flush();
writer.close();
}
}
/**
* Called when submit button below the QR code is pressed or after the
* pairing code typed by the user has been submitted.
*
* <p>
* This method also decides whether the soft keyboard should be kept visible
* (in case the pairing code typed by the user is not valid).
*
* @param view
* the parent view
*/
public void onSubmitPressed(View view) {
String code = inputCode.getText().toString();
Utils.debug("user submitted %s", code);
if (code.length() > MAX_SHORTENED_URL_LENGTH) {
inputCode.setError(getString(R.string.invalidshorturl));
keepKeyboardVisible = true;
} else {
keepKeyboardVisible = false;
}
new CallGooGlOutbound().execute(code);
}
private Bitmap generateQRCode(String dataToEncode, int imageViewSize)
throws WriterException {
MultiFormatWriter writer = new MultiFormatWriter();
BitMatrix matrix = writer.encode(dataToEncode, BarcodeFormat.QR_CODE,
imageViewSize, imageViewSize);
int width = matrix.getWidth();
int height = matrix.getHeight();
int[] pixels = new int[width * height];
for (int y = 0; y < height; y++) {
int offset = y * width;
for (int x = 0; x < width; x++) {
pixels[offset + x] = matrix.get(x, y) ? 0xff000000 : 0xffffffff;
}
}
Bitmap bitmap = Bitmap.createBitmap(width, height,
Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
return bitmap;
}
private String generateRandomSeed() {
StringBuilder builder = new StringBuilder();
String whitespace = "";
Random gen = new Random();
// I know, it's 4 (half) bytes so I could stuff them into a single int,
// but I'm lazy and er.. it's open to future larger keys?
randomSeed = new ArrayList<Integer>();
for (int i = 0; i < 4; i++) {
builder.append(whitespace);
int nextInt = gen.nextInt(128);
builder.append(nextInt);
randomSeed.add(nextInt);
whitespace = " ";
}
builder.append(String.format(" %s %s",
Utils.capitalize(Build.MANUFACTURER), Build.MODEL));
Utils.debug("stuffing this into the QR code: %s", builder.toString());
return builder.toString();
}
}