io.teak.sdk.DeviceConfiguration.java Source code

Java tutorial

Introduction

Here is the source code for io.teak.sdk.DeviceConfiguration.java

Source

/* Teak -- Copyright (C) 2016 GoCarrot Inc.
 *
 * 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 io.teak.sdk;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.provider.Settings;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;

import com.google.android.gms.ads.identifier.AdvertisingIdClient;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.gcm.GoogleCloudMessaging;

import org.json.JSONObject;

import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.FutureTask;

class DeviceConfiguration {
    private static final String LOG_TAG = "Teak:DeviceConfig";

    public String gcmId;

    public final String deviceId;
    public final String deviceManufacturer;
    public final String deviceModel;
    public final String deviceFallback;
    public final String platformString;

    public AdvertisingIdClient.Info advertsingInfo;

    private FutureTask<GoogleCloudMessaging> gcm;
    private SharedPreferences preferences;

    private static final String PREFERENCE_GCM_ID = "io.teak.sdk.Preferences.GcmId";
    private static final String PREFERENCE_APP_VERSION = "io.teak.sdk.Preferences.AppVersion";
    private static final String PREFERENCE_DEVICE_ID = "io.teak.sdk.Preferences.DeviceId";

    public DeviceConfiguration(@NonNull final Context context, @NonNull AppConfiguration appConfiguration) {
        if (android.os.Build.VERSION.RELEASE == null) {
            this.platformString = "android_unknown";
        } else {
            this.platformString = "android_" + android.os.Build.VERSION.RELEASE;
        }

        // Preferences file
        {
            SharedPreferences tempPreferences = null;
            try {
                tempPreferences = context.getSharedPreferences(Teak.PREFERENCES_FILE, Context.MODE_PRIVATE);
            } catch (Exception e) {
                Log.e(LOG_TAG, "Error calling getSharedPreferences(). " + Log.getStackTraceString(e));
            } finally {
                this.preferences = tempPreferences;
            }

            if (this.preferences == null) {
                Log.e(LOG_TAG, "getSharedPreferences() returned null. Some caching is disabled.");
            }
        }

        // Device model/manufacturer
        // https://raw.githubusercontent.com/jaredrummler/AndroidDeviceNames/master/library/src/main/java/com/jaredrummler/android/device/DeviceName.java
        {
            this.deviceManufacturer = Build.MANUFACTURER == null ? "" : Build.MANUFACTURER;
            this.deviceModel = Build.MODEL == null ? "" : Build.MODEL;
            if (this.deviceModel.startsWith(Build.MANUFACTURER)) {
                this.deviceFallback = capitalize(Build.MODEL);
            } else {
                this.deviceFallback = capitalize(Build.MANUFACTURER) + " " + Build.MODEL;
            }
        }

        // Device id
        {
            String tempDeviceId = null;
            try {
                tempDeviceId = UUID.nameUUIDFromBytes(android.os.Build.SERIAL.getBytes("utf8")).toString();
            } catch (Exception e) {
                Log.e(LOG_TAG, "android.os.Build.SERIAL not available, falling back to Settings.Secure.ANDROID_ID. "
                        + Log.getStackTraceString(e));
            }

            if (tempDeviceId == null) {
                try {
                    String androidId = Settings.Secure.getString(context.getContentResolver(),
                            Settings.Secure.ANDROID_ID);
                    if (androidId.equals("9774d56d682e549c")) {
                        Log.e(LOG_TAG,
                                "Settings.Secure.ANDROID_ID == '9774d56d682e549c', falling back to random UUID stored in preferences.");
                    } else {
                        tempDeviceId = UUID.nameUUIDFromBytes(androidId.getBytes("utf8")).toString();
                    }
                } catch (Exception e) {
                    Log.e(LOG_TAG,
                            "Error generating device id from Settings.Secure.ANDROID_ID, falling back to random UUID stored in preferences. "
                                    + Log.getStackTraceString(e));
                }
            }

            if (tempDeviceId == null) {
                if (this.preferences != null) {
                    tempDeviceId = this.preferences.getString(PREFERENCE_DEVICE_ID, null);
                    if (tempDeviceId == null) {
                        try {
                            String prefDeviceId = UUID.randomUUID().toString();
                            SharedPreferences.Editor editor = this.preferences.edit();
                            editor.putString(PREFERENCE_DEVICE_ID, prefDeviceId);
                            editor.apply();
                            tempDeviceId = prefDeviceId;
                        } catch (Exception e) {
                            Log.e(LOG_TAG,
                                    "Error storing random UUID, no more fallbacks. " + Log.getStackTraceString(e));
                        }
                    }
                } else {
                    Log.e(LOG_TAG,
                            "getSharedPreferences() returned null, unable to store random UUID, no more fallbacks.");
                }
            }

            this.deviceId = tempDeviceId;

            if (this.deviceId == null) {
                return;
            }
        }

        // Kick off GCM request
        if (this.preferences != null) {
            int storedAppVersion = this.preferences.getInt(PREFERENCE_APP_VERSION, 0);
            String storedGcmId = this.preferences.getString(PREFERENCE_GCM_ID, null);
            if (storedAppVersion == appConfiguration.appVersion && storedGcmId != null) {
                // No need to get a new one, so put it on the blocking queue
                if (Teak.isDebug) {
                    Log.d(LOG_TAG, "GCM Id found in cache: " + storedGcmId);
                }
                this.gcmId = storedGcmId;
                displayGCMDebugMessage();
            }
        }

        this.gcm = new FutureTask<>(new RetriableTask<>(100, 2000L, new Callable<GoogleCloudMessaging>() {
            @Override
            public GoogleCloudMessaging call() throws Exception {
                return GoogleCloudMessaging.getInstance(context);
            }
        }));
        new Thread(this.gcm).start();

        if (this.gcmId == null) {
            registerForGCM(appConfiguration);
        }

        // Kick off Advertising Info request
        fetchAdvertisingInfo(context);
    }

    public void reRegisterPushToken(@NonNull AppConfiguration appConfiguration) {
        if (this.preferences != null) {
            SharedPreferences.Editor editor = this.preferences.edit();
            editor.putInt(PREFERENCE_APP_VERSION, 0);
            editor.putString(PREFERENCE_GCM_ID, null);
            editor.apply();
        }
        registerForGCM(appConfiguration);
    }

    private void fetchAdvertisingInfo(@NonNull final Context context) {
        final DeviceConfiguration _this = this;
        final FutureTask<AdvertisingIdClient.Info> adInfoFuture = new FutureTask<>(
                new RetriableTask<>(100, 7000L, new Callable<AdvertisingIdClient.Info>() {
                    @Override
                    public AdvertisingIdClient.Info call() throws Exception {
                        int googlePlayStatus = GooglePlayServicesUtil.isGooglePlayServicesAvailable(context);
                        if (googlePlayStatus == ConnectionResult.SUCCESS) {
                            return AdvertisingIdClient.getAdvertisingIdInfo(context);
                        }
                        throw new Exception("Retrying GooglePlayServicesUtil.isGooglePlayServicesAvailable()");
                    }
                }));
        new Thread(adInfoFuture).start();

        // TODO: This needs to be re-checked in case it's something like SERVICE_UPDATING or SERVICE_VERSION_UPDATE_REQUIRED
        int googlePlayStatus = GooglePlayServicesUtil.isGooglePlayServicesAvailable(context);
        if (googlePlayStatus == ConnectionResult.SUCCESS) {

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        AdvertisingIdClient.Info adInfo = adInfoFuture.get();

                        // Inform listeners Ad Info has changed
                        if (adInfo != _this.advertsingInfo) {
                            _this.advertsingInfo = adInfo;
                            synchronized (eventListenersMutex) {
                                for (EventListener e : eventListeners) {
                                    e.onAdvertisingInfoChanged(_this);
                                }
                            }
                        }
                    } catch (Exception e) {
                        if (Teak.isDebug) {
                            Log.e(LOG_TAG, "Couldn't get Google Play Advertising Information.");
                        }
                    }
                }
            }).start();
        }
    }

    private void registerForGCM(@NonNull final AppConfiguration appConfiguration) {
        try {
            if (appConfiguration.pushSenderId != null) {
                final DeviceConfiguration _this = this;

                final FutureTask<String> gcmRegistration = new FutureTask<>(
                        new RetriableTask<>(100, 7000L, new Callable<String>() {
                            @Override
                            public String call() throws Exception {
                                GoogleCloudMessaging gcm = _this.gcm.get();

                                if (Teak.isDebug) {
                                    Log.d(LOG_TAG,
                                            "Registering for GCM with sender id: " + appConfiguration.pushSenderId);
                                }
                                return gcm.register(appConfiguration.pushSenderId);
                            }
                        }));
                new Thread(gcmRegistration).start();

                new Thread(new Runnable() {
                    public void run() {
                        try {
                            String registration = gcmRegistration.get();

                            if (registration == null) {
                                Log.e(LOG_TAG, "Got null token during GCM registration.");
                                return;
                            }

                            if (_this.preferences != null) {
                                SharedPreferences.Editor editor = _this.preferences.edit();
                                editor.putInt(PREFERENCE_APP_VERSION, appConfiguration.appVersion);
                                editor.putString(PREFERENCE_GCM_ID, registration);
                                editor.apply();
                            }

                            // Inform event listeners GCM is here
                            if (!registration.equals(gcmId)) {
                                _this.gcmId = registration;
                                synchronized (eventListenersMutex) {
                                    for (EventListener e : eventListeners) {
                                        e.onGCMIdChanged(_this);
                                    }
                                }
                            }

                            displayGCMDebugMessage();
                        } catch (Exception e) {
                            Log.e(LOG_TAG, Log.getStackTraceString(e));
                        }
                    }
                }).start();
            }
        } catch (Exception ignored) {
        }
    }

    // region Event Listener
    public interface EventListener {
        void onGCMIdChanged(DeviceConfiguration deviceConfiguration);

        void onAdvertisingInfoChanged(DeviceConfiguration deviceConfiguration);
    }

    private static final Object eventListenersMutex = new Object();
    private static ArrayList<EventListener> eventListeners = new ArrayList<>();

    public static void addEventListener(EventListener e) {
        synchronized (eventListenersMutex) {
            if (!eventListeners.contains(e)) {
                eventListeners.add(e);
            }
        }
    }

    public static void removeEventListener(EventListener e) {
        synchronized (eventListenersMutex) {
            eventListeners.remove(e);
        }
    }
    // endregion

    private void displayGCMDebugMessage() {
        if (Teak.isDebug) {
            final DeviceConfiguration _this = this;
            Session.whenUserIdIsReadyRun(new Session.SessionRunnable() {
                @Override
                public void run(Session session) {
                    try {
                        String urlString = "https://app.teak.io/apps/" + session.appConfiguration.appId
                                + "/test_accounts/new" + "?api_key=" + URLEncoder.encode(session.userId(), "UTF-8")
                                + "&gcm_push_key=" + URLEncoder.encode(_this.gcmId, "UTF-8")
                                + "&device_manufacturer=" + URLEncoder.encode(_this.deviceManufacturer, "UTF-8")
                                + "&device_model=" + URLEncoder.encode(_this.deviceModel, "UTF-8")
                                + "&device_fallback=" + URLEncoder.encode(_this.deviceFallback, "UTF-8")
                                + "&bundle_id=" + URLEncoder.encode(session.appConfiguration.bundleId, "UTF-8")
                                + "&device_id=" + URLEncoder.encode(_this.deviceId, "UTF-8");

                        Log.d(LOG_TAG,
                                "If you want to debug or test push notifications on this device please click the link below, or copy/paste into your browser:");
                        Log.d(LOG_TAG, "    " + urlString);
                    } catch (Exception e) {
                        Log.e(LOG_TAG, Log.getStackTraceString(e));
                    }
                }
            });
        }
    }

    // region Helpers
    // https://raw.githubusercontent.com/jaredrummler/AndroidDeviceNames/master/library/src/main/java/com/jaredrummler/android/device/DeviceName.java
    private static String capitalize(String str) {
        if (TextUtils.isEmpty(str)) {
            return str;
        }
        char[] arr = str.toCharArray();
        boolean capitalizeNext = true;
        String phrase = "";
        for (char c : arr) {
            if (capitalizeNext && Character.isLetter(c)) {
                phrase += Character.toUpperCase(c);
                capitalizeNext = false;
                continue;
            } else if (Character.isWhitespace(c)) {
                capitalizeNext = true;
            }
            phrase += c;
        }
        return phrase;
    }
    // endregion

    public Map<String, Object> to_h() {
        HashMap<String, Object> ret = new HashMap<>();
        ret.put("gcmId", this.gcmId);
        ret.put("deviceId", this.deviceId);
        ret.put("deviceManufacturer", this.deviceManufacturer);
        ret.put("deviceModel", this.deviceModel);
        ret.put("deviceFallback", this.deviceFallback);
        ret.put("platformString", this.platformString);
        return ret;
    }

    @Override
    public String toString() {
        try {
            return String.format(Locale.US, "%s: %s", super.toString(), new JSONObject(this.to_h()).toString(2));
        } catch (Exception ignored) {
            return super.toString();
        }
    }

    public class RetriableTask<T> implements Callable<T> {
        private final Callable<T> wrappedTask;
        private final int tries;
        private final long retryDelay;

        public RetriableTask(final int tries, final long retryDelay, final Callable<T> taskToWrap) {
            this.wrappedTask = taskToWrap;
            this.tries = tries;
            this.retryDelay = retryDelay;
        }

        public T call() throws Exception {
            int triesLeft = this.tries;
            while (true) {
                try {
                    return this.wrappedTask.call();
                } catch (final CancellationException | InterruptedException e) {
                    throw e;
                } catch (final Exception e) {
                    triesLeft--;
                    if (triesLeft == 0)
                        throw e;
                    if (this.retryDelay > 0)
                        Thread.sleep(this.retryDelay);
                }
            }
        }
    }
}