/*
* AndFHEM - Open Source Android application to control a FHEM home automation
* server.
*
* Copyright (c) 2011, Matthias Klass or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU GENERAL PUBLIC LICENSE, as published by the Free Software Foundation.
*
* 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 distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package li.klass.fhem.gcm;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import com.google.android.gcm.GCMBaseIntentService;
import com.google.android.gcm.GCMRegistrar;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.security.Key;
import java.util.Map;
import java.util.Set;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.inject.Inject;
import li.klass.fhem.AndFHEMApplication;
import li.klass.fhem.activities.AndFHEMMainActivity;
import li.klass.fhem.constants.Actions;
import li.klass.fhem.constants.BundleExtraKeys;
import li.klass.fhem.domain.core.FhemDevice;
import li.klass.fhem.service.intent.RoomListIntentService;
import li.klass.fhem.service.room.RoomListService;
import li.klass.fhem.util.ApplicationProperties;
import li.klass.fhem.util.NotificationUtil;
import li.klass.fhem.util.Tasker;
import static com.google.common.collect.Maps.newHashMap;
import static li.klass.fhem.constants.PreferenceKeys.GCM_PROJECT_ID;
import static li.klass.fhem.constants.PreferenceKeys.GCM_REGISTRATION_ID;
public class GCMIntentService extends GCMBaseIntentService {
private static final Logger LOG = LoggerFactory.getLogger(GCMIntentService.class);
private static final Set<String> DECRYPT_KEYS = ImmutableSet.of("type", "notifyId", "changes", "deviceName", "tickerText", "contentText", "contentTitle");
@Inject
ApplicationProperties applicationProperties;
@Inject
RoomListService roomListService;
@Override
public void onCreate() {
super.onCreate();
((AndFHEMApplication) getApplication()).getDaggerComponent().inject(this);
}
@Override
protected void onRegistered(Context context, String registrationId) {
applicationProperties.setSharedPreference(GCM_REGISTRATION_ID, registrationId, context);
LOG.info("onRegistered - device registered with regId {}", registrationId);
Intent intent = new Intent(Actions.GCM_REGISTERED);
intent.putExtra(BundleExtraKeys.GCM_REGISTRATION_ID, registrationId);
sendBroadcast(intent);
}
@Override
protected void onUnregistered(Context context, String registrationId) {
LOG.info("onUnregistered - device unregistered");
if (GCMRegistrar.isRegisteredOnServer(context)) {
GCMRegistrar.unregister(this);
} else {
LOG.info("onUnregistered - Ignoring unregister callback");
}
}
@Override
protected void onMessage(Context context, Intent intent) {
Bundle extras = intent.getExtras();
if (extras == null) return;
LOG.info(TAG, "onMessage - received GCM message with content: {}", extras);
if (!extras.containsKey("type") || !extras.containsKey("source")) {
LOG.info(TAG, "onMessage - received GCM message, but doesn't fit required fields");
return;
}
extras = decrypt(extras);
String type = extras.getString("type");
if ("message".equalsIgnoreCase(type)) {
handleMessage(extras);
} else if ("notify".equalsIgnoreCase(type) || Strings.isNullOrEmpty(type)) {
handleNotify(extras);
} else {
LOG.error("onMessage - unknown type: {}", type);
}
}
private Bundle decrypt(Bundle extras) {
if (!extras.containsKey("gcmDeviceName")) {
return extras;
}
Optional<FhemDevice> device = roomListService.getDeviceForName(extras.getString("gcmDeviceName"), Optional.<String>absent(), this);
if (!device.isPresent()) {
return extras;
}
Optional<String> cryptKey = device.get().getXmlListDevice().getAttribute("cryptKey");
if (!cryptKey.isPresent()) {
return extras;
}
return decrypt(extras, cryptKey.get());
}
private Bundle decrypt(Bundle extras, String cryptKey) {
Optional<Cipher> cipherOptional = cipherFor(cryptKey);
if (!cipherOptional.isPresent()) {
return extras;
}
Cipher cipher = cipherOptional.get();
Bundle newBundle = new Bundle();
newBundle.putAll(extras);
for (String key : extras.keySet()) {
if (DECRYPT_KEYS.contains(key)) {
newBundle.putString(key, decrypt(cipher, extras.getString(key)));
}
}
return newBundle;
}
private String decrypt(Cipher cipher, String value) {
try {
byte[] hexBytes = Hex.decodeHex(value.toCharArray());
return new String(cipher.doFinal(hexBytes));
} catch (Exception e) {
e.printStackTrace();
LOG.error("decrypt(" + value + ")", e);
return value;
}
}
@NonNull
private Optional<Cipher> cipherFor(String key) {
try {
byte[] keyBytes = key.getBytes();
Key skey = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ivSpec = new IvParameterSpec(keyBytes);
cipher.init(Cipher.DECRYPT_MODE, skey, ivSpec);
return Optional.of(cipher);
} catch (Exception e) {
LOG.error("cipherFor - cannot create cipher", e);
return Optional.absent();
}
}
private void handleMessage(Bundle extras) {
int notifyId = 1;
try {
if (extras.containsKey("notifyId")) {
notifyId = Integer.valueOf(extras.getString("notifyId"));
}
} catch (Exception e) {
LOG.error("handleMessage - invalid notify id: {}", extras.getString("notifyId"));
}
Intent openIntent = new Intent(this, AndFHEMMainActivity.class);
openIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
openIntent.putExtra("unique", "foobar://" + SystemClock.elapsedRealtime());
PendingIntent pendingIntent = PendingIntent.getActivity(this, notifyId, openIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
NotificationUtil.notify(this, notifyId, pendingIntent, extras.getString("contentTitle"),
extras.getString("contentText"), extras.getString("tickerText"),
shouldVibrate(extras));
}
private void handleNotify(Bundle extras) {
if (!extras.containsKey("changes")) return;
String deviceName = extras.getString("deviceName");
String changesText = extras.getString("changes");
if (changesText == null) return;
startService(new Intent(Actions.UPDATE_DEVICE_WITH_UPDATE_MAP)
.setClass(this, RoomListIntentService.class)
.putExtra(BundleExtraKeys.DEVICE_NAME, deviceName)
.putExtra(BundleExtraKeys.UPDATE_MAP, (Serializable) extractChanges(deviceName, changesText))
.putExtra(BundleExtraKeys.VIBRATE, shouldVibrate(extras)));
}
Map<String, String> extractChanges(String deviceName, String changesText) {
String[] changes = changesText.split("<\\|>");
Map<String, String> changeMap = newHashMap();
for (String change : changes) {
String[] parts = change.split(":");
if (parts.length < 2) continue;
String key, value;
if (parts.length > 2) {
key = "state";
value = change;
} else {
key = parts[0].trim();
value = parts[1].trim();
}
Tasker.sendTaskerNotifyIntent(this, deviceName, key, value);
changeMap.put(key, value);
}
return changeMap;
}
private boolean shouldVibrate(Bundle extras) {
return extras.containsKey("vibrate") && "true".equalsIgnoreCase(extras.getString("vibrate"));
}
@Override
protected void onDeletedMessages(Context context, int total) {
LOG.info("onDeletedMessages - Received deleted messages notification");
}
@Override
public void onError(Context context, String errorId) {
LOG.info("onError - received error: " + errorId);
}
@Override
protected boolean onRecoverableError(Context context, String errorId) {
LOG.info("onRecoverableError - errorId={}", errorId);
return super.onRecoverableError(context, errorId);
}
@Override
protected String[] getSenderIds(Context context) {
String projectId = applicationProperties.getStringSharedPreference(GCM_PROJECT_ID, null, context);
if (Strings.isNullOrEmpty(projectId)) {
return new String[]{};
}
return new String[]{projectId};
}
}