package com.buddy.sdk; import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.location.Location; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; import android.provider.Settings; import android.util.Log; import com.buddy.sdk.models.NotificationResult; import com.buddy.sdk.models.TimedMetric; import com.buddy.sdk.models.User; import com.google.gson.Gson; import com.google.gson.JsonObject; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; public class BuddyClientImpl implements BuddyClient { private String app_id; private String app_key; private BuddyServiceClientImpl serviceClient; private BuddyClientOptions options; private Context context; private Location lastLocation; private String sharedSecret; // Stored here and not in BuddyClientOptions as we don't want to serialize it to stable storage private UserAuthenticationRequiredCallback userAuthCallback; private ConnectivityLevelChangedCallback connectivityLevelChangedCallback; private ConnectivityManager connectivityManager; private ConnectivityLevel _connectivityLevel = ConnectivityLevel.Connected; public BuddyClientImpl(Context context, String appId, String appKey) { this(context, appId, appKey, null); } public BuddyClientImpl(Context context, String appId, String appKey, BuddyClientOptions options) { this.app_id = appId; this.app_key = appKey; this.context = context; BuddyClientSettings settings = getSettings(); if (options == null) { this.options = new BuddyClientOptions(); } else { this.options = options; this.sharedSecret = options.sharedSecret; options.sharedSecret = null; } //if options was null this would always NPE if (this.options.serviceRoot != null && settings.serviceRoot == null) { settings.serviceRoot = this.options.serviceRoot; } getServiceClient(); if (context != null) { this.connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); } } public void setUserAuthenticationRequiredCallback(UserAuthenticationRequiredCallback callback) { this.userAuthCallback = callback; } public void setConnectivityLevelChangedCallback(ConnectivityLevelChangedCallback callback) { this.connectivityLevelChangedCallback = callback; } public void setLastLocation(Location loc) { lastLocation = loc; } public Location getLastLocation() { return lastLocation; } void setDefaultParameters(Map<String, Object> parameters) { if (lastLocation != null && !parameters.containsKey("location")) { parameters.put("location", lastLocation); } } void runOnUiThread(Runnable r) { if (context != null) { // Get a handler that can be used to post to the main thread Handler mainHandler = new Handler(Looper.getMainLooper()); mainHandler.post(r); } else { r.run(); } } private String makeServerDevicesSignature(String apiKey, String Secret) { String stringToSign = String.format("%s\n", apiKey); return serviceClient.signString(stringToSign, Secret); } public void getAccessToken(boolean autoRegister, final AccessTokenCallback callback) { String token = getSettings().getAccessToken(); if (token != null) { callback.completed(null, token); } else if (autoRegister) { registerDevice(new BuddyCallback<AccessTokenResult>(AccessTokenResult.class) { @Override public void completed(BuddyResult<AccessTokenResult> result) { if (result.getIsSuccess()) { AccessTokenResult atr = result.getResult(); if (sharedSecret != null) { String serverSig = makeServerDevicesSignature(app_key, sharedSecret); if (!serverSig.equals(atr.serverSignature)) { callback.completed(result.convert(Boolean.FALSE), null); return; } } BuddyClientSettings settings = getSettings(); settings.deviceToken = atr.accessToken; settings.deviceTokenExpires = atr.accessTokenExpires; settings.appVersion = options.appVersion; if (atr.serviceRoot != null) { settings.serviceRoot = atr.serviceRoot; } saveSettings(); callback.completed(null, atr.accessToken); } else { callback.completed(result.convert(Boolean.FALSE), null); } } }); } else { callback.completed(null, null); } } public String getAppId() { return app_id; } public String getSharedSecret() { return sharedSecret; } // // REST Stuff // public String getServiceRoot() { return getSettings().getServiceRoot(); } BuddyServiceClient getServiceClient() { if (serviceClient == null) { serviceClient = new BuddyServiceClientImpl(this); serviceClient.setSynchronousMode(options.synchronousMode); } return serviceClient; } public <T> Future<BuddyResult<T>> get(String path, Map<String, Object> parameters, Class<T> clazz) { return getServiceClient().makeRequest(BuddyServiceClient.GET, path, parameters, null, clazz); } public <T> Future<BuddyResult<T>> get(String path, Map<String, Object> parameters, final BuddyCallback<T> callback) { return getServiceClient().makeRequest(BuddyServiceClient.GET, path, parameters, callback, null); } public <T> Future<BuddyResult<T>> post(String path, Map<String, Object> parameters, Class<T> clazz) { return getServiceClient().makeRequest(BuddyServiceClient.POST, path, parameters, null, clazz); } public <T> Future<BuddyResult<T>> post(String path, Map<String, Object> parameters, final BuddyCallback<T> callback) { return getServiceClient().makeRequest(BuddyServiceClient.POST, path, parameters, callback, null); } public <T> Future<BuddyResult<T>> patch(String path, Map<String, Object> parameters, Class<T> clazz) { return getServiceClient().makeRequest(BuddyServiceClient.PATCH, path, parameters, null, clazz); } public <T> Future<BuddyResult<T>> patch(String path, Map<String, Object> parameters, final BuddyCallback<T> callback) { return getServiceClient().makeRequest(BuddyServiceClient.PATCH, path, parameters, callback, null); } public <T> Future<BuddyResult<T>> delete(String path, Map<String, Object> parameters, Class<T> clazz) { return getServiceClient().makeRequest(BuddyServiceClient.DELETE, path, parameters, null, clazz); } public <T> Future<BuddyResult<T>> delete(String path, Map<String, Object> parameters, final BuddyCallback<T> callback) { return getServiceClient().makeRequest(BuddyServiceClient.DELETE, path, parameters, callback, null); } public <T> Future<BuddyResult<T>> put(String path, Map<String, Object> parameters, Class<T> clazz) { return getServiceClient().makeRequest(BuddyServiceClient.PUT, path, parameters, null, clazz); } public <T> Future<BuddyResult<T>> put(String path, Map<String, Object> parameters, final BuddyCallback<T> callback) { return getServiceClient().makeRequest(BuddyServiceClient.PUT, path, parameters, callback, null); } private String getDeviceId() { if (context != null) { return Settings.Secure.getString( context.getContentResolver(), Settings.Secure.ANDROID_ID); } return null; } private class AccessTokenResult { public String accessToken; public Date accessTokenExpires; public String serviceRoot; public String serverSignature; } public static final String NoRegisterDevice = "__noregdevice"; private void registerDevice(final BuddyCallback<AccessTokenResult> callback) { Map<String, Object> parameters = new HashMap<String, Object>(); parameters.put("platform", "Android"); parameters.put("model", android.os.Build.MODEL); parameters.put("osVersion", android.os.Build.VERSION.RELEASE); if (options.deviceTag != null) { parameters.put("tag", options.deviceTag); } BuddyClientSettings settings = getSettings(); if (settings.pushToken != null) { parameters.put("pushToken", settings.pushToken); } if (context != null) { PackageManager manager = context.getPackageManager(); PackageInfo info; try { info = manager.getPackageInfo(context.getPackageName(), 0); parameters.put("appVersion", String.format("%s (%d)", info.versionName, info.versionCode)); } catch (PackageManager.NameNotFoundException e) { } } parameters.put("uniqueId", getDeviceId()); parameters.put("appid", app_id); parameters.put("appkey", app_key); if (options.appVersion != null) { parameters.put("appversion", options.appVersion); } parameters.put(NoRegisterDevice, true); this.post("/devices", parameters, callback); } // // User Stuff // public Future<BuddyResult<User>> getCurrentUser(final BuddyCallback<User> callback) { return this.get("/users/me", null, new BuddyCallback<User>(User.class) { @Override public void completed(BuddyResult<User> result) { if (callback != null) { callback.completed(result); } } }); } private BuddyCallback<User> getUserCallback(final BuddyCallback<User> callback) { return new BuddyCallback<User>(User.class) { @Override public void completed(BuddyResult<User> result) { if (result.getIsSuccess()) { JsonObject json; json = result.getResult().getJsonObject(); if (json != null && json.has("accessToken")) { BuddyClientSettings settings = getSettings(); settings.userToken = json.get("accessToken").getAsString(); settings.userTokenExpires = BuddyDateDeserializer.deserialize(json.get("accessTokenExpires").getAsString()); settings.userid = result.getResult().id; saveSettings(); } } if (callback != null) { callback.completed(result); } } }; } public Future<BuddyResult<User>> createUser(String username, String password, String firstName, String lastName, String email, Date dateOfBirth, String gender, String tag, final BuddyCallback<User> callback) { Map<String, Object> parameters = new HashMap<String, Object>(); parameters.put("username", username); parameters.put("password", password); parameters.put("firstname", firstName); parameters.put("lastname", lastName); parameters.put("email", email); if (dateOfBirth != null) { parameters.put("dateOfBirth", dateOfBirth); } if (gender != null) { parameters.put("gender", gender); } parameters.put("tag", tag); return this.post("/users", parameters, getUserCallback(callback)); } public Future<BuddyResult<User>> loginUser(String username, String password, final BuddyCallback<User> callback) { Map<String, Object> parameters = new HashMap<String, Object>(); parameters.put("username", username); parameters.put("password", password); return this.post("/users/login", parameters, getUserCallback(callback)); } public Future<BuddyResult<User>> socialLogin(String identityProviderId, String identityId, String identityAccessToken, final BuddyCallback<User> callback) { Map<String, Object> parameters = new HashMap<String, Object>(); parameters.put("identityProviderId", identityProviderId); parameters.put("identityId", identityId); parameters.put("identityAccessToken", identityAccessToken); return this.post("/users/login/social", parameters, getUserCallback(callback)); } public Future<BuddyResult<Boolean>> logoutUser(final BuddyCallback<Boolean> callback) { Map<String, Object> parameters = new HashMap<String, Object>(); final BuddyFuture<BuddyResult<Boolean>> promise = new BuddyFuture<BuddyResult<Boolean>>(); BuddyFuture<BuddyResult<AccessTokenResult>> handle = (BuddyFuture<BuddyResult<AccessTokenResult>>) this.post("/users/me/logout", parameters, new BuddyCallback<AccessTokenResult>(AccessTokenResult.class) { @Override public void completed(BuddyResult<AccessTokenResult> result) { if (result.getIsSuccess()) { BuddyClientSettings settings = getSettings(); AccessTokenResult r = result.getResult(); boolean hadUser = settings.userid != null && settings.userToken != null; settings.deviceTokenExpires = r.accessTokenExpires; settings.deviceToken = r.accessToken; settings.userToken = null; saveSettings(); if (hadUser && userAuthCallback != null) { runOnUiThread(new Runnable() { @Override public void run() { userAuthCallback.authenticate(); } }); } } if (callback != null) { callback.completed(result.convert(Boolean.TRUE)); } } }); handle.continueWith(new BuddyFutureCallback<BuddyResult<AccessTokenResult>>() { @Override public void completed(BuddyFuture<BuddyResult<AccessTokenResult>> future) { try { promise.setValue(future.get().convert(future.get().getIsSuccess())); return; } catch (InterruptedException e) { } catch (ExecutionException e) { } promise.setValue(null); } }); return promise; } // // Metrics stuff // public Future<BuddyResult<TimedMetric>> recordMetricEvent(String eventName, Map<String, Object> values, final int timeoutInSeconds, final BuddyCallback<TimedMetric> callback) { Map<String, Object> parameters = new HashMap<String, Object>(); if (values != null) { parameters.put("value", values); } if (timeoutInSeconds > 0) { parameters.put("timeoutInSeconds", timeoutInSeconds); } try { eventName = URLEncoder.encode(eventName, "utf-8"); } catch (UnsupportedEncodingException e) { } return this.<TimedMetric>post("/metrics/events/" + eventName, parameters, new BuddyCallback<TimedMetric>(TimedMetric.class) { @Override public void completed(BuddyResult<TimedMetric> result) { if (result.getIsSuccess() && timeoutInSeconds > 0) { TimedMetric tm = result.getResult(); tm.setBuddyClient(BuddyClientImpl.this); } if (callback != null) { callback.completed(result); } } } ); } // // Push Notification Stuff // public Future<BuddyResult<Boolean>> setPushToken(final String pushToken, final BuddyCallback<Boolean> callback) { final BuddyClientSettings settings = getSettings(); if (pushToken != null && pushToken.equals(settings.pushToken)) { // nothing to do. BuddyFuture<BuddyResult<Boolean>> ret = new BuddyFuture<BuddyResult<Boolean>>(); JsonEnvelope<Boolean> env = new JsonEnvelope<Boolean>(); env.result = false; BuddyResult<Boolean> br = new BuddyResult<Boolean>(env); ret.setValue(br); if (callback != null) { callback.completed(br); } return ret; } Map<String, Object> parameters = new HashMap<String, Object>(); if (pushToken != null) { parameters.put("pushToken", pushToken); } final BuddyFuture<BuddyResult<Boolean>> promise = new BuddyFuture<BuddyResult<Boolean>>(); BuddyFuture<BuddyResult<Object>> handle = (BuddyFuture<BuddyResult<Object>>) this.patch("/devices/current", parameters, new BuddyCallback<Object>(Object.class) { @Override public void completed(BuddyResult<Object> result) { if (result.getIsSuccess()) { // save the settings settings.pushToken = pushToken; saveSettings(); } if (callback != null) { callback.completed(result.<Boolean>convert(result.getIsSuccess())); } } }); handle.continueWith(new BuddyFutureCallback<BuddyResult<Object>>() { @Override public void completed(BuddyFuture<BuddyResult<Object>> future) { try { promise.setValue(future.get().<Boolean>convert(future.get().getIsSuccess())); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }); return promise; } public Future<BuddyResult<NotificationResult>> sendPushNotification(List<String> recipientIds, String title, String message, String payload) { return sendPushNotification(recipientIds, title, message, payload, -1); } public Future<BuddyResult<NotificationResult>> sendPushNotification(List<String> recipientIds, String title, String message, String payload, int counterValue) { return sendPushNotification(recipientIds, title, message, payload, counterValue, null); } public Future<BuddyResult<NotificationResult>> sendPushNotification(final List<String> recipientIds, final Map<String, Object> osCustomData) { return sendPushNotification(recipientIds, null, null, null, -1, osCustomData); } public Future<BuddyResult<NotificationResult>> sendPushNotification(List<String> recipientIds, String title, String message, String payload, int counterValue, Map<String, Object> osCustomData) { final Map<String, Object> params = new HashMap<String, Object>(); params.put("recipients", recipientIds); if (title != null) { params.put("title", title); } if (message != null) { params.put("message", message); } if (payload != null) { params.put("payload", payload); } if (counterValue >= 0) { params.put("counterValue", counterValue); } if (osCustomData != null) { params.put("osCustomData", osCustomData); } // send the notification return Buddy.post("/notifications", params, NotificationResult.class); } public void recordNotificationReceived(Intent message) { String id = message.getStringExtra("_bId"); if (id != null && id.length() > 0) { this.post(String.format("/notifications/received/%s", id), null, (Class) null); } } private static String DefaultRoot = "https://api.buddyplatform.com"; private class BuddyClientSettings { public String serviceRoot; public String deviceToken; public Date deviceTokenExpires; public String userToken; public Date userTokenExpires; public String userid; public String pushToken; public String appVersion; public String getAccessToken() { Date now = new Date(); if (userToken != null && userTokenExpires.after(now)) { return userToken; } else if (deviceToken != null && deviceTokenExpires.after(now)) { return deviceToken; } return null; } public String getServiceRoot() { if (serviceRoot == null) { return DefaultRoot; } return serviceRoot; } } private BuddyClientSettings settings; // preferences private SharedPreferences getPreferences() { if (context != null) { return context.getSharedPreferences(String.format("com.buddy-%s-%s", app_id, options == null || options.settingsPrefix == null ? "" : options.settingsPrefix), Context.MODE_PRIVATE); } return null; } @SuppressLint("CommitPrefEdits") private void saveSettings() { SharedPreferences preferences = getPreferences(); if (preferences != null) { SharedPreferences.Editor editor = preferences.edit(); String json = new Gson().toJson(settings); editor.putString(this.app_id, json); editor.commit(); } } private BuddyClientSettings getSettings() { if (settings == null) { SharedPreferences preferences = getPreferences(); if (preferences != null) { String json = preferences.getString(this.app_id, null); if (json != null) { settings = new Gson().fromJson(json, BuddyClientSettings.class); } } if (settings == null) { settings = new BuddyClientSettings(); } } return settings; } public void handleError(BuddyResult result) { String error = result.getError(); if (error != null) { BuddyClientSettings settings = getSettings(); if (error.equals("NoInternetConnection")) { OnConnectivityChanged(ConnectivityLevel.None); } else if (error.equals("AuthAppCredentialsInvalid") || error.equals("AuthAccessTokenInvalid")) { // Bad token, clear all settings so they'll be required. // settings.deviceToken = settings.userToken = null; saveSettings(); } else if (error.equals("AuthUserAccessTokenRequired") && userAuthCallback != null) { runOnUiThread(new Runnable() { @Override public void run() { userAuthCallback.authenticate(); } }); } } } protected void OnConnectivityChanged(final ConnectivityLevel level) { if (_connectivityLevel == level) { return; } if (connectivityLevelChangedCallback != null) { runOnUiThread(new Runnable() { @Override public void run() { connectivityLevelChangedCallback.connectivityLevelChanged(level); } }); } _connectivityLevel = level; if (level == ConnectivityLevel.None) { CheckConnectivity(); } } private void CheckConnectivity() { AsyncTask<BuddyClientImpl, Void, Void> connectionTask = new AsyncTask<BuddyClientImpl, Void, Void>() { private Random random = new Random(); @Override protected Void doInBackground(BuddyClientImpl... clients) { BuddyResult<String> result = null; int retryCount = 0; do { retryCount += retryCount < 21 ? 1 : 0; int retryIntervalInMilliseconds = getRetryInterval(retryCount); Log.i("Retry", String.format("Retry interval (milliseconds): %s, Retry count: %s", retryIntervalInMilliseconds, retryCount)); try { Thread.sleep(retryIntervalInMilliseconds); Future<BuddyResult<String>> handle = clients[0].get("/service/ping", null, String.class); result = handle.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } if (result != null && result.getIsSuccess()) { OnConnectivityChanged(getConnectivityType()); } } while (result == null || !result.getIsSuccess()); return null; } private int getRetryInterval(int retryCount) { if (BuildConfig.DEBUG && !(retryCount > 0)) { throw new AssertionError(); } final int retryCapInMilliseconds = 30000; final int retryBaseInMilliseconds = 500; return random.nextInt(Math.min(retryCapInMilliseconds, retryBaseInMilliseconds * (int) (Math.pow(2, retryCount) - 1))); } }; connectionTask.execute(this); } private ConnectivityLevel getConnectivityType() { if (this.connectivityManager == null) { // If we have no connectivity manager, assume we are connected; API failures need to be handled regardless. // Connectivity manager is unavailable during unit tests. return ConnectivityLevel.Connected; } NetworkInfo networkInfo = this.connectivityManager.getActiveNetworkInfo(); int networkInfoType = networkInfo.getType(); switch (networkInfoType) { case ConnectivityManager.TYPE_WIFI: case ConnectivityManager.TYPE_WIMAX: return ConnectivityLevel.WiFi; case ConnectivityManager.TYPE_MOBILE: case ConnectivityManager.TYPE_MOBILE_DUN: return ConnectivityLevel.Carrier; default: return networkInfo.isConnected() ? ConnectivityLevel.Connected : ConnectivityLevel.None; } } }