blob: b449c10547879010f981c179df18df8c90d88676 [file] [log] [blame]
package org.wordpress.android;
import android.app.Activity;
import android.app.Application;
import android.app.Dialog;
import android.content.ComponentCallbacks2;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.net.http.HttpResponseCache;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.StrictMode;
import android.os.SystemClock;
import android.support.multidex.MultiDexApplication;
import android.text.TextUtils;
import android.util.AndroidRuntimeException;
import android.webkit.WebSettings;
import android.webkit.WebView;
import com.android.volley.RequestQueue;
import com.android.volley.VolleyLog;
import com.android.volley.toolbox.ImageLoader;
import com.android.volley.toolbox.Volley;
import com.crashlytics.android.Crashlytics;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.gcm.GoogleCloudMessaging;
import com.google.android.gms.iid.InstanceID;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.wordpress.rest.RestClient;
import com.wordpress.rest.RestRequest;
import org.wordpress.android.analytics.AnalyticsTracker;
import org.wordpress.android.analytics.AnalyticsTracker.Stat;
import org.wordpress.android.analytics.AnalyticsTrackerMixpanel;
import org.wordpress.android.analytics.AnalyticsTrackerNosara;
import org.wordpress.android.datasets.ReaderDatabase;
import org.wordpress.android.models.AccountHelper;
import org.wordpress.android.models.Blog;
import org.wordpress.android.networking.ConnectionChangeReceiver;
import org.wordpress.android.networking.OAuthAuthenticator;
import org.wordpress.android.networking.OAuthAuthenticatorFactory;
import org.wordpress.android.networking.RestClientUtils;
import org.wordpress.android.networking.SelfSignedSSLCertsManager;
import org.wordpress.android.ui.ActivityId;
import org.wordpress.android.ui.accounts.helpers.UpdateBlogListTask.GenericUpdateBlogListTask;
import org.wordpress.android.ui.notifications.utils.NotificationsUtils;
import org.wordpress.android.ui.notifications.utils.SimperiumUtils;
import org.wordpress.android.ui.prefs.AppPrefs;
import org.wordpress.android.ui.stats.StatsWidgetProvider;
import org.wordpress.android.ui.stats.datasets.StatsDatabaseHelper;
import org.wordpress.android.ui.stats.datasets.StatsTable;
import org.wordpress.android.util.AnalyticsUtils;
import org.wordpress.android.util.AppLog;
import org.wordpress.android.util.AppLog.T;
import org.wordpress.android.util.BitmapLruCache;
import org.wordpress.android.util.CoreEvents;
import org.wordpress.android.util.CoreEvents.UserSignedOutCompletely;
import org.wordpress.android.util.CoreEvents.UserSignedOutWordPressCom;
import org.wordpress.android.util.DateTimeUtils;
import org.wordpress.android.util.HelpshiftHelper;
import org.wordpress.android.util.NetworkUtils;
import org.wordpress.android.util.PackageUtils;
import org.wordpress.android.util.ProfilingUtils;
import org.wordpress.android.util.RateLimitedTask;
import org.wordpress.android.util.SqlUtils;
import org.wordpress.android.util.VolleyUtils;
import org.wordpress.android.util.WPActivityUtils;
import org.wordpress.passcodelock.AbstractAppLock;
import org.wordpress.passcodelock.AppLockManager;
import org.xmlrpc.android.ApiHelper;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.security.GeneralSecurityException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import de.greenrobot.event.EventBus;
import io.fabric.sdk.android.Fabric;
public class WordPress extends MultiDexApplication {
public static String versionName;
public static Blog currentBlog;
public static WordPressDB wpDB;
public static RequestQueue requestQueue;
public static ImageLoader imageLoader;
private static RestClientUtils mRestClientUtils;
private static RestClientUtils mRestClientUtilsVersion1_1;
private static RestClientUtils mRestClientUtilsVersion1_2;
private static RestClientUtils mRestClientUtilsVersion1_3;
private static RestClientUtils mRestClientUtilsVersion0;
private static final int SECONDS_BETWEEN_OPTIONS_UPDATE = 10 * 60;
private static final int SECONDS_BETWEEN_BLOGLIST_UPDATE = 6 * 60 * 60;
private static final int SECONDS_BETWEEN_DELETE_STATS = 5 * 60; // 5 minutes
private static Context mContext;
private static BitmapLruCache mBitmapCache;
/**
* Updates Options for the current blog in background.
*/
public static RateLimitedTask sUpdateCurrentBlogOption = new RateLimitedTask(SECONDS_BETWEEN_OPTIONS_UPDATE) {
protected boolean run() {
Blog currentBlog = WordPress.getCurrentBlog();
if (currentBlog != null) {
new ApiHelper.RefreshBlogContentTask(currentBlog, null).executeOnExecutor(
AsyncTask.THREAD_POOL_EXECUTOR, false);
return true;
}
return false;
}
};
/**
* Update blog list in a background task. Broadcast WordPress.BROADCAST_ACTION_BLOG_LIST_CHANGED if the
* list changed.
*/
public static RateLimitedTask sUpdateWordPressComBlogList = new RateLimitedTask(SECONDS_BETWEEN_BLOGLIST_UPDATE) {
protected boolean run() {
if (AccountHelper.isSignedInWordPressDotCom()) {
new GenericUpdateBlogListTask(getContext()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
return true;
}
};
/**
* Delete stats cache that is already expired
*/
public static RateLimitedTask sDeleteExpiredStats = new RateLimitedTask(SECONDS_BETWEEN_DELETE_STATS) {
protected boolean run() {
// Offload to a separate thread. We don't want to slown down the app on startup/resume.
new Thread(new Runnable() {
public void run() {
// subtracts to the current time the cache TTL
long timeToDelete = System.currentTimeMillis() - (StatsTable.CACHE_TTL_MINUTES * 60 * 1000);
StatsTable.deleteOldStats(WordPress.getContext(), timeToDelete);
}
}).start();
return true;
}
};
public static BitmapLruCache getBitmapCache() {
if (mBitmapCache == null) {
// The cache size will be measured in kilobytes rather than
// number of items. See http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 16; //Use 1/16th of the available memory for this memory cache.
mBitmapCache = new BitmapLruCache(cacheSize);
}
return mBitmapCache;
}
@Override
public void onCreate() {
super.onCreate();
long startDate = SystemClock.elapsedRealtime();
mContext = this;
ProfilingUtils.start("App Startup");
// Enable log recording
AppLog.enableRecording(true);
AppLog.i(T.UTILS, "WordPress.onCreate");
if (!PackageUtils.isDebugBuild()) {
Fabric.with(this, new Crashlytics());
}
versionName = PackageUtils.getVersionName(this);
initWpDb();
enableHttpResponseCache(mContext);
// EventBus setup
EventBus.TAG = "WordPress-EVENT";
EventBus.builder()
.logNoSubscriberMessages(false)
.sendNoSubscriberEvent(false)
.throwSubscriberException(true)
.installDefaultEventBus();
EventBus.getDefault().register(this);
RestClientUtils.setUserAgent(getUserAgent());
// Volley networking setup
setupVolleyQueue();
AppLockManager.getInstance().enableDefaultAppLockIfAvailable(this);
if (AppLockManager.getInstance().isAppLockFeatureEnabled()) {
AppLockManager.getInstance().getAppLock().setExemptActivities(
new String[]{"org.wordpress.android.ui.ShareIntentReceiverActivity"});
}
HelpshiftHelper.init(this);
ApplicationLifecycleMonitor applicationLifecycleMonitor = new ApplicationLifecycleMonitor();
registerComponentCallbacks(applicationLifecycleMonitor);
registerActivityLifecycleCallbacks(applicationLifecycleMonitor);
initAnalytics(SystemClock.elapsedRealtime() - startDate);
// If users uses a custom locale set it on start of application
WPActivityUtils.applyLocale(getContext());
}
private void initAnalytics(final long elapsedTimeOnCreate) {
AnalyticsTracker.registerTracker(new AnalyticsTrackerMixpanel(getContext(), BuildConfig.MIXPANEL_TOKEN));
AnalyticsTracker.registerTracker(new AnalyticsTrackerNosara(getContext()));
AnalyticsTracker.init(getContext());
AnalyticsUtils.refreshMetadata();
// Track app upgrade and install
int versionCode = PackageUtils.getVersionCode(getContext());
int oldVersionCode = AppPrefs.getLastAppVersionCode();
if (oldVersionCode == 0) {
// Track application installed if there isn't old version code
AnalyticsTracker.track(Stat.APPLICATION_INSTALLED);
AppPrefs.setVisualEditorPromoRequired(false);
}
if (oldVersionCode != 0 && oldVersionCode < versionCode) {
Map<String, Long> properties = new HashMap<String, Long>(1);
properties.put("elapsed_time_on_create", elapsedTimeOnCreate);
// app upgraded
AnalyticsTracker.track(AnalyticsTracker.Stat.APPLICATION_UPGRADED, properties);
}
AppPrefs.setLastAppVersionCode(versionCode);
}
/**
* Application.onCreate is called before any activity, service, or receiver - it can be called while the app
* is in background by a sticky service or a receiver, so we don't want Application.onCreate to make network request
* or other heavy tasks.
*
* This deferredInit method is called when a user starts an activity for the first time, ie. when he sees a
* screen for the first time. This allows us to have heavy calls on first activity startup instead of app startup.
*/
public void deferredInit(Activity activity) {
AppLog.i(T.UTILS, "Deferred Initialisation");
if (isGooglePlayServicesAvailable(activity)) {
// Register for Cloud messaging
startService(new Intent(this, GCMRegistrationIntentService.class));
}
configureSimperium();
// Refresh account informations
if (AccountHelper.isSignedInWordPressDotCom()) {
AccountHelper.getDefaultAccount().fetchAccountDetails();
}
}
// Configure Simperium and start buckets if we are signed in to WP.com
private void configureSimperium() {
if (AccountHelper.isSignedInWordPressDotCom()) {
AppLog.i(T.NOTIFS, "Configuring Simperium");
SimperiumUtils.configureSimperium(this, AccountHelper.getDefaultAccount().getAccessToken());
}
}
public static void setupVolleyQueue() {
requestQueue = Volley.newRequestQueue(mContext, VolleyUtils.getHTTPClientStack(mContext));
imageLoader = new ImageLoader(requestQueue, getBitmapCache());
VolleyLog.setTag(AppLog.TAG);
// http://stackoverflow.com/a/17035814
imageLoader.setBatchedResponseDelay(0);
}
private void initWpDb() {
if (!createAndVerifyWpDb()) {
AppLog.e(T.DB, "Invalid database, sign out user and delete database");
currentBlog = null;
if (wpDB != null) {
wpDB.updateLastBlogId(-1);
}
// Force DB deletion
WordPressDB.deleteDatabase(this);
wpDB = new WordPressDB(this);
}
}
private boolean createAndVerifyWpDb() {
try {
wpDB = new WordPressDB(this);
// verify account data - query will return 1 if any blog names or urls are null
int result = SqlUtils.intForQuery(wpDB.getDatabase(),
"SELECT 1 FROM accounts WHERE blogName IS NULL OR url IS NULL LIMIT 1", null);
return result != 1;
} catch (RuntimeException e) {
AppLog.e(T.DB, e);
return false;
}
}
public static Context getContext() {
return mContext;
}
public static RestClientUtils getRestClientUtils() {
if (mRestClientUtils == null) {
OAuthAuthenticator authenticator = OAuthAuthenticatorFactory.instantiate();
mRestClientUtils = new RestClientUtils(mContext, requestQueue, authenticator, mOnAuthFailedListener);
}
return mRestClientUtils;
}
private static RestRequest.OnAuthFailedListener mOnAuthFailedListener = new RestRequest.OnAuthFailedListener() {
@Override
public void onAuthFailed() {
if (getContext() == null) return;
// If this is called, it means the WP.com token is no longer valid.
EventBus.getDefault().post(new CoreEvents.RestApiUnauthorized());
}
};
public static RestClientUtils getRestClientUtilsV1_1() {
if (mRestClientUtilsVersion1_1 == null) {
OAuthAuthenticator authenticator = OAuthAuthenticatorFactory.instantiate();
mRestClientUtilsVersion1_1 = new RestClientUtils(mContext, requestQueue, authenticator, mOnAuthFailedListener, RestClient.REST_CLIENT_VERSIONS.V1_1);
}
return mRestClientUtilsVersion1_1;
}
public static RestClientUtils getRestClientUtilsV1_2() {
if (mRestClientUtilsVersion1_2 == null) {
OAuthAuthenticator authenticator = OAuthAuthenticatorFactory.instantiate();
mRestClientUtilsVersion1_2 = new RestClientUtils(mContext, requestQueue, authenticator, mOnAuthFailedListener, RestClient.REST_CLIENT_VERSIONS.V1_2);
}
return mRestClientUtilsVersion1_2;
}
public static RestClientUtils getRestClientUtilsV1_3() {
if (mRestClientUtilsVersion1_3 == null) {
OAuthAuthenticator authenticator = OAuthAuthenticatorFactory.instantiate();
mRestClientUtilsVersion1_3 = new RestClientUtils(mContext, requestQueue, authenticator, mOnAuthFailedListener, RestClient.REST_CLIENT_VERSIONS.V1_3);
}
return mRestClientUtilsVersion1_3;
}
public static RestClientUtils getRestClientUtilsV0() {
if (mRestClientUtilsVersion0 == null) {
OAuthAuthenticator authenticator = OAuthAuthenticatorFactory.instantiate();
mRestClientUtilsVersion0 = new RestClientUtils(mContext, requestQueue, authenticator, mOnAuthFailedListener, RestClient.REST_CLIENT_VERSIONS.V0);
}
return mRestClientUtilsVersion0;
}
/**
* enables "strict mode" for testing - should NEVER be used in release builds
*/
private static void enableStrictMode() {
// return if the build is not a debug build
if (!BuildConfig.DEBUG) {
AppLog.e(T.UTILS, "You should not call enableStrictMode() on a non debug build");
return;
}
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.penaltyFlashScreen()
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectActivityLeaks()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.detectLeakedRegistrationObjects() // <-- requires Jelly Bean
.penaltyLog()
.build());
AppLog.w(T.UTILS, "Strict mode enabled");
}
public boolean isGooglePlayServicesAvailable(Activity activity) {
GoogleApiAvailability googleApiAvailability = GoogleApiAvailability.getInstance();
int connectionResult = googleApiAvailability.isGooglePlayServicesAvailable(activity);
switch (connectionResult) {
// Success: return true
case ConnectionResult.SUCCESS:
return true;
// Play Services unavailable, show an error dialog is the Play Services Lib needs an update
case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED:
Dialog dialog = googleApiAvailability.getErrorDialog(activity, connectionResult, 0);
if (dialog != null) {
dialog.show();
}
default:
case ConnectionResult.SERVICE_MISSING:
case ConnectionResult.SERVICE_DISABLED:
case ConnectionResult.SERVICE_INVALID:
AppLog.w(T.NOTIFS, "Google Play Services unavailable, connection result: "
+ googleApiAvailability.getErrorString(connectionResult));
}
return false;
}
/**
* Get the currently active blog.
* <p/>
* If the current blog is not already set, try and determine the last active blog from the last
* time the application was used. If we're not able to determine the last active blog, try to
* select the first visible blog. If there are no more visible blogs, try to select the first
* hidden blog. If there are no blogs at all, return null.
*/
public static Blog getCurrentBlog() {
if (currentBlog == null || !wpDB.isDotComBlogVisible(currentBlog.getRemoteBlogId())) {
attemptToRestoreLastActiveBlog();
}
return currentBlog;
}
/**
* Get the blog with the specified ID.
*
* @param id ID of the blog to retrieve.
* @return the blog with the specified ID, or null if blog could not be retrieved.
*/
public static Blog getBlog(int id) {
try {
return wpDB.instantiateBlogByLocalId(id);
} catch (Exception e) {
return null;
}
}
/**
* Set the last active blog as the current blog.
*
* @return the current blog
*/
public static Blog setCurrentBlogToLastActive() {
List<Map<String, Object>> accounts = WordPress.wpDB.getVisibleBlogs();
int lastBlogId = WordPress.wpDB.getLastBlogId();
if (lastBlogId != -1) {
for (Map<String, Object> account : accounts) {
int id = Integer.valueOf(account.get("id").toString());
if (id == lastBlogId) {
setCurrentBlog(id);
return currentBlog;
}
}
}
// Previous active blog is hidden or deleted
currentBlog = null;
return null;
}
/**
* Set the blog with the specified id as the current blog.
*
* @param id id of the blog to set as current
*/
public static void setCurrentBlog(int id) {
currentBlog = getBlog(id);
}
public static void setCurrentBlogAndSetVisible(int id) {
setCurrentBlog(id);
if (currentBlog != null && currentBlog.isHidden()) {
wpDB.setDotComBlogsVisibility(id, true);
currentBlog.setHidden(false);
}
}
/**
* returns the blogID of the current blog or null if current blog is null or remoteID is null.
*/
public static String getCurrentRemoteBlogId() {
return (getCurrentBlog() != null ? getCurrentBlog().getDotComBlogId() : null);
}
public static int getCurrentLocalTableBlogId() {
return (getCurrentBlog() != null ? getCurrentBlog().getLocalTableBlogId() : -1);
}
/**
* Sign out from wpcom account.
* Note: This method must not be called on UI Thread.
*/
public static void WordPressComSignOut(Context context) {
// Keep the analytics tracking at the beginning, before the account data is actual removed.
AnalyticsTracker.track(Stat.ACCOUNT_LOGOUT);
removeWpComUserRelatedData(context);
// broadcast an event: wpcom user signed out
EventBus.getDefault().post(new UserSignedOutWordPressCom());
// broadcast an event only if the user is completely signed out
if (!AccountHelper.isSignedIn()) {
EventBus.getDefault().post(new UserSignedOutCompletely());
}
}
@SuppressWarnings("unused")
public void onEventMainThread(UserSignedOutCompletely event) {
try {
SelfSignedSSLCertsManager.getInstance(getContext()).emptyLocalKeyStoreFile();
} catch (GeneralSecurityException e) {
AppLog.e(T.UTILS, "Error while cleaning the Local KeyStore File", e);
} catch (IOException e) {
AppLog.e(T.UTILS, "Error while cleaning the Local KeyStore File", e);
}
flushHttpCache();
// Analytics resets
AnalyticsTracker.endSession(false);
AnalyticsTracker.clearAllData();
// disable passcode lock
AbstractAppLock appLock = AppLockManager.getInstance().getAppLock();
if (appLock != null) {
appLock.setPassword(null);
}
// dangerously delete all content!
wpDB.dangerouslyDeleteAllContent();
}
public static void removeWpComUserRelatedData(Context context) {
// cancel all Volley requests - do this before unregistering push since that uses
// a Volley request
VolleyUtils.cancelAllRequests(requestQueue);
NotificationsUtils.unregisterDevicePushNotifications(context);
try {
String gcmId = BuildConfig.GCM_ID;
if (!TextUtils.isEmpty(gcmId)) {
InstanceID.getInstance(context).deleteToken(gcmId, GoogleCloudMessaging.INSTANCE_ID_SCOPE);
}
} catch (Exception e) {
AppLog.e(T.NOTIFS, "Could not delete GCM Token", e);
}
// delete wpcom blogs
wpDB.deleteWordPressComBlogs(context);
// reset default account
AccountHelper.getDefaultAccount().signout();
// reset all reader-related prefs & data
AppPrefs.reset();
ReaderDatabase.reset();
// Reset Stats Data
StatsDatabaseHelper.getDatabase(context).reset();
StatsWidgetProvider.updateWidgetsOnLogout(context);
// Reset Simperium buckets (removes local data)
SimperiumUtils.resetBucketsAndDeauthorize();
}
public static String getLoginUrl(Blog blog) {
String loginURL = null;
Gson gson = new Gson();
Type type = new TypeToken<Map<?, ?>>() {
}.getType();
Map<?, ?> blogOptions = gson.fromJson(blog.getBlogOptions(), type);
if (blogOptions != null) {
Map<?, ?> homeURLMap = (Map<?, ?>) blogOptions.get("login_url");
if (homeURLMap != null)
loginURL = homeURLMap.get("value").toString();
}
// Try to guess the login URL if blogOptions is null (blog not added to the app), or WP version is < 3.6
if (loginURL == null) {
if (blog.getUrl().lastIndexOf("/") != -1) {
return blog.getUrl().substring(0, blog.getUrl().lastIndexOf("/"))
+ "/wp-login.php";
} else {
return blog.getUrl().replace("xmlrpc.php", "wp-login.php");
}
}
return loginURL;
}
/**
* Device's default User-Agent string.
* E.g.:
* "Mozilla/5.0 (Linux; Android 6.0; Android SDK built for x86_64 Build/MASTER; wv)
* AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/44.0.2403.119 Mobile
* Safari/537.36"
*/
private static String mDefaultUserAgent;
public static String getDefaultUserAgent() {
if (mDefaultUserAgent == null) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
mDefaultUserAgent = WebSettings.getDefaultUserAgent(getContext());
} else {
mDefaultUserAgent = new WebView(getContext()).getSettings().getUserAgentString();
}
} catch (AndroidRuntimeException | NullPointerException e) {
// Catch AndroidRuntimeException that could be raised by the WebView() constructor.
// See https://github.com/wordpress-mobile/WordPress-Android/issues/3594
// Catch NullPointerException that could be raised by WebSettings.getDefaultUserAgent()
// See https://github.com/wordpress-mobile/WordPress-Android/issues/3838
// init with the empty string, it's a rare issue
mDefaultUserAgent = "";
}
}
return mDefaultUserAgent;
}
/**
* User-Agent string when making HTTP connections, for both API traffic and WebViews.
* Appends "wp-android/version" to WebView's default User-Agent string for the webservers
* to get the full feature list of the browser and serve content accordingly, e.g.:
* "Mozilla/5.0 (Linux; Android 6.0; Android SDK built for x86_64 Build/MASTER; wv)
* AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/44.0.2403.119 Mobile
* Safari/537.36 wp-android/4.7"
* Note that app versions prior to 2.7 simply used "wp-android" as the user agent
**/
private static final String USER_AGENT_APPNAME = "wp-android";
private static String mUserAgent;
public static String getUserAgent() {
if (mUserAgent == null) {
String defaultUserAgent = getDefaultUserAgent();
if (TextUtils.isEmpty(defaultUserAgent)) {
mUserAgent = USER_AGENT_APPNAME + "/" + PackageUtils.getVersionName(getContext());
} else {
mUserAgent = defaultUserAgent + " "+ USER_AGENT_APPNAME + "/"
+ PackageUtils.getVersionName(getContext());
}
}
return mUserAgent;
}
/*
* enable caching for HttpUrlConnection
* http://developer.android.com/training/efficient-downloads/redundant_redundant.html
*/
private static void enableHttpResponseCache(Context context) {
try {
long httpCacheSize = 5 * 1024 * 1024; // 5MB
File httpCacheDir = new File(context.getCacheDir(), "http");
HttpResponseCache.install(httpCacheDir, httpCacheSize);
} catch (IOException e) {
AppLog.w(T.UTILS, "Failed to enable http response cache");
}
}
private static void flushHttpCache() {
HttpResponseCache cache = HttpResponseCache.getInstalled();
if (cache != null) {
cache.flush();
}
}
private static void attemptToRestoreLastActiveBlog() {
if (setCurrentBlogToLastActive() == null) {
int blogId = WordPress.wpDB.getFirstVisibleBlogId();
if (blogId == 0) {
blogId = WordPress.wpDB.getFirstHiddenBlogId();
}
setCurrentBlogAndSetVisible(blogId);
wpDB.updateLastBlogId(blogId);
}
}
/**
* Gets a field from the project's BuildConfig using reflection. This is useful when flavors
* are used at the project level to set custom fields.
* based on: https://code.google.com/p/android/issues/detail?id=52962#c38
* @param application Used to find the correct file
* @param fieldName The name of the field-to-access
* @return The value of the field, or {@code null} if the field is not found.
*/
public static Object getBuildConfigValue(Application application, String fieldName) {
try {
String packageName = application.getClass().getPackage().getName();
Class<?> clazz = Class.forName(packageName + ".BuildConfig");
Field field = clazz.getField(fieldName);
return field.get(null);
} catch (Exception e) {
return null;
}
}
/**
* Detect when the app goes to the background and come back to the foreground.
*
* Turns out that when your app has no more visible UI, a callback is triggered.
* The callback, implemented in this custom class, is called ComponentCallbacks2 (yes, with a two).
*
* This class also uses ActivityLifecycleCallbacks and a timer used as guard,
* to make sure to detect the send to background event and not other events.
*
*/
private class ApplicationLifecycleMonitor implements Application.ActivityLifecycleCallbacks, ComponentCallbacks2 {
private final int DEFAULT_TIMEOUT = 2 * 60; // 2 minutes
private Date mLastPingDate;
private Date mApplicationOpenedDate;
boolean mFirstActivityResumed = true;
private Timer mActivityTransitionTimer;
private TimerTask mActivityTransitionTimerTask;
private final long MAX_ACTIVITY_TRANSITION_TIME_MS = 2000;
boolean mIsInBackground = true;
@Override
public void onConfigurationChanged(final Configuration newConfig) {
// Reapply locale on configuration change
WPActivityUtils.applyLocale(getContext());
}
@Override
public void onLowMemory() {
}
@Override
public void onTrimMemory(final int level) {
boolean evictBitmaps = false;
switch (level) {
case TRIM_MEMORY_COMPLETE:
case TRIM_MEMORY_MODERATE:
case TRIM_MEMORY_RUNNING_MODERATE:
case TRIM_MEMORY_RUNNING_CRITICAL:
case TRIM_MEMORY_RUNNING_LOW:
evictBitmaps = true;
break;
default:
break;
}
if (evictBitmaps && mBitmapCache != null) {
mBitmapCache.evictAll();
}
}
private boolean isPushNotificationPingNeeded() {
if (mLastPingDate == null) {
// first startup
return false;
}
Date now = new Date();
if (DateTimeUtils.secondsBetween(now, mLastPingDate) >= DEFAULT_TIMEOUT) {
mLastPingDate = now;
return true;
}
return false;
}
/**
* Check if user has valid credentials, and that at least 2 minutes are passed
* since the last ping, then try to update the PN token.
*/
private void updatePushNotificationTokenIfNotLimited() {
// Synch Push Notifications settings
if (isPushNotificationPingNeeded() && AccountHelper.isSignedInWordPressDotCom()) {
// Register for Cloud messaging
startService(new Intent(getContext(), GCMRegistrationIntentService.class));
}
}
/**
* The two methods below (startActivityTransitionTimer and stopActivityTransitionTimer)
* are used to track when the app goes to background.
*
* Our implementation uses `onActivityPaused` and `onActivityResumed` of ApplicationLifecycleMonitor
* to start and stop the timer that detects when the app goes to background.
*
* So when the user is simply navigating between the activities, the onActivityPaused() calls `startActivityTransitionTimer`
* and starts the timer, but almost immediately the new activity being entered, the ApplicationLifecycleMonitor cancels the timer
* in its onActivityResumed method, that in order calls `stopActivityTransitionTimer`.
* And so mIsInBackground would be false.
*
* In the case the app is sent to background, the TimerTask is instead executed, and the code that handles all the background logic is run.
*/
private void startActivityTransitionTimer() {
this.mActivityTransitionTimer = new Timer();
this.mActivityTransitionTimerTask = new TimerTask() {
public void run() {
AppLog.i(T.UTILS, "App goes to background");
// We're in the Background
mIsInBackground = true;
String lastActivityString = AppPrefs.getLastActivityStr();
ActivityId lastActivity = ActivityId.getActivityIdFromName(lastActivityString);
Map<String, Object> properties = new HashMap<String, Object>();
properties.put("last_visible_screen", lastActivity.toString());
if (mApplicationOpenedDate != null) {
Date now = new Date();
properties.put("time_in_app", DateTimeUtils.secondsBetween(now, mApplicationOpenedDate));
mApplicationOpenedDate = null;
}
AnalyticsTracker.track(AnalyticsTracker.Stat.APPLICATION_CLOSED, properties);
AnalyticsTracker.endSession(false);
ConnectionChangeReceiver.setEnabled(WordPress.this, false);
}
};
this.mActivityTransitionTimer.schedule(mActivityTransitionTimerTask,
MAX_ACTIVITY_TRANSITION_TIME_MS);
}
private void stopActivityTransitionTimer() {
if (this.mActivityTransitionTimerTask != null) {
this.mActivityTransitionTimerTask.cancel();
}
if (this.mActivityTransitionTimer != null) {
this.mActivityTransitionTimer.cancel();
}
mIsInBackground = false;
}
/**
* This method is called when:
* 1. the app starts (but it's not opened by a service or a broadcast receiver, i.e. an activity is resumed)
* 2. the app was in background and is now foreground
*/
private void onAppComesFromBackground() {
AppLog.i(T.UTILS, "App comes from background");
ConnectionChangeReceiver.setEnabled(WordPress.this, true);
AnalyticsUtils.refreshMetadata();
mApplicationOpenedDate = new Date();
AnalyticsTracker.track(AnalyticsTracker.Stat.APPLICATION_OPENED);
if (NetworkUtils.isNetworkAvailable(mContext)) {
// Rate limited PN Token Update
updatePushNotificationTokenIfNotLimited();
// Rate limited WPCom blog list Update
sUpdateWordPressComBlogList.runIfNotLimited();
// Rate limited blog options Update
sUpdateCurrentBlogOption.runIfNotLimited();
}
sDeleteExpiredStats.runIfNotLimited();
}
@Override
public void onActivityResumed(Activity activity) {
if (mIsInBackground) {
// was in background before
onAppComesFromBackground();
}
stopActivityTransitionTimer();
mIsInBackground = false;
if (mFirstActivityResumed) {
deferredInit(activity);
}
mFirstActivityResumed = false;
}
@Override
public void onActivityCreated(Activity arg0, Bundle arg1) {
}
@Override
public void onActivityDestroyed(Activity arg0) {
}
@Override
public void onActivityPaused(Activity arg0) {
mLastPingDate = new Date();
startActivityTransitionTimer();
}
@Override
public void onActivitySaveInstanceState(Activity arg0, Bundle arg1) {
}
@Override
public void onActivityStarted(Activity arg0) {
}
@Override
public void onActivityStopped(Activity arg0) {
}
}
}