package com.appboy.wear; import android.content.Context; import android.os.Bundle; import android.util.Log; import com.appboy.wear.communication.WearCommunicationUtils; import com.appboy.wear.enums.Gender; import com.appboy.wear.enums.Month; import com.appboy.wear.enums.WearScreenShape; import com.appboy.wear.enums.WearSdkActions; import com.appboy.wear.managers.AppboyWearDeviceIdReader; import com.appboy.wear.managers.WearDeviceDataProvider; import com.appboy.wear.models.AppboyProperties; import com.appboy.wear.models.WearDevice; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.wearable.DataApi; import com.google.android.gms.wearable.DataMap; import com.google.android.gms.wearable.PutDataMapRequest; import com.google.android.gms.wearable.PutDataRequest; import com.google.android.gms.wearable.Wearable; import java.math.BigDecimal; /** * Use this class to log Appboy SDK events from a wearable. The wearable must be paired with a device * running the Appboy SDK for the events to be logged. * <p/> * Methods containing 'User', such as addToUserCustomAttributeArray(), get called on the current AppboyUser * running on the phone. */ @SuppressWarnings("PMD.AvoidDuplicateLiterals") public class AppboyWearableAdapter implements GoogleApiClient.ConnectionCallbacks { private static final String TAG = String.format("%s.%s", Constants.APPBOY_LOG_TAG_PREFIX, AppboyWearableAdapter.class.getName()); private static final String DATA_SYNC_PATH_PREFIX = "/appboy-data-sync/"; private final GoogleApiClient mGoogleApiClient; private final AppboyWearDeviceIdReader mDeviceIdReader; private final Context mContext; private static AppboyWearableAdapter sInstance; private String mWearScreenType = null; public static AppboyWearableAdapter getInstance(Context context) { if (sInstance == null) { sInstance = new AppboyWearableAdapter(context); } return sInstance; } AppboyWearableAdapter(final Context context) { mContext = context.getApplicationContext(); mDeviceIdReader = new AppboyWearDeviceIdReader(mContext); mGoogleApiClient = new GoogleApiClient.Builder(mContext) .addApiIfAvailable(Wearable.API) .build(); // When the api client has connected, send the wear device. // We have to register the connection callback on this to not throw AbstractMethodErrors. mGoogleApiClient.registerConnectionCallbacks(this); mGoogleApiClient.connect(); Log.i(TAG, "Adapter started"); } /** * Logs the shape of this wearable screen. See https://github.com/tajchert/ShapeWear for instructions * on how to collect this information. * * @param shape The screen shape. Either round or square. * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean logWearScreenShape(WearScreenShape shape) { if (!isWearableApiConnectionAvailable()) { return false; } mWearScreenType = shape.forJsonPut(); sendWearDeviceData(); return true; } /** * see IAppboy#logCustomEvent(String) * * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean logCustomEvent(String eventName) { return logCustomEvent(eventName, null); } /** * see IAppboy#logCustomEvent(String, AppboyProperties) * * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean logCustomEvent(String eventName, AppboyProperties properties) { if (!isWearableApiConnectionAvailable()) { return false; } if (isNullOrEmpty(eventName)) { Log.w(TAG, "Event name null or empty. Ignoring custom event."); return false; } PutDataMapRequest putDataMapRequest = getNewPutDataMapRequest(); DataMap dataMap = putDataMapRequest.getDataMap(); if (properties == null) { WearCommunicationUtils.modifyDataMapWithCustomEvent(dataMap, eventName); } else { WearCommunicationUtils.modifyDataMapWithCustomEvent(dataMap, properties, eventName); } syncDataMapRequest(putDataMapRequest); return true; } /** * see IAppboy.logPurchase(String, String, BigDecimal) * * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean logPurchase(String productId, String currencyCode, BigDecimal price) { return logPurchase(productId, currencyCode, price, null); } /** * see IAppboy#logPurchase(String, String, BigDecimal, int, AppboyProperties) * * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean logPurchase(String productId, String currencyCode, BigDecimal price, AppboyProperties properties) { if (!isWearableApiConnectionAvailable()) { return false; } if (isNullOrEmpty(productId)) { Log.w(TAG, "Product id null or empty. Not logging in-app purchase to Appboy."); return false; } if (isNullOrEmpty(currencyCode)) { Log.w(TAG, "Currency code null or empty. Not logging in-app purchase to Appboy."); return false; } if (price == null) { Log.w(TAG, "Price is null. Not logging in-app purchase to Appboy."); return false; } PutDataMapRequest putDataMapRequest = getNewPutDataMapRequest(); DataMap dataMap = putDataMapRequest.getDataMap(); if (properties == null) { WearCommunicationUtils.modifyDataMapWithPurchase(dataMap, currencyCode, price, productId); } else { WearCommunicationUtils.modifyDataMapWithPurchase(dataMap, currencyCode, price, properties, productId); } syncDataMapRequest(putDataMapRequest); return true; } /** * see IAppboy#logPushNotificationOpened(String) * * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean logPushNotificationOpened(String campaignId) { if (!isWearableApiConnectionAvailable()) { return false; } if (isNullOrEmpty(campaignId)) { Log.w(TAG, "Campaign id null or empty. Not logging push open."); return false; } PutDataMapRequest putDataMapRequest = getNewPutDataMapRequest(); DataMap dataMap = putDataMapRequest.getDataMap(); WearCommunicationUtils.modifyDataMapWithPushNotificationOpened(dataMap, campaignId); syncDataMapRequest(putDataMapRequest); return true; } /** * see IAppboy#submitFeedback(String, String, boolean) * * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean submitFeedback(String replyToEmail, String message, boolean isReportingABug) { if (!isWearableApiConnectionAvailable()) { return false; } if (isNullOrEmpty(replyToEmail)) { Log.w(TAG, "Reply email null or empty. Not submitting feedback."); return false; } if (isNullOrEmpty(message)) { Log.w(TAG, "Message null or empty. Not submitting feedback."); return false; } PutDataMapRequest putDataMapRequest = getNewPutDataMapRequest(); DataMap dataMap = putDataMapRequest.getDataMap(); WearCommunicationUtils.modifyDataMapWithSubmitFeedback(dataMap, message, isReportingABug, replyToEmail); syncDataMapRequest(putDataMapRequest); return true; } /** * see com.appboy.AppboyUser#addToCustomAttributeArray(String, String) * * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean addToUserCustomAttributeArray(String key, String value) { if (!isWearableApiConnectionAvailable()) { return false; } if (isNullOrEmpty(key)) { Log.w(TAG, "Key null or empty. Not adding to custom attribute array."); return false; } if (isNullOrEmpty(value)) { Log.w(TAG, "Value null or empty. Not adding to custom attribute array."); return false; } PutDataMapRequest putDataMapRequest = getNewPutDataMapRequest(); DataMap dataMap = putDataMapRequest.getDataMap(); WearCommunicationUtils.modifyDataMapWithUserAddToCustomAttributeArray(dataMap, value, key); syncDataMapRequest(putDataMapRequest); return true; } /** * see com.appboy.AppboyUser#incrementCustomUserAttribute(String) * * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean incrementCustomUserAttribute(String key) { if (!isWearableApiConnectionAvailable()) { return false; } if (isNullOrEmpty(key)) { Log.w(TAG, "Key null or empty. Not incrementing custom user attribute."); return false; } PutDataMapRequest putDataMapRequest = getNewPutDataMapRequest(); DataMap dataMap = putDataMapRequest.getDataMap(); WearCommunicationUtils.modifyDataMapWithUserIncrementCustomAttribute(dataMap, key); syncDataMapRequest(putDataMapRequest); return true; } /** * see com.appboy.AppboyUser#incrementCustomUserAttribute(String, int) * * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean incrementCustomUserAttribute(String key, int incrementValue) { if (!isWearableApiConnectionAvailable()) { return false; } if (isNullOrEmpty(key)) { Log.w(TAG, "Key null or empty. Not incrementing custom user attribute."); return false; } PutDataMapRequest putDataMapRequest = getNewPutDataMapRequest(); DataMap dataMap = putDataMapRequest.getDataMap(); WearCommunicationUtils.modifyDataMapWithUserIncrementCustomAttribute(dataMap, incrementValue, key); syncDataMapRequest(putDataMapRequest); return true; } /** * see com.appboy.AppboyUser#removeFromCustomAttributeArray(String, String) * * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean removeFromUserCustomAttributeArray(String key, String value) { if (!isWearableApiConnectionAvailable()) { return false; } if (isNullOrEmpty(key)) { Log.w(TAG, "Key null or empty. Not removing from custom user attribute array."); return false; } if (isNullOrEmpty(value)) { Log.w(TAG, "Value null or empty. Not removing from custom user attribute array."); return false; } PutDataMapRequest putDataMapRequest = getNewPutDataMapRequest(); DataMap dataMap = putDataMapRequest.getDataMap(); WearCommunicationUtils.modifyDataMapWithUserRemoveFromCustomAttributeArray(dataMap, value, key); syncDataMapRequest(putDataMapRequest); return true; } /** * see com.appboy.AppboyUser#setCustomAttributeArray(String, String[]) * * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean setUserCustomAttributeArray(String key, String[] values) { if (!isWearableApiConnectionAvailable()) { return false; } if (isNullOrEmpty(key)) { Log.w(TAG, "Key null or empty. Not setting custom user attribute array."); return false; } if (values == null) { Log.w(TAG, "Values array null. Not setting custom user attribute array."); return false; } for (String value : values) { if (isNullOrEmpty(value)) { Log.w(TAG, "Value null or empty. Not setting custom user attribute array."); return false; } } PutDataMapRequest putDataMapRequest = getNewPutDataMapRequest(); DataMap dataMap = putDataMapRequest.getDataMap(); WearCommunicationUtils.modifyDataMapWithUserSetCustomAttributeArray(dataMap, values, key); syncDataMapRequest(putDataMapRequest); return true; } /** * see com.appboy.AppboyUser#setCustomUserAttribute(String, boolean) * * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean setUserCustomAttribute(String key, boolean value) { if (!isWearableApiConnectionAvailable()) { return false; } if (isNullOrEmpty(key)) { Log.w(TAG, "Key null or empty. Not setting custom user attribute."); return false; } PutDataMapRequest putDataMapRequest = getNewPutDataMapRequest(); DataMap dataMap = putDataMapRequest.getDataMap(); WearCommunicationUtils.modifyDataMapWithUserSetCustomAttribute(dataMap, value, key); syncDataMapRequest(putDataMapRequest); return true; } /** * see com.appboy.AppboyUser#setCustomUserAttribute(String, float) * * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean setUserCustomAttribute(String key, float value) { if (!isWearableApiConnectionAvailable()) { return false; } if (isNullOrEmpty(key)) { Log.w(TAG, "Key null or empty. Not setting custom user attribute."); return false; } PutDataMapRequest putDataMapRequest = getNewPutDataMapRequest(); DataMap dataMap = putDataMapRequest.getDataMap(); WearCommunicationUtils.modifyDataMapWithUserSetCustomAttribute(dataMap, value, key); syncDataMapRequest(putDataMapRequest); return true; } /** * see com.appboy.AppboyUser#setCustomUserAttribute(String, int) * * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean setUserCustomAttribute(String key, int value) { if (!isWearableApiConnectionAvailable()) { return false; } if (isNullOrEmpty(key)) { Log.w(TAG, "Key null or empty. Not setting custom user attribute."); return false; } PutDataMapRequest putDataMapRequest = getNewPutDataMapRequest(); DataMap dataMap = putDataMapRequest.getDataMap(); WearCommunicationUtils.modifyDataMapWithUserSetCustomAttribute(dataMap, value, key); syncDataMapRequest(putDataMapRequest); return true; } /** * see com.appboy.AppboyUser#setCustomUserAttribute(String, long) * * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean setUserCustomAttribute(String key, long value) { if (!isWearableApiConnectionAvailable()) { return false; } if (isNullOrEmpty(key)) { Log.w(TAG, "Key null or empty. Not setting custom user attribute."); return false; } PutDataMapRequest putDataMapRequest = getNewPutDataMapRequest(); DataMap dataMap = putDataMapRequest.getDataMap(); WearCommunicationUtils.modifyDataMapWithUserSetCustomAttribute(dataMap, value, key); syncDataMapRequest(putDataMapRequest); return true; } /** * see com.appboy.AppboyUser#setCustomUserAttribute(String, String) * * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean setUserCustomAttribute(String key, String value) { if (!isWearableApiConnectionAvailable()) { return false; } if (isNullOrEmpty(key)) { Log.w(TAG, "Key null or empty. Not setting custom user attribute."); return false; } if (isNullOrEmpty(value)) { Log.w(TAG, "Value null or empty. Not setting custom user attribute."); return false; } PutDataMapRequest putDataMapRequest = getNewPutDataMapRequest(); DataMap dataMap = putDataMapRequest.getDataMap(); WearCommunicationUtils.modifyDataMapWithUserSetCustomAttribute(dataMap, value, key); syncDataMapRequest(putDataMapRequest); return true; } /** * see com.appboy.AppboyUser#setCustomUserAttributeToNow(String) * * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean setUserCustomAttributeToNow(String key) { if (!isWearableApiConnectionAvailable()) { return false; } if (isNullOrEmpty(key)) { Log.w(TAG, "Key null or empty. Not setting custom user attribute to now."); return false; } PutDataMapRequest putDataMapRequest = getNewPutDataMapRequest(); DataMap dataMap = putDataMapRequest.getDataMap(); WearCommunicationUtils.modifyDataMapWithUserSetCustomAttributeToNow(dataMap, key); syncDataMapRequest(putDataMapRequest); return true; } /** * see com.appboy.AppboyUser#unsetCustomUserAttribute(String) * * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean unsetUserCustomAttribute(String key) { if (!isWearableApiConnectionAvailable()) { return false; } if (isNullOrEmpty(key)) { Log.w(TAG, "Key null or empty. Not unsetting custom user attribute."); return false; } PutDataMapRequest putDataMapRequest = getNewPutDataMapRequest(); DataMap dataMap = putDataMapRequest.getDataMap(); WearCommunicationUtils.modifyDataMapWithUserUnsetCustomAttribute(dataMap, key); syncDataMapRequest(putDataMapRequest); return true; } /** * see com.appboy.AppboyUser#setCustomUserAttributeToSecondsFromEpoch(String, long) * * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean setUserCustomAttributeToSecondsFromEpoch(String key, long secondsFromEpoch) { if (!isWearableApiConnectionAvailable()) { return false; } if (isNullOrEmpty(key)) { Log.w(TAG, "Key null or empty. Not setting custom user attribute to seconds from epoch."); return false; } PutDataMapRequest putDataMapRequest = getNewPutDataMapRequest(); DataMap dataMap = putDataMapRequest.getDataMap(); WearCommunicationUtils.modifyDataMapWithUserSetCustomAttributeToSecondsFromEpoch(dataMap, secondsFromEpoch, key); syncDataMapRequest(putDataMapRequest); return true; } /** * Convenience method to DRY the other user profile methods * * @param value the string argument * @param actionType the type of action * @return a boolean indicating whether or not this action has been sent to the phone. */ private boolean sendBasicUserProfileStringValue(String value, WearSdkActions actionType) { if (!isWearableApiConnectionAvailable()) { return false; } PutDataMapRequest putDataMapRequest = getNewPutDataMapRequest(); DataMap dataMap = putDataMapRequest.getDataMap(); WearCommunicationUtils.modifyDataMapWithUserProfileString(dataMap, actionType, value); syncDataMapRequest(putDataMapRequest); return true; } /** * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean setUserAvatarImageUrl(String url) { if (isNullOrEmpty(url)) { Log.w(TAG, "Url null or empty. Not setting user avatar image url."); return false; } return sendBasicUserProfileStringValue(url, WearSdkActions.SET_AVATAR_IMAGE_URL); } /** * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean setUserCountry(String country) { if (isNullOrEmpty(country)) { Log.w(TAG, "Country null or empty. Not setting user country"); return false; } return sendBasicUserProfileStringValue(country, WearSdkActions.SET_COUNTRY); } /** * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean setUserEmail(String email) { if (isNullOrEmpty(email)) { Log.w(TAG, "Email null or empty. Not setting user email"); return false; } return sendBasicUserProfileStringValue(email, WearSdkActions.SET_EMAIL); } /** * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean setUserFirstName(String firstName) { if (isNullOrEmpty(firstName)) { Log.w(TAG, "First name null or empty. Not setting user first name"); return false; } return sendBasicUserProfileStringValue(firstName, WearSdkActions.SET_FIRST_NAME); } /** * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean setUserHomeCity(String homeCity) { if (isNullOrEmpty(homeCity)) { Log.w(TAG, "Home city null or empty. Not setting user home city"); return false; } return sendBasicUserProfileStringValue(homeCity, WearSdkActions.SET_HOME_CITY); } /** * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean setUserLastName(String lastName) { if (isNullOrEmpty(lastName)) { Log.w(TAG, "Last name null or empty. Not setting user last name"); return false; } return sendBasicUserProfileStringValue(lastName, WearSdkActions.SET_LAST_NAME); } /** * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean setUserPhoneNumber(String phoneNumber) { if (isNullOrEmpty(phoneNumber)) { Log.w(TAG, "Phone number null or empty. Not setting user phone number"); return false; } return sendBasicUserProfileStringValue(phoneNumber, WearSdkActions.SET_PHONE_NUMBER); } public boolean setUserDateOfBirth(int year, Month month, int day) { if (!isWearableApiConnectionAvailable()) { return false; } PutDataMapRequest putDataMapRequest = getNewPutDataMapRequest(); DataMap dataMap = putDataMapRequest.getDataMap(); WearCommunicationUtils.modifyDataMapWithUserDateOfBirth(dataMap, year, month, day); syncDataMapRequest(putDataMapRequest); return true; } public boolean setUserGender(Gender gender) { if (!isWearableApiConnectionAvailable()) { return false; } PutDataMapRequest putDataMapRequest = getNewPutDataMapRequest(); DataMap dataMap = putDataMapRequest.getDataMap(); WearCommunicationUtils.modifyDataMapWithUserGender(dataMap, gender); syncDataMapRequest(putDataMapRequest); return true; } /** * see com.appboy.AppboyUser#setLastKnownLocation(double, double, Double, Double) * * @return a boolean indicating whether or not this action has been sent to the phone. */ public boolean setUserLastKnownLocation(double latitude, double longitude, Double altitude, Double accuracy) { if (!isWearableApiConnectionAvailable()) { return false; } PutDataMapRequest putDataMapRequest = getNewPutDataMapRequest(); DataMap dataMap = putDataMapRequest.getDataMap(); WearCommunicationUtils.modifyDataMapWithUserSetLastKnownLocation(dataMap, longitude, altitude, accuracy, latitude); syncDataMapRequest(putDataMapRequest); return true; } public PutDataMapRequest getNewPutDataMapRequest() { // Create a data item request with the path of the current time. Paths must start with slashes return PutDataMapRequest.createWithAutoAppendedId(DATA_SYNC_PATH_PREFIX); } public void syncDataMapRequest(PutDataMapRequest putDataMapRequest) { // Sync the data over the Data Sync API PutDataRequest putDataReq = putDataMapRequest.asPutDataRequest(); final PendingResult<DataApi.DataItemResult> pendingResult = Wearable.DataApi.putDataItem(mGoogleApiClient, putDataReq); Thread sendCustomEventActionThread = new Thread(new Runnable() { @Override public void run() { DataApi.DataItemResult dataItemResult = pendingResult.await(); if (!dataItemResult.getStatus().isSuccess()) { // We can't return this asynchronous value, but we can log it Log.w(TAG, "Appboy sdk action failed for reason: " + dataItemResult.getStatus().getStatusMessage()); } } }); sendCustomEventActionThread.start(); } /** * Checks if we can communicate via GMS over the Wearable API. If the GoogleApiClient is null or * the Wearable API is unconnected then returns false. * * @return a boolean indicating whether or not this action has been sent to the phone. false if the connection cannot be made. */ public boolean isWearableApiConnectionAvailable() { if (mGoogleApiClient == null) { Log.w(TAG, "Google Api Client null. Wearable connection could not be made."); return false; } if (!mGoogleApiClient.hasConnectedApi(Wearable.API)) { Log.w(TAG, "Google Wearable Api not connected to the client. Wearable connection could not be made."); return false; } return true; } /** * @return a boolean indicating whether or not this action has been sent to the phone. Wear device information */ public WearDevice getWearDeviceData() { WearDeviceDataProvider wearDeviceDataProvider = new WearDeviceDataProvider(mContext, mDeviceIdReader, mWearScreenType); return wearDeviceDataProvider.getWearDevice(); } /** * Collects the wearable device info and sends it over to the phone. */ private void sendWearDeviceData() { WearDevice wearDevice = getWearDeviceData(); if (wearDevice != null) { PutDataMapRequest putDataMapRequest = getNewPutDataMapRequest(); DataMap dataMap = putDataMapRequest.getDataMap(); WearCommunicationUtils.modifyDataMapWithWearDeviceInformation(dataMap, wearDevice); syncDataMapRequest(putDataMapRequest); } } @Override public void onConnected(Bundle bundle) { sendWearDeviceData(); // We only need to send the device data once, so unregister ourselves from further callbacks mGoogleApiClient.unregisterConnectionCallbacks(this); } @Override public void onConnectionSuspended(int cause) { // GMS will auto re-connect so we don't have to handle the reconnection ourselves. // https://developers.google.com/android/reference/com/google/android/gms/common/api/GoogleApiClient.ConnectionCallbacks.html#onConnectionSuspended(int) } static boolean isNullOrEmpty(String reference) { return reference == null || reference.length() == 0; } }