package com.dozuki.ifixit; import android.accounts.Account; import android.app.Activity; import android.app.Application; import android.content.ContentResolver; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Bundle; import android.os.StrictMode; import android.preference.PreferenceManager; import android.util.Log; import com.dozuki.ifixit.model.auth.Authenticator; import com.dozuki.ifixit.model.dozuki.Site; import com.dozuki.ifixit.model.dozuki.SiteChangedEvent; import com.dozuki.ifixit.model.user.LoginEvent; import com.dozuki.ifixit.model.user.User; import com.dozuki.ifixit.util.ImageSizes; import com.dozuki.ifixit.util.OkConnectionFactory; import com.dozuki.ifixit.util.Utils; import com.dozuki.ifixit.util.api.Api; import com.dozuki.ifixit.util.api.ApiCall; import com.dozuki.ifixit.util.api.ApiContentProvider; import com.dozuki.ifixit.util.api.ApiSyncAdapter; import com.github.kevinsawicki.http.HttpRequest; import com.google.analytics.tracking.android.Fields; import com.google.analytics.tracking.android.GAServiceManager; import com.google.analytics.tracking.android.GoogleAnalytics; import com.google.analytics.tracking.android.Logger; import com.google.analytics.tracking.android.MapBuilder; import com.google.analytics.tracking.android.StandardExceptionParser; import com.google.analytics.tracking.android.Tracker; import com.squareup.otto.Bus; import java.net.URL; public class App extends Application { /* * Google Analytics configuration values. */ // Dispatch period in seconds. private static final int GA_DISPATCH_PERIOD = 30; // Key used to store a user's tracking preferences in SharedPreferences. private static final String TRACKING_PREF_KEY = "trackingPreference"; private static Tracker mGaTracker; private static final String PREFERENCE_FILE = "PREFERENCE_FILE"; private static final String FIRST_TIME_GALLERY_USER = "FIRST_TIME_GALLERY_USER"; private static final String LAST_SYNC_TIME = "LAST_SYNC_TIME"; public static final long NEVER_SYNCED_VALUE = -1; private static final String TAG = "App"; /** * Singleton reference. */ private static App sApp; /** * Singleton for Bus (Otto). */ private static Bus sBus; /** * Currently logged in user or null if user is not logged in. */ private User mUser; /** * Current logged in Account. Must be kept in sync with mUser. */ private Account mAccount; /** * Current site. Shouldn't ever be null. Set to "dozuki" for dozuki splash screen. */ private Site mSite; /** * True if the user is in the middle of authenticating. Used to determine whether or * not to open a new login dialog and for finishing Activities that require the user * to be logged in. */ private boolean mIsLoggingIn = false; /** * User agent singleton. */ private String mUserAgent = null; private boolean mUrlStreamFactorySet = false; private boolean mConnectionFactorySet = false; @Override public void onCreate() { // OkHttp changes the global SSL context, breaks other HTTP clients. Google Analytics uses a different http // client, which OkHttp doesn't handle well. // https://github.com/square/okhttp/issues/184 if (!mUrlStreamFactorySet) { URL.setURLStreamHandlerFactory(Utils.createOkHttpClient()); mUrlStreamFactorySet = true; } // Use OkHttp instead of HttpUrlConnection to handle HTTP requests, OkHttp supports 2.2 while HttpURLConnection // is a bit buggy on froyo. if (!mConnectionFactorySet) { HttpRequest.setConnectionFactory(new OkConnectionFactory()); mConnectionFactorySet = true; } if (false && inDebug()) { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectDiskReads() .detectDiskWrites() .detectNetwork() // or .detectAll() for all detectable problems .penaltyLog() .build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedSqlLiteObjects() .detectLeakedClosableObjects() .penaltyLog() .penaltyDeath() .build()); } super.onCreate(); initializeGa(); Api.init(); ImageSizes.init(this); sApp = this; setSite(getDefaultSite()); } public static void sendEvent(String category, String action, String label, Long value) { mGaTracker.send(MapBuilder.createEvent(category, action, label, value).build()); } public static void sendScreenView(String screenName) { mGaTracker.send(MapBuilder.createAppView().set(Fields.SCREEN_NAME, screenName).build()); } public static void sendException(String tag, String message, Exception exception) { Log.e(tag, message, exception); mGaTracker.send(MapBuilder.createException( new StandardExceptionParser(get(), null).getDescription( Thread.currentThread().getName(), exception), false).build()); } /* * Method to handle basic Google Analytics initialization. This call will not * block as all Google Analytics work occurs off the main thread. */ private void initializeGa() { GoogleAnalytics ga = GoogleAnalytics.getInstance(this); mGaTracker = ga.getTracker(BuildConfig.GA_PROPERTY_ID); GAServiceManager.getInstance().setLocalDispatchPeriod(GA_DISPATCH_PERIOD); // Set dryRun to disable event dispatching. ga.setDryRun(BuildConfig.DEBUG); ga.getLogger().setLogLevel(BuildConfig.DEBUG ? Logger.LogLevel.INFO : Logger.LogLevel.WARNING); // Set the opt out flag when user updates a tracking preference. SharedPreferences userPrefs = PreferenceManager.getDefaultSharedPreferences(this); userPrefs.registerOnSharedPreferenceChangeListener(new SharedPreferences.OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (key.equals(TRACKING_PREF_KEY)) { GoogleAnalytics.getInstance(getApplicationContext()) .setAppOptOut(sharedPreferences.getBoolean(key, false)); } } }); } /** * Singleton getter. */ public static App get() { return sApp; } public Site getSite() { return mSite; } public void setSite(Site site) { mSite = site; // Update logged in user based on current site. setupLoggedInUser(site); getBus().post(new SiteChangedEvent(mSite, mUser)); } public String getTopicName() { String topicName = getString(R.string.category); if (mSite.mName.equals("ifixit")) { topicName = getString(R.string.device); } return topicName; } public boolean inPortraitMode() { return getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; } /** * Returns the resource id for the current site's theme. */ public int getSiteTheme() { if (mSite == null) { return R.style.Theme_Dozuki; } return mSite.theme(); } public String getUserAgent() { if (mUserAgent == null) { int versionCode = -1; String versionName = ""; try { PackageInfo packageInfo; packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0); versionCode = packageInfo.versionCode; versionName = packageInfo.versionName; } catch (PackageManager.NameNotFoundException e) { Log.e("iFixit", "Can't get application version", e); } /** * Returns the Site that this app is "built" for. e.g. Dozuki even if the user * is currently viewing a different nanosite. */ Site currentApp = getDefaultSite(); mUserAgent = currentApp.mTitle + "Android/" + versionName + " (" + versionCode + ") | " + System.getProperty("http.agent"); } return mUserAgent; } public void setIsLoggingIn(boolean isLoggingIn) { mIsLoggingIn = isLoggingIn; } public boolean isLoggingIn() { return mIsLoggingIn; } // Returns true if the app is in debug mode (not in production) public static boolean inDebug() { return BuildConfig.DEBUG; } public static Bus getBus() { if (sBus == null) { sBus = new Bus(); } return sBus; } public User getUser() { return mUser; } public Account getUserAccount() { return mAccount; } private void setupLoggedInUser(Site site) { Authenticator authenticator = new Authenticator(this); mAccount = authenticator.getAccountForSite(site); mUser = null; if (mAccount != null) { mUser = authenticator.createUser(mAccount); } } public boolean isFirstTimeGalleryUser() { SharedPreferences preferenceFile = getSharedPreferences(PREFERENCE_FILE, MODE_PRIVATE); return preferenceFile.getBoolean(FIRST_TIME_GALLERY_USER, true); } public void setFirstTimeGalleryUser(boolean firstTimeGalleryUser) { SharedPreferences preferenceFile = getSharedPreferences(PREFERENCE_FILE, MODE_PRIVATE); Editor editor = preferenceFile.edit(); editor.putBoolean(FIRST_TIME_GALLERY_USER, firstTimeGalleryUser); editor.commit(); } public boolean isUserLoggedIn() { return mUser != null; } /** * Returns true iff this is the dozuki app (com.dozuki.dozuki). */ public static boolean isDozukiApp() { return BuildConfig.SITE_NAME.equals("dozuki"); } /** * Should only be used to get the current site for a "custom" app * (iFixit/Crucial etc.). */ private Site getDefaultSite() { return Site.getSite(BuildConfig.SITE_NAME); } /** * Logs the given user in by writing it to SharedPreferences and setting mUser. */ public void login(User user, String email, String password, boolean notify) { mUser = user; // Set the email because it isn't included in the API response. mUser.mEmail = email; mAccount = new Authenticator(this).onAccountAuthenticated(mSite, email, user.getUsername(), user.getUserid(), password, user.getAuthToken()); if (notify) { getBus().post(new LoginEvent.Login(mUser)); } setIsLoggingIn(false); /** * Execute pending API call if one exists. */ ApiCall pendingApiCall = Api.getAndRemovePendingApiCall(this); if (pendingApiCall != null) { pendingApiCall.updateUser(mUser); Api.call(null, pendingApiCall); } } /** * Light version of logout that doesn't fire any events or perform any API calls. * logout, below, should almost always be the one to use. * * Warning: This removes the account from AccountManager which could have very bad * consequences for account preferences including sync. */ public void shallowLogout(boolean removeAccount) { if (removeAccount && mAccount != null) { new Authenticator(this).removeAccount(mAccount); } mUser = null; mAccount = null; } /** * Logs the currently logged in user out by deleting it from SharedPreferences, making * the logout API call to delete the auth token, and resetting mUser. */ public void logout(Activity activity) { // Check if the user is null because we're paranoid. if (mUser != null && activity != null) { // Perform the API call to delete the user's authToken. Api.call(activity, ApiCall.logout(mUser)); } shallowLogout(true); getBus().post(new LoginEvent.Logout()); } /** * Call when the user has cancelled login. */ public void cancelLogin() { // Clear the pending api call if one exists. Api.getAndRemovePendingApiCall(this); setIsLoggingIn(false); getBus().post(new LoginEvent.Cancel()); } /** * Requests a sync for the current user. This operation does nothing if * force is false and a sync is already in progress. */ public void requestSync(boolean force) { if (!isUserLoggedIn()) { return; } String authority = ApiContentProvider.getAuthority(); boolean syncActive = ContentResolver.isSyncActive(mAccount, authority); if (syncActive && !force) { // Do nothing if the sync is active and we don't want to force it. return; } if (syncActive) { // Sync is already started so lets restart it. ApiSyncAdapter.restartSync(this); } else { Bundle bundle = new Bundle(); bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); bundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); ContentResolver.requestSync(mAccount, authority, bundle); } } public void cancelSync() { if (!isUserLoggedIn()) { return; } String authority = ApiContentProvider.getAuthority(); ContentResolver.cancelSync(mAccount, authority); } public void setSyncAutomatically(boolean syncAutomatically) { if (!isUserLoggedIn()) { return; } ContentResolver.setSyncAutomatically(mAccount, ApiContentProvider.getAuthority(), syncAutomatically); } public boolean getSyncAutomatically() { if (!isUserLoggedIn()) { return false; } return ContentResolver.getSyncAutomatically(mAccount, ApiContentProvider.getAuthority()); } /** * Sets the last sync time for the given user to the current time. */ public void setLastSyncTime(Site site, User user) { String lastSyncTimeKey = getLastSyncTimeKey(site, user); SharedPreferences preferenceFile = getSharedPreferences(PREFERENCE_FILE, MODE_PRIVATE | MODE_MULTI_PROCESS); Editor editor = preferenceFile.edit(); editor.putLong(lastSyncTimeKey, System.currentTimeMillis()); editor.commit(); } public long getLastSyncTime() { if (!isUserLoggedIn()) { return NEVER_SYNCED_VALUE; } String lastSyncTimeKey = getLastSyncTimeKey(mSite, mUser); SharedPreferences preferenceFile = getSharedPreferences(PREFERENCE_FILE, MODE_PRIVATE | MODE_MULTI_PROCESS); return preferenceFile.getLong(lastSyncTimeKey, NEVER_SYNCED_VALUE); } private String getLastSyncTimeKey(Site site, User user) { return LAST_SYNC_TIME + "_" + site.mSiteid + "_" + user.getUserid(); } public boolean isScreenLarge() { final int screenSize = getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK; return screenSize == Configuration.SCREENLAYOUT_SIZE_LARGE || screenSize == Configuration.SCREENLAYOUT_SIZE_XLARGE; } public boolean isConnected() { ConnectivityManager cm = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo netInfo = cm.getActiveNetworkInfo(); return netInfo != null && netInfo.isConnected(); } }