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.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.SystemClock;
import android.support.multidex.MultiDexApplication;
import android.support.v7.app.AppCompatDelegate;
import android.text.TextUtils;
import android.util.AndroidRuntimeException;
import android.webkit.WebSettings;
import android.webkit.WebView;
import com.android.volley.RequestQueue;
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.wordpress.rest.RestClient;
import com.yarolegovich.wellsql.WellSql;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
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.NotificationsTable;
import org.wordpress.android.datasets.ReaderDatabase;
import org.wordpress.android.fluxc.Dispatcher;
import org.wordpress.android.fluxc.generated.AccountActionBuilder;
import org.wordpress.android.fluxc.generated.SiteActionBuilder;
import org.wordpress.android.fluxc.model.SiteModel;
import org.wordpress.android.fluxc.module.AppContextModule;
import org.wordpress.android.fluxc.persistence.SiteSqlUtils.DuplicateSiteException;
import org.wordpress.android.fluxc.persistence.WellSqlConfig;
import org.wordpress.android.fluxc.store.AccountStore;
import org.wordpress.android.fluxc.store.AccountStore.OnAccountChanged;
import org.wordpress.android.fluxc.store.PostStore;
import org.wordpress.android.fluxc.store.SiteStore;
import org.wordpress.android.fluxc.store.SiteStore.OnSiteChanged;
import org.wordpress.android.fluxc.store.SiteStore.SiteErrorType;
import org.wordpress.android.fluxc.tools.FluxCImageLoader;
import org.wordpress.android.fluxc.utils.ErrorUtils.OnUnexpectedError;
import org.wordpress.android.modules.AppComponent;
import org.wordpress.android.modules.DaggerAppComponent;
import org.wordpress.android.networking.ConnectionChangeReceiver;
import org.wordpress.android.networking.OAuthAuthenticator;
import org.wordpress.android.networking.RestClientUtils;
import org.wordpress.android.push.GCMRegistrationIntentService;
import org.wordpress.android.ui.ActivityId;
import org.wordpress.android.ui.notifications.NotificationsListFragment;
import org.wordpress.android.ui.notifications.services.NotificationsUpdateService;
import org.wordpress.android.ui.notifications.utils.NotificationsUtils;
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.AppLogListener;
import org.wordpress.android.util.AppLog.LogLevel;
import org.wordpress.android.util.AppLog.T;
import org.wordpress.android.util.BitmapLruCache;
import org.wordpress.android.util.CrashlyticsUtils;
import org.wordpress.android.util.DateTimeUtils;
import org.wordpress.android.util.FluxCUtils;
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.VolleyUtils;
import org.wordpress.android.util.WPActivityUtils;
import org.wordpress.android.util.WPLegacyMigrationUtils;
import org.wordpress.passcodelock.AbstractAppLock;
import org.wordpress.passcodelock.AppLockManager;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
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 javax.inject.Inject;
import javax.inject.Named;
import de.greenrobot.event.EventBus;
import io.fabric.sdk.android.Fabric;
public class WordPress extends MultiDexApplication {
public static final String SITE = "SITE";
public static String versionName;
public static WordPressDB wpDB;
private static RestClientUtils sRestClientUtils;
private static RestClientUtils sRestClientUtilsVersion1_1;
private static RestClientUtils sRestClientUtilsVersion1_2;
private static RestClientUtils sRestClientUtilsVersion1_3;
private static RestClientUtils sRestClientUtilsVersion0;
private static final int SECONDS_BETWEEN_SITE_UPDATE = 60 * 60; // 1 hour
private static final int SECONDS_BETWEEN_BLOGLIST_UPDATE = 15 * 60; // 15 minutes
private static final int SECONDS_BETWEEN_DELETE_STATS = 5 * 60; // 5 minutes
private static Context mContext;
private static BitmapLruCache mBitmapCache;
@Inject Dispatcher mDispatcher;
@Inject AccountStore mAccountStore;
@Inject SiteStore mSiteStore;
@Inject PostStore mPostStore;
@Inject @Named("custom-ssl") RequestQueue mRequestQueue;
public static RequestQueue sRequestQueue;
@Inject FluxCImageLoader mImageLoader;
public static FluxCImageLoader sImageLoader;
@Inject OAuthAuthenticator mOAuthAuthenticator;
public static OAuthAuthenticator sOAuthAuthenticator;
private AppComponent mAppComponent;
public AppComponent component() {
return mAppComponent;
}
// FluxC migration - drop the migration code after wpandroid 7.8
public static boolean sIsMigrationInProgress;
public static boolean sIsMigrationError;
private static MigrationListener sMigrationListener;
private int mRemainingSelfHostedSitesToFetch;
public interface MigrationListener {
void onError();
void onCompletion();
}
/**
* Update site list in a background task. (WPCOM site list, and eventually self hosted multisites)
*/
public RateLimitedTask mUpdateSiteList = new RateLimitedTask(SECONDS_BETWEEN_BLOGLIST_UPDATE) {
protected boolean run() {
if (mAccountStore.hasAccessToken()) {
mDispatcher.dispatch(SiteActionBuilder.newFetchSitesAction());
}
return true;
}
};
/**
* Update site infos in a background task.
*/
public RateLimitedTask mUpdateSelectedSite = new RateLimitedTask(SECONDS_BETWEEN_SITE_UPDATE) {
protected boolean run() {
int siteLocalId = AppPrefs.getSelectedSite();
SiteModel selectedSite = mSiteStore.getSiteByLocalId(siteLocalId);
if (selectedSite != null) {
mDispatcher.dispatch(SiteActionBuilder.newFetchSiteAction(selectedSite));
}
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;
}
};
/**
* Shutdown task used if migration to FluxC can't be performed due to lack of network connectivity.
*/
private static final Runnable sShutdown = new Runnable() {
@Override
public void run() {
System.exit(0);
}
};
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 / 4; //Use 1/4th of the available memory for this memory cache.
mBitmapCache = new BitmapLruCache(cacheSize);
}
return mBitmapCache;
}
@Override
public void onCreate() {
super.onCreate();
mContext = this;
long startDate = SystemClock.elapsedRealtime();
// Init WellSql
WellSql.init(new WellSqlConfig(getApplicationContext()));
// Init Dagger
mAppComponent = DaggerAppComponent.builder()
.appContextModule(new AppContextModule(getApplicationContext()))
.build();
component().inject(this);
mDispatcher.register(this);
// Init static fields from dagger injected singletons, for legacy Actions/Utils
sRequestQueue = mRequestQueue;
sImageLoader = mImageLoader;
sOAuthAuthenticator = mOAuthAuthenticator;
if (!PackageUtils.isDebugBuild()) {
Fabric.with(this, new Crashlytics());
}
ProfilingUtils.start("App Startup");
// Enable log recording
AppLog.enableRecording(true);
AppLog.addListener(new AppLogListener() {
@Override
public void onLog(T tag, LogLevel logLevel, String message) {
StringBuffer sb = new StringBuffer();
sb.append(logLevel.toString()).append("/").append(AppLog.TAG).append("-")
.append(tag.toString()).append(": ").append(message);
CrashlyticsUtils.log(sb.toString());
}
});
AppLog.i(T.UTILS, "WordPress.onCreate");
// If the migration was not done and if we have something to migrate
runFluxCMigration();
versionName = PackageUtils.getVersionName(this);
initWpDb();
enableHttpResponseCache(mContext);
// EventBus setup
EventBus.TAG = "WordPress-EVENT";
EventBus.builder()
.logNoSubscriberMessages(false)
.sendNoSubscriberEvent(false)
.throwSubscriberException(true)
.installDefaultEventBus();
RestClientUtils.setUserAgent(getUserAgent());
// PasscodeLock setup
if(!AppLockManager.getInstance().isAppLockFeatureEnabled()) {
// Make sure that PasscodeLock isn't already in place.
// Notifications services can enable it before the app is started.
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());
// Allows vector drawable from resources (in selectors for instance) on Android < 21 (can cause issues
// with memory usage and the use of Configuration). More informations:
// https://developer.android.com/reference/android/support/v7/app/AppCompatDelegate.html#setCompatVectorFromResourcesEnabled(boolean)
// Note: if removed, this will cause crashes on Android < 21
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
private void runFluxCMigration() {
// If the migration was not done and if we have something to migrate
if ((!AppPrefs.wasAccessTokenMigrated() || !AppPrefs.wereSelfHostedSitesMigratedToFluxC()
|| !AppPrefs.wereDraftsMigratedToFluxC())
&& (WPLegacyMigrationUtils.hasSelfHostedSiteToMigrate(this)
|| WPLegacyMigrationUtils.getLatestDeprecatedAccessToken(this) != null
|| WPLegacyMigrationUtils.hasDraftsToMigrate(this))) {
sIsMigrationInProgress = true;
// No connection? Then exit and ask the user to come back.
if (!NetworkUtils.isNetworkAvailable(this)) {
AppLog.i(T.DB, "No connection - aborting migration");
sIsMigrationError = true;
new Handler().postDelayed(sShutdown, 3500);
return;
}
migrateAccessToken();
}
}
private void migrateAccessToken() {
// Migrate access token AccountStore
if (!AppPrefs.wasAccessTokenMigrated() && !mAccountStore.hasAccessToken()) {
AppLog.i(T.DB, "No access token found in FluxC - attempting to migrate existing one");
// It will take some time to update the access token in the AccountStore if it was migrated
// so it will be set to the migrated token
String migratedToken = WPLegacyMigrationUtils.migrateAccessTokenToAccountStore(this, mDispatcher);
if (!TextUtils.isEmpty(migratedToken)) {
AppLog.i(T.DB, "Access token successfully migrated to FluxC - fetching accounts and sites");
AppPrefs.setAccessTokenMigrated(true);
mDispatcher.dispatch(AccountActionBuilder.newFetchAccountAction());
mDispatcher.dispatch(AccountActionBuilder.newFetchSettingsAction());
mDispatcher.dispatch(SiteActionBuilder.newFetchSitesAction());
return;
}
// Even if there was no token to migrate, turn this flag on so we don't attempt to migrate again
AppPrefs.setAccessTokenMigrated(true);
}
migrateSelfHostedSites();
}
private void migrateSelfHostedSites() {
if (!AppPrefs.wereSelfHostedSitesMigratedToFluxC()) {
List<SiteModel> siteList = WPLegacyMigrationUtils.migrateSelfHostedSitesFromDeprecatedDB(this, mDispatcher);
if (siteList != null && !siteList.isEmpty()) {
AppLog.i(T.DB, "Finished migrating " + siteList.size() + " self-hosted sites - fetching site info");
AppPrefs.setSelfHostedSitesMigratedToFluxC(true);
mRemainingSelfHostedSitesToFetch = siteList.size();
for (SiteModel siteModel : siteList) {
mDispatcher.dispatch(SiteActionBuilder.newFetchSiteAction(siteModel));
}
return;
} else {
AppLog.i(T.DB, "No self-hosted sites to migrate");
AppPrefs.setSelfHostedSitesMigratedToFluxC(true);
}
} else {
AppLog.i(T.DB, "Self-hosted sites have already been migrated");
}
migrateDrafts();
}
private void migrateDrafts() {
// Migrate drafts to FluxC
if (!AppPrefs.wereDraftsMigratedToFluxC()) {
WPLegacyMigrationUtils.migrateDraftsFromDeprecatedDB(this, mDispatcher, mSiteStore);
AppPrefs.setDraftsMigratedToFluxC(true);
}
AppLog.i(T.DB, "Migration complete!");
endMigration();
}
private void endMigration() {
AppLog.i(T.DB, "Ending migration to FluxC");
sIsMigrationInProgress = false;
if (sMigrationListener != null) {
sMigrationListener.onCompletion();
sMigrationListener = null;
}
}
public static void registerMigrationListener(MigrationListener listener) {
sMigrationListener = listener;
if (sIsMigrationError) {
sMigrationListener.onError();
}
}
private void initAnalytics(final long elapsedTimeOnCreate) {
AnalyticsTracker.registerTracker(new AnalyticsTrackerMixpanel(getContext(), BuildConfig.MIXPANEL_TOKEN));
AnalyticsTracker.registerTracker(new AnalyticsTrackerNosara(getContext()));
AnalyticsTracker.init(getContext());
AnalyticsUtils.refreshMetadata(mAccountStore, mSiteStore);
// 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.setNewEditorPromoRequired(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));
}
// Refresh account informations
if (mAccountStore.hasAccessToken()) {
if (!sIsMigrationInProgress) {
mDispatcher.dispatch(AccountActionBuilder.newFetchAccountAction());
mDispatcher.dispatch(AccountActionBuilder.newFetchSettingsAction());
}
NotificationsUpdateService.startService(getContext());
}
}
private void initWpDb() {
if (!createAndVerifyWpDb()) {
AppLog.e(T.DB, "Invalid database, sign out user and delete database");
// Force DB deletion
WordPressDB.deleteDatabase(this);
wpDB = new WordPressDB(this);
}
}
private boolean createAndVerifyWpDb() {
try {
wpDB = new WordPressDB(this);
return true;
} catch (RuntimeException e) {
AppLog.e(T.DB, e);
return false;
}
}
public static Context getContext() {
return mContext;
}
public static RestClientUtils getRestClientUtils() {
if (sRestClientUtils == null) {
sRestClientUtils = new RestClientUtils(mContext, sRequestQueue, sOAuthAuthenticator, null);
}
return sRestClientUtils;
}
public static RestClientUtils getRestClientUtilsV1_1() {
if (sRestClientUtilsVersion1_1 == null) {
sRestClientUtilsVersion1_1 = new RestClientUtils(mContext, sRequestQueue, sOAuthAuthenticator,
null, RestClient.REST_CLIENT_VERSIONS.V1_1);
}
return sRestClientUtilsVersion1_1;
}
public static RestClientUtils getRestClientUtilsV1_2() {
if (sRestClientUtilsVersion1_2 == null) {
sRestClientUtilsVersion1_2 = new RestClientUtils(mContext, sRequestQueue, sOAuthAuthenticator,
null, RestClient.REST_CLIENT_VERSIONS.V1_2);
}
return sRestClientUtilsVersion1_2;
}
public static RestClientUtils getRestClientUtilsV1_3() {
if (sRestClientUtilsVersion1_3 == null) {
sRestClientUtilsVersion1_3 = new RestClientUtils(mContext, sRequestQueue, sOAuthAuthenticator,
null, RestClient.REST_CLIENT_VERSIONS.V1_3);
}
return sRestClientUtilsVersion1_3;
}
public static RestClientUtils getRestClientUtilsV0() {
if (sRestClientUtilsVersion0 == null) {
sRestClientUtilsVersion0 = new RestClientUtils(mContext, sRequestQueue, sOAuthAuthenticator,
null, RestClient.REST_CLIENT_VERSIONS.V0);
}
return sRestClientUtilsVersion0;
}
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;
}
/**
* Sign out from wpcom account.
* Note: This method must not be called on UI Thread.
*/
public void wordPressComSignOut() {
// Keep the analytics tracking at the beginning, before the account data is actual removed.
AnalyticsTracker.track(Stat.ACCOUNT_LOGOUT);
removeWpComUserRelatedData(getApplicationContext());
}
@SuppressWarnings("unused")
@Subscribe(threadMode = ThreadMode.MAIN)
public void onAccountChanged(OnAccountChanged event) {
if (!FluxCUtils.isSignedInWPComOrHasWPOrgSite(mAccountStore, mSiteStore)) {
flushHttpCache();
// Analytics resets
AnalyticsTracker.endSession(false);
AnalyticsTracker.clearAllData();
// disable passcode lock
AbstractAppLock appLock = AppLockManager.getInstance().getAppLock();
if (appLock != null) {
appLock.setPassword(null);
}
}
}
@SuppressWarnings("unused")
@Subscribe(threadMode = ThreadMode.MAIN)
public void onSiteChanged(OnSiteChanged event) {
if (event.isError() && event.error.type == SiteErrorType.DUPLICATE_SITE) {
CrashlyticsUtils.logException(new DuplicateSiteException(), T.MAIN, "Duplicate site detected");
}
if (!sIsMigrationInProgress) {
return;
}
if (mRemainingSelfHostedSitesToFetch == 0) {
// Token has been migrated, and any WP.com sites have been fetched
// Attempt to migrate self-hosted sites
AppLog.i(T.DB, "Access token migrated and WP.com sites fetched - attempting to migrate self-hosted sites");
migrateSelfHostedSites();
} else if (mRemainingSelfHostedSitesToFetch > 1) {
mRemainingSelfHostedSitesToFetch--;
AppLog.i(T.DB, "Self-hosted sites remaining to fetch for migration: " + mRemainingSelfHostedSitesToFetch);
} else {
AppLog.i(T.DB, "The last self-hosted site has been fetched - starting draft migration");
migrateDrafts();
}
}
@SuppressWarnings("unused")
@Subscribe(threadMode = ThreadMode.MAIN)
public void onParseError(OnUnexpectedError event) {
AppLog.d(T.API, "Receiving OnUnexpectedError event, message: " + event.exception.getMessage());
String description = "FluxC: " + event.description;
if (event.extras != null) {
for (String key : event.extras.keySet()) {
CrashlyticsUtils.setString(key, event.extras.get(key));
}
}
CrashlyticsUtils.logException(event.exception, event.type, description);
}
public void removeWpComUserRelatedData(Context context) {
// cancel all Volley requests - do this before unregistering push since that uses
// a Volley request
VolleyUtils.cancelAllRequests(sRequestQueue);
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);
}
// reset default account
mDispatcher.dispatch(AccountActionBuilder.newSignOutAction());
// delete wpcom and jetpack sites
mDispatcher.dispatch(SiteActionBuilder.newRemoveWpcomAndJetpackSitesAction());
// reset all reader-related prefs & data
AppPrefs.reset();
ReaderDatabase.reset();
// Reset Stats Data
StatsDatabaseHelper.getDatabase(context).reset();
StatsWidgetProvider.refreshAllWidgets(context, mSiteStore);
// Reset Notifications Data
NotificationsTable.reset();
}
/**
* 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();
}
}
/**
* 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() && mAccountStore.hasAccessToken()) {
// 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(Activity activity) {
AppLog.i(T.UTILS, "App comes from background");
ConnectionChangeReceiver.setEnabled(WordPress.this, true);
AnalyticsUtils.refreshMetadata(mAccountStore, mSiteStore);
mApplicationOpenedDate = new Date();
Map<String, Boolean> properties = new HashMap<>(1);
properties.put("pin_lock_enabled", AppLockManager.getInstance().getAppLock() != null
&& AppLockManager.getInstance().getAppLock().isPasswordLocked());
AnalyticsTracker.track(Stat.APPLICATION_OPENED, properties);
if (NetworkUtils.isNetworkAvailable(mContext)) {
// Refresh account informations and Notifications
if (mAccountStore.hasAccessToken()) {
Intent intent = activity.getIntent();
if (intent != null && intent.hasExtra(NotificationsListFragment.NOTE_ID_EXTRA)) {
NotificationsUpdateService.startService(getContext(),
getNoteIdFromNoteDetailActivityIntent(activity.getIntent()));
} else {
NotificationsUpdateService.startService(getContext());
}
}
// Rate limited PN Token Update
updatePushNotificationTokenIfNotLimited();
// Don't update sites or delete expired stats if migration is in progress
if (sIsMigrationInProgress) {
return;
}
// Rate limited WPCom blog list update
mUpdateSiteList.runIfNotLimited();
// Rate limited Site informations and options update
mUpdateSelectedSite.runIfNotLimited();
}
sDeleteExpiredStats.runIfNotLimited();
}
// gets the note id from the extras that started this activity, so
// we can remember to re-set that to unread once the note fetch update takes place
private String getNoteIdFromNoteDetailActivityIntent(Intent intent) {
String noteId = "";
if (intent != null) {
noteId = intent.getStringExtra(NotificationsListFragment.NOTE_ID_EXTRA);
}
return noteId;
}
@Override
public void onActivityResumed(Activity activity) {
if (mIsInBackground) {
// was in background before
onAppComesFromBackground(activity);
}
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) {
}
}
}