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();
}
}
}