/*
* Copyright (C) 2012-2015 Paul Watts (paulcwatts@gmail.com), University of South Florida
*
* 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 org.onebusaway.android.app;
import com.google.android.gms.analytics.GoogleAnalytics;
import com.google.android.gms.analytics.Tracker;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.common.api.GoogleApiClient;
import org.onebusaway.android.BuildConfig;
import org.onebusaway.android.R;
import org.onebusaway.android.io.ObaAnalytics;
import org.onebusaway.android.io.ObaApi;
import org.onebusaway.android.io.elements.ObaRegion;
import org.onebusaway.android.provider.ObaContract;
import org.onebusaway.android.util.BuildFlavorUtils;
import org.onebusaway.android.util.LocationUtils;
import org.onebusaway.android.util.PreferenceUtils;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.hardware.GeomagneticField;
import android.location.Location;
import android.location.LocationManager;
import android.preference.PreferenceManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import edu.usf.cutr.open311client.Open311Manager;
import edu.usf.cutr.open311client.models.Open311Option;
import static com.google.android.gms.location.LocationServices.FusedLocationApi;
public class Application extends android.app.Application {
public static final String APP_UID = "app_uid";
// Region preference (long id)
private static final String TAG = "Application";
private SharedPreferences mPrefs;
private static Application mApp;
/**
* We centralize location tracking in the Application class to allow all objects to make
* use of the last known location that we've seen. This is more reliable than using the
* getLastKnownLocation() method of the location providers, and allows us to track both
* Location
* API v1 and fused provider. It allows us to avoid strange behavior like animating a map view
* change when opening a new Activity, even when the previous Activity had a current location.
*/
private static Location mLastKnownLocation = null;
// Magnetic declination is based on location, so track this centrally too.
static GeomagneticField mGeomagneticField = null;
/**
* Google analytics tracker configs
*/
public enum TrackerName {
APP_TRACKER, // Tracker used only in this app.
GLOBAL_TRACKER, // Tracker used by all the apps from a company. eg: roll-up tracking.
}
HashMap<TrackerName, Tracker> mTrackers = new HashMap<TrackerName, Tracker>();
@Override
public void onCreate() {
super.onCreate();
mApp = this;
mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
initOba();
initObaRegion();
initOpen311(getCurrentRegion());
ObaAnalytics.initAnalytics(this);
reportAnalytics();
}
/**
* Per http://developer.android.com/reference/android/app/Application.html#onTerminate(),
* this code is only executed in emulated process environments - it will never be called
* on a production Android device.
*/
@Override
public void onTerminate() {
super.onTerminate();
mApp = null;
}
//
// Public helpers
//
public static Application get() {
return mApp;
}
public static SharedPreferences getPrefs() {
return get().mPrefs;
}
/**
* Returns the last known location that the application has seen, or null if we haven't seen a
* location yet. When trying to get a most recent location in one shot, this method should
* always be called.
*
* @param cxt The Context being used, or null if one isn't available
* @param client The GoogleApiClient being used to obtain fused provider updates, or null if
* one
* isn't available
* @return the last known location that the application has seen, or null if we haven't seen a
* location yet
*/
public static synchronized Location getLastKnownLocation(Context cxt, GoogleApiClient client) {
if (mLastKnownLocation == null) {
// Try to get a last known location from the location providers
try {
mLastKnownLocation = getLocation2(cxt, client);
} catch (SecurityException e) {
Log.e(TAG, "User may have denied location permission - " + e);
}
}
// Pass back last known saved location, hopefully from past location listener updates
return mLastKnownLocation;
}
/**
* Sets the last known location observed by the application via an instance of LocationHelper
*
* @param l a location received by a LocationHelper instance
*/
public static synchronized void setLastKnownLocation(Location l) {
// If the new location is better than the old one, save it
if (LocationUtils.compareLocations(l, mLastKnownLocation)) {
if (mLastKnownLocation == null) {
mLastKnownLocation = new Location("Last known location");
}
mLastKnownLocation.set(l);
mGeomagneticField = new GeomagneticField(
(float) l.getLatitude(),
(float) l.getLongitude(),
(float) l.getAltitude(),
System.currentTimeMillis());
// Log.d(TAG, "Newest best location: " + mLastKnownLocation.toString());
}
}
/**
* Returns the declination of the horizontal component of the magnetic field from true north,
* in
* degrees (i.e. positive means the magnetic field is rotated east that much from true north).
*
* @return declination of the horizontal component of the magnetic field from true north, in
* degrees (i.e. positive means the magnetic field is rotated east that much from true north),
* or null if its not available
*/
public static Float getMagneticDeclination() {
if (mGeomagneticField != null) {
return mGeomagneticField.getDeclination();
} else {
return null;
}
}
/**
* We need to provide the API for a location used to disambiguate stop IDs in case of
* collision,
* or to provide multiple results in the case multiple agencies. But we really don't need it to
* be very accurate.
* <p/>
* Note that the GoogleApiClient must already have been initialized and connected prior to
* calling
* this method, since GoogleApiClient.connect() is asynchronous and doesn't connect before it
* returns,
* which requires additional initialization time (prior to calling this method)
*
* @param client an initialized and connected GoogleApiClient, or null if Google Play Services
* isn't available
* @return a recent location, considering both Google Play Services (if available) and the
* Android Location API
*/
private static Location getLocation(Context cxt, GoogleApiClient client) {
Location last = getLocation2(cxt, client);
if (last != null) {
return last;
} else {
return LocationUtils.getDefaultSearchCenter();
}
}
/**
* Returns a location, considering both Google Play Services (if available) and the Android
* Location API
* <p/>
* Note that the GoogleApiClient must already have been initialized and connected prior to
* calling
* this method, since GoogleApiClient.connect() is asynchronous and doesn't connect before it
* returns,
* which requires additional initialization time (prior to calling this method)
*
* @param client an initialized and connected GoogleApiClient, or null if Google Play Services
* isn't available
* @return a recent location, considering both Google Play Services (if available) and the
* Android Location API
* @throws SecurityException if the user has remove location permissions
*/
private static Location getLocation2(Context cxt, GoogleApiClient client)
throws SecurityException {
GoogleApiAvailability api = GoogleApiAvailability.getInstance();
Location playServices = null;
if (client != null &&
cxt != null &&
api.isGooglePlayServicesAvailable(cxt)
== ConnectionResult.SUCCESS
&& client.isConnected()) {
playServices = FusedLocationApi.getLastLocation(client);
Log.d(TAG, "Got location from Google Play Services, testing against API v1...");
}
Location apiV1 = getLocationApiV1(cxt);
if (LocationUtils.compareLocationsByTime(playServices, apiV1)) {
Log.d(TAG, "Using location from Google Play Services");
return playServices;
} else {
Log.d(TAG, "Using location from Location API v1");
return apiV1;
}
}
private static Location getLocationApiV1(Context cxt) {
if (cxt == null) {
return null;
}
LocationManager mgr = (LocationManager) cxt.getSystemService(Context.LOCATION_SERVICE);
List<String> providers = mgr.getProviders(true);
Location last = null;
for (Iterator<String> i = providers.iterator(); i.hasNext(); ) {
Location loc = mgr.getLastKnownLocation(i.next());
// If this provider has a last location, and either:
// 1. We don't have a last location,
// 2. Our last location is older than this location.
if (LocationUtils.compareLocationsByTime(loc, last)) {
last = loc;
}
}
return last;
}
//
// Helper to get/set the regions
//
public synchronized ObaRegion getCurrentRegion() {
return ObaApi.getDefaultContext().getRegion();
}
public synchronized void setCurrentRegion(ObaRegion region) {
setCurrentRegion(region, true);
}
public synchronized void setCurrentRegion(ObaRegion region, boolean regionChanged) {
if (region != null) {
// First set it in preferences, then set it in OBA.
ObaApi.getDefaultContext().setRegion(region);
PreferenceUtils
.saveLong(mPrefs, getString(R.string.preference_key_region), region.getId());
//We're using a region, so clear the custom API URL preference
setCustomApiUrl(null);
if (regionChanged && region.getOtpBaseUrl() != null) {
setCustomOtpApiUrl(null);
setUseOldOtpApiUrlVersion(false);
}
} else {
//User must have just entered a custom API URL via Preferences, so clear the region info
ObaApi.getDefaultContext().setRegion(null);
PreferenceUtils.saveLong(mPrefs, getString(R.string.preference_key_region), -1);
}
// Init the reporting with the new endpoints
initOpen311(region);
}
/**
* Gets the date at which the region information was last updated, in the number of
* milliseconds
* since January 1, 1970, 00:00:00 GMT
* Default value is 0 if the region info has never been updated.
*
* @return the date at which the region information was last updated, in the number of
* milliseconds since January 1, 1970, 00:00:00 GMT. Default value is 0 if the region info has
* never been updated.
*/
public long getLastRegionUpdateDate() {
SharedPreferences preferences = getPrefs();
return preferences.getLong(getString(R.string.preference_key_last_region_update), 0);
}
/**
* Sets the date at which the region information was last updated
*
* @param date the date at which the region information was last updated, in the number of
* milliseconds since January 1, 1970, 00:00:00 GMT
*/
public void setLastRegionUpdateDate(long date) {
PreferenceUtils
.saveLong(mPrefs, getString(R.string.preference_key_last_region_update), date);
}
/**
* Returns the custom URL if the user has set a custom API URL manually via Preferences, or
* null
* if it has not been set
*
* @return the custom URL if the user has set a custom API URL manually via Preferences, or null
* if it has not been set
*/
public String getCustomApiUrl() {
SharedPreferences preferences = getPrefs();
return preferences.getString(getString(R.string.preference_key_oba_api_url), null);
}
/**
* Sets the custom URL used to reach a OBA REST API server that is not available via the
* Regions
* REST API
*
* @param url the custom URL
*/
public void setCustomApiUrl(String url) {
PreferenceUtils.saveString(getString(R.string.preference_key_oba_api_url), url);
}
/**
* Returns the custom OTP URL if the user has set a custom API URL manually via Preferences, or
* null
* if it has not been set
*
* @return the custom URL if the user has set a custom API URL manually via Preferences, or null
* if it has not been set
*/
public String getCustomOtpApiUrl() {
SharedPreferences preferences = getPrefs();
return preferences.getString(getString(R.string.preference_key_otp_api_url), null);
}
/**
* Sets the custom OTP URL used to reach a OBA REST API server that is not available via the
* Regions
* REST API
*
* @param url the custom URL
*/
public void setCustomOtpApiUrl(String url) {
PreferenceUtils.saveString(getString(R.string.preference_key_otp_api_url), url);
}
/**
* @return true if the OTP url version is old, or false if it has not been set
*/
public boolean getUseOldOtpApiUrlVersion() {
SharedPreferences preferences = getPrefs();
return preferences.getBoolean(getString(R.string.preference_key_otp_api_url_version), false);
}
/**
* Sets the OTP Api url version
*
* @param useOldOtpApiUrlVersion indicates that if otp url structure belongs to older version
*/
public void setUseOldOtpApiUrlVersion(boolean useOldOtpApiUrlVersion) {
PreferenceUtils.saveBoolean(getString(R.string.preference_key_otp_api_url_version),
useOldOtpApiUrlVersion);
}
private static final String HEXES = "0123456789abcdef";
public static String getHex(byte[] raw) {
final StringBuilder hex = new StringBuilder(2 * raw.length);
for (byte b : raw) {
hex.append(HEXES.charAt((b & 0xF0) >> 4))
.append(HEXES.charAt((b & 0x0F)));
}
return hex.toString();
}
private String getAppUid() {
try {
final TelephonyManager telephony =
(TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
final String id = telephony.getDeviceId();
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(id.getBytes());
return getHex(digest.digest());
} catch (Exception e) {
return UUID.randomUUID().toString();
}
}
private void initOba() {
String uuid = mPrefs.getString(APP_UID, null);
if (uuid == null) {
// Generate one and save that.
uuid = getAppUid();
PreferenceUtils.saveString(APP_UID, uuid);
}
checkArrivalStylePreferenceDefault();
// Get the current app version.
PackageManager pm = getPackageManager();
PackageInfo appInfo = null;
try {
appInfo = pm.getPackageInfo(getPackageName(), PackageManager.GET_META_DATA);
} catch (NameNotFoundException e) {
// Do nothing, perhaps we'll get to show it again? Or never.
return;
}
ObaApi.getDefaultContext().setAppInfo(appInfo.versionCode, uuid);
}
private void checkArrivalStylePreferenceDefault() {
String arrivalInfoStylePrefKey = getResources()
.getString(R.string.preference_key_arrival_info_style);
String arrivalInfoStylePref = mPrefs.getString(arrivalInfoStylePrefKey, null);
if (arrivalInfoStylePref == null) {
// First execution of app - set the default arrival info style based on the BuildConfig value
switch (BuildConfig.ARRIVAL_INFO_STYLE) {
case BuildFlavorUtils.ARRIVAL_INFO_STYLE_A:
// Use OBA classic style for default
PreferenceUtils.saveString(arrivalInfoStylePrefKey, BuildFlavorUtils
.getPreferenceOptionForArrivalInfoBuildFlavorStyle(
BuildFlavorUtils.ARRIVAL_INFO_STYLE_A));
Log.d(TAG, "Using arrival info style A (OBA Classic) as default preference");
break;
case BuildFlavorUtils.ARRIVAL_INFO_STYLE_B:
// Use a card-styled footer for default
PreferenceUtils.saveString(arrivalInfoStylePrefKey, BuildFlavorUtils
.getPreferenceOptionForArrivalInfoBuildFlavorStyle(
BuildFlavorUtils.ARRIVAL_INFO_STYLE_B));
Log.d(TAG, "Using arrival info style B (Cards) as default preference");
break;
default:
// Use a card-styled footer for default
PreferenceUtils.saveString(arrivalInfoStylePrefKey, BuildFlavorUtils
.getPreferenceOptionForArrivalInfoBuildFlavorStyle(
BuildFlavorUtils.ARRIVAL_INFO_STYLE_B));
Log.d(TAG, "Using arrival info style B (Cards) as default preference");
break;
}
}
}
private void initObaRegion() {
// Read the region preference, look it up in the DB, then set the region.
long id = mPrefs.getLong(getString(R.string.preference_key_region), -1);
if (id < 0) {
Log.d(TAG, "Regions preference ID is less than 0, returning...");
return;
}
ObaRegion region = ObaContract.Regions.get(this, (int) id);
if (region == null) {
Log.d(TAG, "Regions preference is null, returning...");
return;
}
ObaApi.getDefaultContext().setRegion(region);
}
private void initOpen311(ObaRegion region) {
if (BuildConfig.DEBUG) {
Open311Manager.getSettings().setDebugMode(true);
Open311Manager.getSettings().setDryRun(true);
Log.w(TAG,
"Open311 issue reporting is in debug/dry run mode - no issues will be submitted.");
}
// Clear all open311 endpoints
Open311Manager.clearOpen311();
// Read the open311 preferences from the region and set
if (region != null && region.getOpen311Servers() != null) {
for (ObaRegion.Open311Server open311Server : region.getOpen311Servers()) {
String jurisdictionId = open311Server.getJuridisctionId();
Open311Option option = new Open311Option(open311Server.getBaseUrl(),
open311Server.getApiKey(),
TextUtils.isEmpty(jurisdictionId) ? null : jurisdictionId);
Open311Manager.initOpen311WithOption(option);
}
}
}
public synchronized Tracker getTracker(TrackerName trackerId) {
if (!mTrackers.containsKey(trackerId)) {
GoogleAnalytics analytics = GoogleAnalytics.getInstance(this);
Tracker t = (trackerId == TrackerName.APP_TRACKER) ? analytics.newTracker(R.xml.app_tracker)
: (trackerId == TrackerName.GLOBAL_TRACKER) ? analytics.newTracker(R.xml.global_tracker)
: analytics.newTracker(R.xml.global_tracker);
mTrackers.put(trackerId, t);
}
return mTrackers.get(trackerId);
}
private void reportAnalytics() {
if (getCustomApiUrl() == null && getCurrentRegion() != null) {
ObaAnalytics.reportEventWithCategory(ObaAnalytics.ObaEventCategory.APP_SETTINGS.toString(),
getString(R.string.analytics_action_configured_region), getString(R.string.analytics_label_region)
+ getCurrentRegion().getName());
} else if (Application.get().getCustomApiUrl() != null) {
String customUrl = null;
MessageDigest digest = null;
try {
digest = MessageDigest.getInstance("SHA-1");
digest.update(getCustomApiUrl().getBytes());
customUrl = getString(R.string.analytics_label_custom_url) +
": " + getHex(digest.digest());
} catch (Exception e) {
customUrl = Application.get().getString(R.string.analytics_label_custom_url);
}
ObaAnalytics.reportEventWithCategory(ObaAnalytics.ObaEventCategory.APP_SETTINGS.toString(),
getString(R.string.analytics_action_configured_region), getString(R.string.analytics_label_region)
+ customUrl);
}
Boolean experimentalRegions = getPrefs().getBoolean(getString(R.string.preference_key_experimental_regions),
Boolean.FALSE);
Boolean autoRegion = getPrefs().getBoolean(getString(R.string.preference_key_auto_select_region),
true);
ObaAnalytics.reportEventWithCategory(ObaAnalytics.ObaEventCategory.APP_SETTINGS.toString(),
getString(R.string.analytics_action_edit_general), getString(R.string.analytics_label_experimental)
+ (experimentalRegions ? "YES" : "NO"));
ObaAnalytics.reportEventWithCategory(ObaAnalytics.ObaEventCategory.APP_SETTINGS.toString(),
getString(R.string.analytics_action_edit_general), getString(R.string.analytics_label_region_auto)
+ (autoRegion ? "YES" : "NO"));
}
}