package org.azavea.otm; import android.app.Activity; import android.app.Application; import android.app.Fragment; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.os.Build; import android.os.Bundle; import android.os.Handler.Callback; import android.os.Message; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; import com.google.android.gms.analytics.GoogleAnalytics; import com.google.android.gms.analytics.HitBuilders; import com.google.android.gms.analytics.Tracker; import com.rollbar.android.Rollbar; import org.azavea.helpers.Logger; import org.azavea.lists.NearbyList; import org.azavea.otm.data.InstanceInfo; import org.azavea.otm.data.User; import org.azavea.otm.rest.RequestGenerator; import org.azavea.otm.rest.handlers.RestHandler; import org.json.JSONException; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.io.InputStream; import java.util.ArrayList; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; /** * A global singleton object to maintain application state */ public class App extends Application { public static final String LOG_TAG = "AZ_OTM"; public static final String INSTANCE_CODE = "instance_code"; private static App appInstance = null; private static FilterManager filterManager = null; private static FieldManager fieldManager = null; private static SharedPreferences sharedPreferences = null; private static boolean pendingEnabled = false; private static InstanceInfo currentInstance; private static boolean loadingInstance = false; private static ArrayList<Callback> registeredInstanceCallbacks = new ArrayList<>(); private LoginManager loginManager = null; private NearbyList nearbyList = null; private Tracker apptracker = null; public static App getAppInstance() { checkAppInstance(); return appInstance; } public static LoginManager getLoginManager() { App app = getAppInstance(); if (app.loginManager == null) { app.loginManager = new LoginManager(appInstance); } return app.loginManager; } /** * Static access to single search manager instance */ public static FilterManager getFilterManager() { return filterManager; } /** * Static access to single field manager instance */ public static FieldManager getFieldManager() { return fieldManager; } public static SharedPreferences getSharedPreferences() { if (sharedPreferences == null) { checkAppInstance(); sharedPreferences = appInstance.getSharedPreferences(appInstance.getString(R.string.app_name), Context.MODE_PRIVATE); // Set-up SharedPreferences if they haven't been set up before setDefaultSharedPreferences(sharedPreferences); } return sharedPreferences; } public static boolean isPendingEnabled() { return pendingEnabled; } private static void checkAppInstance() { if (appInstance == null) { throw new IllegalStateException("Application not created yet"); } } // (re)Load the relevant resources into the // applications shared-preferences. private static void setDefaultSharedPreferences(SharedPreferences prefs) { Editor editor = prefs.edit(); Context context = appInstance.getApplicationContext(); editor.putString("base_url", context.getString(R.string.base_url)) .putString("api_url", context.getString(R.string.api_url)) .putString("tiler_url", context.getString(R.string.tiler_url)) .putString("plot_feature", context.getString(R.string.plot_feature)) .putString("boundary_feature", context.getString(R.string.boundary_feature)) .putString("access_key", context.getString(R.string.access_key)) .putString("secret_key", context.getString(R.string.secret_key)) .putString("max_nearby_plots", context.getString(R.string.max_nearby_plots)) .putString("starting_zoom_level", context.getString(R.string.starting_zoom_level)) .commit(); } private static void loadPendingStatus() { // Load the pending setting from the included XML resource InputStream filterFile = App.getAppInstance().getResources().openRawResource(R.raw.configuration); try { DocumentBuilder xml = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document doc = xml.parse(filterFile); NodeList pending = doc.getElementsByTagName("pending"); if (pending.getLength() > 0) { Node el = pending.item(0); pendingEnabled = Boolean.parseBoolean(el.getTextContent()); } } catch (Exception e) { pendingEnabled = false; Log.e(LOG_TAG, "Invalid pending configuration xml file", e); } } @Override public void onCreate() { super.onCreate(); appInstance = this; // Create an instance of login manager immediately, so that // the app can try to auto log in on any saved credentials getLoginManager(); loadPendingStatus(); setupRollbarLogging(); setupGoogleAnalytics(); } private void setupRollbarLogging() { String rollbarKey = getString(R.string.rollbar_client_access_token); if (!TextUtils.isEmpty(rollbarKey)) { Rollbar.init(this, rollbarKey, getString(R.string.environment)); // Include logs if we don't need to add a permission to do so if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { Rollbar.setIncludeLogcat(true); } } } synchronized private void setupGoogleAnalytics() { String appTrackerKey = getString(R.string.app_google_analytics_id); if (!TextUtils.isEmpty(appTrackerKey)) { GoogleAnalytics ga = GoogleAnalytics.getInstance(this); ga.enableAutoActivityReports(this); apptracker = ga.newTracker(appTrackerKey); apptracker.enableAutoActivityTracking(true); } } synchronized public void setUserOnAnalyticsTracker(@Nullable User user) { if (apptracker != null) { String userId = null; if (user != null) { try { userId = Integer.toString(user.getId()); } catch (JSONException e) { Logger.error("Could not get user id", e); } } apptracker.set("&uid", userId); } } synchronized private void setInstanceOnAnalyticstracker(@Nullable InstanceInfo instance) { if (apptracker != null) { // We're overriding the "App Name" to have a place to put instance.url_name // If there is no Instance, we want to default to the original App Name String urlName = getString(R.string.app_name); if (instance != null) { urlName = instance.getUrlName(); } apptracker.setAppName(urlName); } } synchronized public void sendFragmentView(@NonNull Fragment fragment, @NonNull Activity activity) { if (apptracker != null) { String screen = activity.getClass().getCanonicalName() + "/" + fragment.getClass().getCanonicalName(); // "&cd" is the "ScreenView" parameter apptracker.send(new HitBuilders.ScreenViewBuilder().set("&cd", screen).build()); } } public static NearbyList getNearbyList(Context context) { App app = getAppInstance(); if (app.nearbyList == null) { app.nearbyList = new NearbyList(context); } return app.nearbyList; } /** * INSTANCE API * <p> * TODO: Move to another class. */ public static String getInstanceName() { // If this is a skinned app, always return the app name // If this is the cloud app, conditionally return either // the app name or the instance name, depending on whether // the user is connected to an instance. String appName = appInstance.getString(R.string.app_name); if (hasSkinCode()) { return appName; } else { InstanceInfo currentInstance = App.getCurrentInstance(); return currentInstance == null ? appName : currentInstance.getName(); } } public static class InstanceRefreshHandler extends RestHandler<InstanceInfo> { private Callback callback; private InstanceInfo responseInstanceInfo; InstanceRefreshHandler(Callback callback) { this(new InstanceInfo(), callback); } InstanceRefreshHandler() { this(new InstanceInfo(), null); } InstanceRefreshHandler(InstanceInfo instanceInfo, Callback callback) { super(instanceInfo); this.responseInstanceInfo = instanceInfo; this.callback = callback; } private void handleRegisteredCallbacks(Message msg) { if (registeredInstanceCallbacks.size() > 0) { for (Callback registeredCallback : registeredInstanceCallbacks) { registeredCallback.handleMessage(msg); } registeredInstanceCallbacks.clear(); } } private void handleCallback(boolean success) { Message msg = new Message(); Bundle data = new Bundle(); data.putBoolean(SUCCESS_KEY, success); msg.setData(data); // Flag to track if instance request is pending loadingInstance = false; if (callback != null) { callback.handleMessage(msg); } // In addition to the direct caller, other callbacks may have been // registered to be notified of instance loaded status handleRegisteredCallbacks(msg); } @Override public void failure(Throwable e, String message) { String errorMessage = appInstance.getString(R.string.cannot_load_instance) + responseInstanceInfo.getName(); Logger.error(errorMessage, e); Toast.makeText(appInstance, errorMessage, Toast.LENGTH_LONG).show(); handleCallback(false); } @Override public void dataReceived(InstanceInfo instanceInfo) { setCurrentInstance(instanceInfo); handleCallback(true); } } public static InstanceInfo getCurrentInstance() { return currentInstance; } public static boolean hasSkinCode() { return !TextUtils.isEmpty(appInstance.getString(R.string.skin_code)); } public static boolean hasInstanceCode() { return hasSkinCode() || getSharedPreferences().contains(INSTANCE_CODE); } public static void removeCurrentInstance() { currentInstance = null; getSharedPreferences().edit().remove(INSTANCE_CODE).commit(); } /** * Clear the current instance info and force a reload of the configured instance * * @param callback to call when instance is loaded */ public static void reloadInstanceInfo(String instanceCode, Callback callback) { currentInstance = null; loadingInstance = true; // unnecessary if there is a hard coded instance, can't RequestGenerator rg = new RequestGenerator(); InstanceRefreshHandler handler = new InstanceRefreshHandler(callback); rg.getInstanceInfo(instanceCode, handler); } public static void reloadInstanceInfo(Callback callback) { String hardCodedInstanceCode = appInstance.getString(R.string.skin_code); String instanceCode; if (!TextUtils.isEmpty(hardCodedInstanceCode)) { instanceCode = hardCodedInstanceCode; } else if (getSharedPreferences().contains(INSTANCE_CODE)) { String neverUsed = ""; instanceCode = getSharedPreferences().getString(INSTANCE_CODE, neverUsed); } else { Logger.error("Incorrect state, attempted to reload an instance without an instance code"); return; } reloadInstanceInfo(instanceCode, callback); } /** * Callback to ensure the instance has been loaded, either via a loaded, pending * or missing instance info. This method is safe to call at any time to wait for * instance info before proceeding with the callback. * * @param callback */ public void ensureInstanceLoaded(final Callback callback) { if (currentInstance != null) { Message msg = Message.obtain(); Bundle data = new Bundle(); data.putBoolean("success", true); msg.setData(data); callback.handleMessage(msg); } else { // If an instance request is pending, register for a callback on completion, // otherwise, force an instance request if (loadingInstance) { registeredInstanceCallbacks.add(callback); } else { reloadInstanceInfo(callback); } } } /** * Given the provided instance, create fields and filters */ private static void setCurrentInstance(InstanceInfo currentInstance) { App.currentInstance = currentInstance; getSharedPreferences().edit().putString(INSTANCE_CODE, currentInstance.getUrlName()).commit(); App.getAppInstance().setInstanceOnAnalyticstracker(currentInstance); try { fieldManager = new FieldManager(currentInstance); filterManager = new FilterManager(currentInstance); } catch (Exception e) { Logger.error("Unable to create field manager from instance", e); Toast.makeText(appInstance, "Error setting up OpenTreeMap", Toast.LENGTH_LONG).show(); } } }