/* * Copyright 2014 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.samples.apps.iosched.gcm; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.text.TextUtils; import com.google.samples.apps.iosched.Config; import com.google.samples.apps.iosched.util.AccountUtils; import java.io.IOException; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.*; import java.util.Map.Entry; import static com.google.samples.apps.iosched.util.LogUtils.*; /** * Helper class used to communicate with the demo server. */ public final class ServerUtilities { private static final String TAG = makeLogTag("GCMs"); private static final String PREFERENCES = "com.google.samples.apps.iosched.gcm"; private static final String PROPERTY_REGISTERED_TS = "registered_ts"; private static final String PROPERTY_REG_ID = "reg_id"; private static final String PROPERTY_GCM_KEY = "gcm_key"; private static final int MAX_ATTEMPTS = 5; private static final int BACKOFF_MILLI_SECONDS = 2000; private static final Random sRandom = new Random(); private static boolean checkGcmEnabled() { if (TextUtils.isEmpty(Config.GCM_SERVER_URL)) { LOGD(TAG, "GCM feature disabled (no URL configured)"); return false; } else if (TextUtils.isEmpty(Config.GCM_API_KEY)) { LOGD(TAG, "GCM feature disabled (no API key configured)"); return false; } else if (TextUtils.isEmpty(Config.GCM_SENDER_ID)) { LOGD(TAG, "GCM feature disabled (no sender ID configured)"); return false; } return true; } /** * Register this account/device pair within the server. * * @param context Current context * @param gcmId The GCM registration ID for this device * @param gcmKey The GCM key with which to register. * @return whether the registration succeeded or not. */ public static boolean register(final Context context, final String gcmId, final String gcmKey) { if (!checkGcmEnabled()) { return false; } LOGI(TAG, "registering device (gcm_id = " + gcmId + ")"); String serverUrl = Config.GCM_SERVER_URL + "/register"; LOGI(TAG, "registering on GCM with GCM key: " + AccountUtils.sanitizeGcmKey(gcmKey)); Map<String, String> params = new HashMap<String, String>(); params.put("gcm_id", gcmId); params.put("gcm_key", gcmKey); long backoff = BACKOFF_MILLI_SECONDS + sRandom.nextInt(1000); // Once GCM returns a registration id, we need to register it in the // demo server. As the server might be down, we will retry it a couple // times. for (int i = 1; i <= MAX_ATTEMPTS; i++) { LOGV(TAG, "Attempt #" + i + " to register"); try { post(serverUrl, params, Config.GCM_API_KEY); setRegisteredOnServer(context, true, gcmId, gcmKey); return true; } catch (IOException e) { // Here we are simplifying and retrying on any error; in a real // application, it should retry only on unrecoverable errors // (like HTTP error code 503). LOGE(TAG, "Failed to register on attempt " + i, e); if (i == MAX_ATTEMPTS) { break; } try { LOGV(TAG, "Sleeping for " + backoff + " ms before retry"); Thread.sleep(backoff); } catch (InterruptedException e1) { // Activity finished before we complete - exit. LOGD(TAG, "Thread interrupted: abort remaining retries!"); Thread.currentThread().interrupt(); return false; } // increase backoff exponentially backoff *= 2; } } return false; } /** * Unregister this account/device pair within the server. * * @param context Current context * @param gcmId The GCM registration ID for this device */ static void unregister(final Context context, final String gcmId) { if (!checkGcmEnabled()) { return; } LOGI(TAG, "unregistering device (gcmId = " + gcmId + ")"); String serverUrl = Config.GCM_SERVER_URL + "/unregister"; Map<String, String> params = new HashMap<String, String>(); params.put("gcm_id", gcmId); try { post(serverUrl, params, Config.GCM_API_KEY); setRegisteredOnServer(context, false, gcmId, null); } catch (IOException e) { // At this point the device is unregistered from GCM, but still // registered in the server. // We could try to unregister again, but it is not necessary: // if the server tries to send a message to the device, it will get // a "NotRegistered" error message and should unregister the device. LOGD(TAG, "Unable to unregister from application server", e); } finally { // Regardless of server success, clear local preferences setRegisteredOnServer(context, false, null, null); } } /** * Request user data sync. * * @param context Current context */ public static void notifyUserDataChanged(final Context context) { if (!checkGcmEnabled()) { return; } LOGI(TAG, "Notifying GCM that user data changed"); String serverUrl = Config.GCM_SERVER_URL + "/send/self/sync_user"; try { String gcmKey = AccountUtils.getGcmKey(context, AccountUtils.getActiveAccountName(context)); if (gcmKey != null) { post(serverUrl, new HashMap<String, String>(), gcmKey); } } catch (IOException e) { LOGE(TAG, "Unable to notify GCM about user data change", e); } } /** * Sets whether the device was successfully registered in the server side. * * @param context Current context * @param flag True if registration was successful, false otherwise * @param gcmId True if registration was successful, false otherwise */ private static void setRegisteredOnServer(Context context, boolean flag, String gcmId, String gcmKey) { final SharedPreferences prefs = context.getSharedPreferences( PREFERENCES, Context.MODE_PRIVATE); LOGD(TAG, "Setting registered on server status as: " + flag + ", gcmKey=" + AccountUtils.sanitizeGcmKey(gcmKey)); Editor editor = prefs.edit(); if (flag) { editor.putLong(PROPERTY_REGISTERED_TS, new Date().getTime()); editor.putString(PROPERTY_GCM_KEY, gcmKey == null ? "" : gcmKey); editor.putString(PROPERTY_REG_ID, gcmId); } else { editor.remove(PROPERTY_REG_ID); } editor.commit(); } /** * Checks whether the device was successfully registered in the server side. * * @param context Current context * @return True if registration was successful, false otherwise */ public static boolean isRegisteredOnServer(Context context, String gcmKey) { final SharedPreferences prefs = context.getSharedPreferences( PREFERENCES, Context.MODE_PRIVATE); // Find registration threshold Calendar cal = Calendar.getInstance(); cal.add(Calendar.DATE, -1); long yesterdayTS = cal.getTimeInMillis(); long regTS = prefs.getLong(PROPERTY_REGISTERED_TS, 0); gcmKey = gcmKey == null ? "" : gcmKey; if (regTS > yesterdayTS) { LOGV(TAG, "GCM registration current. regTS=" + regTS + " yesterdayTS=" + yesterdayTS); final String registeredGcmKey = prefs.getString(PROPERTY_GCM_KEY, ""); if (registeredGcmKey.equals(gcmKey)) { LOGD(TAG, "GCM registration is valid and for the correct gcm key: " + AccountUtils.sanitizeGcmKey(registeredGcmKey)); return true; } LOGD(TAG, "GCM registration is for DIFFERENT gcm key " + AccountUtils.sanitizeGcmKey(registeredGcmKey) + ". We were expecting " + AccountUtils.sanitizeGcmKey(gcmKey)); return false; } else { LOGV(TAG, "GCM registration expired. regTS=" + regTS + " yesterdayTS=" + yesterdayTS); return false; } } public static String getGcmId(Context context) { final SharedPreferences prefs = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE); return prefs.getString(PROPERTY_REG_ID, null); } /** * Unregister the current GCM ID when we sign-out * * @param context Current context */ public static void onSignOut(Context context) { String gcmId = getGcmId(context); if (gcmId != null) { unregister(context, gcmId); } } /** * Issue a POST request to the server. * * @param endpoint POST address. * @param params request parameters. * @throws java.io.IOException propagated from POST. */ private static void post(String endpoint, Map<String, String> params, String key) throws IOException { URL url; try { url = new URL(endpoint); } catch (MalformedURLException e) { throw new IllegalArgumentException("invalid url: " + endpoint); } params.put("key", key); StringBuilder bodyBuilder = new StringBuilder(); Iterator<Entry<String, String>> iterator = params.entrySet().iterator(); // constructs the POST body using the parameters while (iterator.hasNext()) { Entry<String, String> param = iterator.next(); bodyBuilder.append(param.getKey()).append('=') .append(param.getValue()); if (iterator.hasNext()) { bodyBuilder.append('&'); } } String body = bodyBuilder.toString(); LOGV(TAG, "Posting '" + body + "' to " + url); HttpURLConnection conn = null; try { conn = (HttpURLConnection) url.openConnection(); conn.setDoOutput(true); conn.setUseCaches(false); conn.setChunkedStreamingMode(0); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"); conn.setRequestProperty("Content-Length", Integer.toString(body.length())); // post the request OutputStream out = conn.getOutputStream(); out.write(body.getBytes()); out.close(); // handle the response int status = conn.getResponseCode(); if (status != 200) { throw new IOException("Post failed with error code " + status); } } finally { if (conn != null) { conn.disconnect(); } } } }