/**
* GCMIntentService.java Created on 19 Jun 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 it.mb.whatshare.MainActivity.PairedDevice;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.StreamCorruptedException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import com.google.android.gcm.GCMBaseIntentService;
import com.google.android.gcm.GCMRegistrar;
/**
* Issues notifications to the system whenever messages come from GCM.
*
* @author Michele Bonazza
*/
public class GCMIntentService extends GCMBaseIntentService {
/**
* Lock to synchronize with {@link GCMIntentService}.
*/
private static final Lock REGISTRATION_LOCK = new ReentrantLock();
private static final String PROJECT_SERVER_ID = "213874322054";
private static final String UNKNOWN_GCM_ERROR = "UNKNOWN_GCM_ERROR";
private static final Map<String, Integer> ERROR_CODES = new HashMap<String, Integer>() {
private static final long serialVersionUID = 649852390172936228L;
{
put("ACCOUNT_MISSING", R.string.account_missing);
put("TOO_MANY_REGISTRATIONS", R.string.too_many_registrations);
put("INVALID_SENDER", R.string.invalid_sender);
put("PHONE_REGISTRATION_ERROR", R.string.phone_registration_error);
put(UNKNOWN_GCM_ERROR, R.string.unknown_gcm_error);
}
};
/**
* Condition that is released when {@link GCMIntentService} is done with the
* registration process.
*/
private static final Condition REGISTERED = REGISTRATION_LOCK
.newCondition();
private static AtomicInteger counter = new AtomicInteger();
private static String registrationID = "";
private static int errorID = -1;
private Set<String> senderWhitelist = new HashSet<String>();
private long lastCheckedWhitelist;
/**
* Creates a new intent service for the application.
*/
public GCMIntentService() {
super(PROJECT_SERVER_ID);
}
/**
* Registers the device with the GCM server.
*
* @param activity
* the calling activity
*/
static void registerWithGCM(Context activity) {
if ("".equals(registrationID)) {
GCMRegistrar.checkDevice(activity);
GCMRegistrar.checkManifest(activity);
// register using the project ID
GCMRegistrar.register(activity, PROJECT_SERVER_ID);
}
}
/**
* Returns the registration ID for this device, waiting for GCM to give this
* device one if it hasn't done so before.
*
* @return the registration ID
* @throws CantRegisterWithGCMException
* in case an error message is received from GCM when trying to
* register this device
*/
static String getRegistrationID() throws CantRegisterWithGCMException {
if ("".equals(registrationID)) {
try {
REGISTRATION_LOCK.lock();
// check again within lock
if ("".equals(registrationID) && errorID == -1) {
REGISTERED.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
REGISTRATION_LOCK.unlock();
if (errorID != -1) {
int error = errorID;
errorID = -1;
throw new CantRegisterWithGCMException(error);
}
}
}
return registrationID;
}
private void readWhitelist() {
File whitelist = new File(getFilesDir(),
MainActivity.INBOUND_DEVICES_FILENAME);
// if whitelist doesn't exist, don't bother
long lastModified = whitelist.exists() ? whitelist.lastModified()
: Long.MIN_VALUE;
if (lastCheckedWhitelist < lastModified) {
FileInputStream fis = null;
try {
fis = openFileInput(MainActivity.INBOUND_DEVICES_FILENAME);
ObjectInputStream ois = new ObjectInputStream(fis);
Object read = ois.readObject();
@SuppressWarnings("unchecked")
List<PairedDevice> devices = (ArrayList<PairedDevice>) read;
senderWhitelist = new HashSet<String>();
for (PairedDevice device : devices) {
try {
senderWhitelist
.add(String.valueOf(device.id.hashCode()));
} catch (NullPointerException e) {
// backward compatibility... devices didn't have an ID
senderWhitelist.add(String.valueOf(device.name
.hashCode()));
}
}
} catch (FileNotFoundException e) {
// it's ok, no whitelist, all messages are rejected
} catch (StreamCorruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO here the error should be notified I guess
e.printStackTrace();
} finally {
if (fis != null)
try {
fis.close();
} catch (IOException e) {
// can't do much...
e.printStackTrace();
}
}
// whatever happens, don't keep checking for nothing
lastCheckedWhitelist = lastModified;
}
}
/*
* (non-Javadoc)
*
* @see
* com.google.android.gcm.GCMBaseIntentService#onError(android.content.Context
* , java.lang.String)
*/
@Override
protected void onError(Context arg0, String arg1) {
Utils.checkDebug(arg0);
try {
REGISTRATION_LOCK.lock();
Utils.debug("Cannot register: %s", arg1);
registrationID = "";
errorID = ERROR_CODES.get(UNKNOWN_GCM_ERROR);
if (arg1 != null) {
Integer error = ERROR_CODES.get(arg1);
if (error != null)
errorID = error;
}
REGISTERED.signalAll();
} finally {
REGISTRATION_LOCK.unlock();
}
}
/*
* (non-Javadoc)
*
* @see
* com.google.android.gcm.GCMBaseIntentService#onRecoverableError(android
* .content.Context, java.lang.String)
*/
@Override
protected boolean onRecoverableError(Context context, String errorId) {
Utils.checkDebug(context);
return super.onRecoverableError(context, errorId);
}
/*
* (non-Javadoc)
*
* @see
* com.google.android.gcm.GCMBaseIntentService#onMessage(android.content
* .Context, android.content.Intent)
*/
@Override
protected void onMessage(Context arg0, Intent arg1) {
Utils.checkDebug(arg0);
Bundle bundle = arg1.getExtras();
String sender = bundle.getString("sender");
Utils.debug("new incoming message from %s: %s", sender,
bundle.getString("message"));
readWhitelist();
if (senderWhitelist.contains(sender)) {
String type = bundle.getString(MainActivity.INTENT_TYPE_EXTRA);
if (type == null) {
type = MainActivity.SHARE_VIA_WHATSAPP_EXTRA;
}
generateNotification(arg0, bundle.getString("message"),
MainActivity.SHARE_VIA_WHATSAPP_EXTRA.equals(type));
} else {
Utils.debug("ignoring message from %s, not in the whitelist",
sender);
}
}
/**
* Creates a notification informing the user that new content is to be
* shared.
*
* @param context
* the current application context
* @param message
* the message to be shared
* @param useWhatsapp
* whether the content should be shared bypassing the app choice
* dialog calling whatsapp directly instead
*/
@SuppressWarnings("deprecation")
public void generateNotification(Context context, String message,
boolean useWhatsapp) {
// @formatter:off
String title = context.getString(R.string.whatshare);
Class<?> dst = useWhatsapp ? SendToWhatsappActivity.class : SendToAppActivity.class;
// setAction is called so filterEquals() always returns false for all
// our intents
Intent whatshareIntent = new Intent(context, dst)
.putExtra("message", message)
.setAction(String.valueOf(System.currentTimeMillis()))
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent intent = PendingIntent.getActivity(context, 0,
whatshareIntent, Intent.FLAG_ACTIVITY_NEW_TASK);
Notification notification = null;
NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
.setSmallIcon(useWhatsapp ? R.drawable.notification_icon : R.drawable.whatshare_logo_notification, 0)
.setContentTitle(title)
.setContentText(message)
.setContentIntent(intent)
.setDefaults(Notification.DEFAULT_ALL);
// @formatter:on
if (Build.VERSION.SDK_INT > 15) {
notification = buildForJellyBean(builder);
} else {
notification = builder.getNotification();
}
notification.flags |= Notification.FLAG_AUTO_CANCEL;
((NotificationManager) context
.getSystemService(Context.NOTIFICATION_SERVICE)).notify(
counter.incrementAndGet(), notification);
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private Notification buildForJellyBean(NotificationCompat.Builder builder) {
return builder.build();
}
/*
* (non-Javadoc)
*
* @see
* com.google.android.gcm.GCMBaseIntentService#onRegistered(android.content
* .Context, java.lang.String)
*/
@Override
protected void onRegistered(Context arg0, String arg1) {
Utils.checkDebug(arg0);
try {
REGISTRATION_LOCK.lock();
Utils.debug("Now app is registered with ID %s", arg1);
registrationID = arg1;
REGISTERED.signalAll();
} finally {
REGISTRATION_LOCK.unlock();
}
}
/*
* (non-Javadoc)
*
* @see
* com.google.android.gcm.GCMBaseIntentService#onUnregistered(android.content
* .Context, java.lang.String)
*/
@Override
protected void onUnregistered(Context arg0, String arg1) {
// nothing to do here
}
}