package kr.kdev.dg1s.biowiki; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.app.Application; import android.content.ComponentCallbacks2; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Configuration; import android.database.sqlite.SQLiteException; import android.os.Build; import android.os.Bundle; import android.os.StrictMode; import android.preference.PreferenceManager; import com.android.volley.RequestQueue; import com.android.volley.VolleyLog; import com.android.volley.toolbox.ImageLoader; import com.android.volley.toolbox.Volley; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import org.wordpress.passcodelock.AppLockManager; import java.lang.reflect.Type; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; import kr.kdev.dg1s.biowiki.models.Blog; import kr.kdev.dg1s.biowiki.models.Post; import kr.kdev.dg1s.biowiki.networking.OAuthAuthenticator; import kr.kdev.dg1s.biowiki.networking.OAuthAuthenticatorFactory; import kr.kdev.dg1s.biowiki.networking.RestClientUtils; import kr.kdev.dg1s.biowiki.util.AppLog; import kr.kdev.dg1s.biowiki.util.BitmapLruCache; import kr.kdev.dg1s.biowiki.util.VolleyUtils; public class BioWiki extends Application { public static final String ACCESS_TOKEN_PREFERENCE = "wp_pref_wpcom_access_token"; public static final String WPCOM_USERNAME_PREFERENCE = "wp_pref_wpcom_username"; public static final String WPCOM_PASSWORD_PREFERENCE = "wp_pref_wpcom_password"; public static final String TAG = "BioWiki"; public static final String BROADCAST_ACTION_SIGNOUT = "wp-signout"; public static final String BROADCAST_ACTION_XMLRPC_INVALID_CREDENTIALS = "XMLRPC_INVALID_CREDENTIALS"; public static final String BROADCAST_ACTION_XMLRPC_INVALID_SSL_CERTIFICATE = "INVALID_SSL_CERTIFICATE"; public static final String BROADCAST_ACTION_XMLRPC_TWO_FA_AUTH = "TWO_FA_AUTH"; public static final String BROADCAST_ACTION_XMLRPC_LOGIN_LIMIT = "LOGIN_LIMIT"; /** * User-Agent string when making HTTP connections, for both API traffic and WebViews. * Follows the format detailed at http://tools.ietf.org/html/rfc2616#section-14.43, * ie: "AppName/AppVersion (OS Version; Locale; Device)" * "wp-android/2.6.4 (Android 4.3; en_US; samsung GT-I9505/jfltezh)" * "wp-android/2.6.3 (Android 4.4.2; en_US; LGE Nexus 5/hammerhead)" * 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"; public static String versionName; public static Blog currentBlog; public static Post currentPost; public static BioWikiDB wpDB; //public static BioWikiStatsDB wpStatsDB; public static OnPostUploadedListener onPostUploadedListener = null; public static boolean postsShouldRefresh; public static boolean shouldRestoreSelectedActivityBIOWIKI; public static boolean shouldRestoreSelectedActivityBIOINFO; public static RestClientUtils mRestClientUtils; public static RequestQueue requestQueue; public static ImageLoader imageLoader; private static Context mContext; private static BitmapLruCache mBitmapCache; private static String mUserAgent; 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; } public static void setupVolleyQueue() { requestQueue = Volley.newRequestQueue(mContext, VolleyUtils.getHTTPClientStack(mContext)); imageLoader = new ImageLoader(requestQueue, getBitmapCache()); VolleyLog.setTag(TAG); imageLoader.setBatchedResponseDelay(0); } public static Context getContext() { return mContext; } public static RestClientUtils getRestClientUtils() { if (mRestClientUtils == null) { OAuthAuthenticator authenticator = OAuthAuthenticatorFactory.instantiate(); mRestClientUtils = new RestClientUtils(requestQueue, authenticator); } return mRestClientUtils; } /* * enables "strict mode" for testing - should NEVER be used in release builds */ @SuppressLint("NewApi") private static void enableStrictMode() { // strict mode requires API level 9 or later if (Build.VERSION.SDK_INT < 9) return; StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectDiskReads() .detectDiskWrites() .detectNetwork() .penaltyLog() .penaltyFlashScreen() .build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectActivityLeaks() .detectLeakedSqlLiteObjects() .detectLeakedClosableObjects() .detectLeakedRegistrationObjects() .penaltyLog() .build()); AppLog.w(AppLog.T.UTILS, "Strict mode enabled"); } public static String getVersionName(Context context) { PackageManager pm = context.getPackageManager(); try { PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0); return pi.versionName == null ? "" : pi.versionName; } catch (NameNotFoundException e) { return ""; } } public static void setOnPostUploadedListener(OnPostUploadedListener listener) { onPostUploadedListener = listener; } public static void postUploaded(String postId) { if (onPostUploadedListener != null) { try { onPostUploadedListener.OnPostUploaded(postId); } catch (Exception e) { postsShouldRefresh = true; } } else { postsShouldRefresh = true; } } /** * 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, just * select the first one. */ public static Blog getCurrentBlog() { if (currentBlog == null || !wpDB.isDotComAccountVisible(currentBlog.getRemoteBlogId())) { // attempt to restore the last active blog setCurrentBlogToLastActive(); // fallback to just using the first blog List<Map<String, Object>> accounts = BioWiki.wpDB.getVisibleAccounts(); if (currentBlog == null && accounts.size() > 0) { int id = Integer.valueOf(accounts.get(0).get("id").toString()); setCurrentBlog(id); wpDB.updateLastBlogId(id); } } 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 { Blog blog = wpDB.instantiateBlogByLocalId(id); return blog; } 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 = BioWiki.wpDB.getVisibleAccounts(); int lastBlogId = BioWiki.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 * @return the current blog */ public static Blog setCurrentBlog(int id) { currentBlog = wpDB.instantiateBlogByLocalId(id); return currentBlog; } /* * returns the blogID of the current blog */ public static int getCurrentRemoteBlogId() { return (getCurrentBlog() != null ? getCurrentBlog().getRemoteBlogId() : -1); } public static int getCurrentLocalTableBlogId() { return (getCurrentBlog() != null ? getCurrentBlog().getLocalTableBlogId() : -1); } /** * Checks for WordPress.com credentials * * @return true if we have credentials or false if not */ /* public static boolean hasValidWPComCredentials(Context context) { SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); String username = settings.getString(WPCOM_USERNAME_PREFERENCE, null); String password = settings.getString(WPCOM_PASSWORD_PREFERENCE, null); return username != null && password != null; } */ public static boolean isSignedIn(Context context) { /* if (BioWiki.hasValidWPComCredentials(context)) { return true; } */ return BioWiki.wpDB.getNumVisibleAccounts() != 0; } /** * Sign out from all accounts by clearing out the password, which will require user to sign in * again */ public static void signOut(Context context) { /* try { SelfSignedSSLCertsManager.getInstance(context).emptyLocalKeyStoreFile(); } catch (GeneralSecurityException e) { AppLog.e(AppLog.T.UTILS, "Error while cleaning the Local KeyStore File", e); } catch (IOException e) { AppLog.e(AppLog.T.UTILS, "Error while cleaning the Local KeyStore File", e); } */ wpDB.deleteAllAccounts(); wpDB.updateLastBlogId(-1); currentBlog = null; // send broadcast that user is signing out - this is received by BWActionBarActivity // descendants Intent broadcastIntent = new Intent(); broadcastIntent.setAction(BROADCAST_ACTION_SIGNOUT); context.sendBroadcast(broadcastIntent); } 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; } /** * Returns WordPress.com Auth Token * * @return String - The wpcom Auth token, or null if not authenticated. */ /* public static String getWPComAuthToken(Context context) { SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); return settings.getString(BioWiki.ACCESS_TOKEN_PREFERENCE, null); } */ public static String getUserAgent() { if (mUserAgent == null) { PackageInfo pkgInfo; try { String pkgName = getContext().getApplicationInfo().packageName; pkgInfo = getContext().getPackageManager().getPackageInfo(pkgName, 0); } catch (PackageManager.NameNotFoundException e) { return USER_AGENT_APPNAME; } mUserAgent = USER_AGENT_APPNAME + "/" + pkgInfo.versionName + " (Android " + Build.VERSION.RELEASE + "; " + Locale.getDefault().toString() + "; " + Build.MANUFACTURER + " " + Build.MODEL + "/" + Build.PRODUCT + ")"; } return mUserAgent; } /* public static void removeWpComUserRelatedData(Context context) { // cancel all Volley requests - do this before unregistering push since that uses // a Volley request VolleyUtils.cancelAllRequests(requestQueue); NotificationUtils.unregisterDevicePushNotifications(context); try { GCMRegistrar.checkDevice(context); GCMRegistrar.unregister(context); } catch (Exception e) { AppLog.v(AppLog.T.NOTIFS, "Could not unregister for GCM: " + e.getMessage()); } SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); editor.remove(BioWiki.WPCOM_USERNAME_PREFERENCE); editor.remove(BioWiki.WPCOM_PASSWORD_PREFERENCE); editor.remove(BioWiki.ACCESS_TOKEN_PREFERENCE); editor.commit(); // reset all reader-related prefs & data UserPrefs.reset(); ReaderDatabase.reset(); // send broadcast that user is signing out - this is received by BWActionBarActivity // descendants Intent broadcastIntent = new Intent(); broadcastIntent.setAction(BROADCAST_ACTION_SIGNOUT); context.sendBroadcast(broadcastIntent); } */ @Override public void onCreate() { // Enable log recording AppLog.enableRecording(true); versionName = getVersionName(this); initWpDb(); //wpStatsDB = new BioWikiStatsDB(this); mContext = this; // Volley networking setup setupVolleyQueue(); SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); if (settings.getInt("bw_pref_last_activity", -1) >= 0) { shouldRestoreSelectedActivityBIOWIKI = true; } if (settings.getInt("bi_pref_last_activity", -1) >= 0) { shouldRestoreSelectedActivityBIOINFO = true; } //registerForCloudMessaging(this); // Uncomment this line if you want to test the app locking feature AppLockManager.getInstance().enableDefaultAppLockIfAvailable(this); if (AppLockManager.getInstance().isAppLockFeatureEnabled()) { AppLockManager.getInstance().getCurrentAppLock().setDisabledActivities( new String[]{"kr.kdev.dg1s.biowiki.ui.ShareIntentReceiverActivity"}); } /* BWMobileStatsUtil.initialize(); BWMobileStatsUtil.trackEventForSelfHostedAndWPCom(BWMobileStatsUtil.StatsEventAppOpened); */ super.onCreate(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { PushNotificationsBackendMonitor pnBackendMponitor = new PushNotificationsBackendMonitor(); registerComponentCallbacks(pnBackendMponitor); registerActivityLifecycleCallbacks(pnBackendMponitor); } } private void initWpDb() { if (!createAndVerifyWpDb()) { AppLog.e(AppLog.T.DB, "Invalid database, sign out user and delete database"); SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit(); currentBlog = null; /* editor.remove(BioWiki.WPCOM_USERNAME_PREFERENCE); editor.remove(BioWiki.WPCOM_PASSWORD_PREFERENCE); editor.remove(BioWiki.ACCESS_TOKEN_PREFERENCE); */ editor.commit(); if (wpDB != null) { wpDB.updateLastBlogId(-1); wpDB.deleteDatabase(this); } wpDB = new BioWikiDB(this); } } private boolean createAndVerifyWpDb() { try { wpDB = new BioWikiDB(this); // verify account data List<Map<String, Object>> accounts = wpDB.getAllAccounts(); for (Map<String, Object> account : accounts) { if (account == null || account.get("blogName") == null || account.get("url") == null) { return false; } } return true; } catch (SQLiteException sqle) { AppLog.e(AppLog.T.DB, sqle); return false; } catch (RuntimeException re) { AppLog.e(AppLog.T.DB, re); return false; } } /* public static void registerForCloudMessaging(Context ctx) { if (BioWiki.hasValidWPComCredentials(ctx)) { String token = null; try { // Register for Google Cloud Messaging GCMRegistrar.checkDevice(ctx); GCMRegistrar.checkManifest(ctx); token = GCMRegistrar.getRegistrationId(ctx); String gcmId = Config.GCM_ID; if (gcmId != null && token.equals("")) { GCMRegistrar.register(ctx, gcmId); } else { // Send the token to WP.com in case it was invalidated NotificationUtils.registerDeviceForPushNotifications(ctx, token); AppLog.v(AppLog.T.NOTIFS, "Already registered for GCM"); } } catch (Exception e) { AppLog.e(AppLog.T.NOTIFS, "Could not register for GCM: " + e.getMessage()); } } } */ public interface OnPostUploadedListener { public abstract void OnPostUploaded(String postId); } /* * 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 callback is only available in API Level 14 (Ice Cream Sandwich) and above. * * 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. * */ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) private class PushNotificationsBackendMonitor implements Application.ActivityLifecycleCallbacks, ComponentCallbacks2 { private final int DEFAULT_TIMEOUT = 2 * 60; //2 minutes boolean background = false; private Date lastPingDate; @Override public void onConfigurationChanged(final Configuration newConfig) { } @Override public void onLowMemory() { } @Override public void onTrimMemory(final int level) { if (level == ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) { // We're in the Background background = true; } else { background = false; } //Levels that we need to consider are TRIM_MEMORY_RUNNING_CRITICAL = 15; - TRIM_MEMORY_RUNNING_LOW = 10; - TRIM_MEMORY_RUNNING_MODERATE = 5; if (level < ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN && mBitmapCache != null) { mBitmapCache.evictAll(); } } /* private boolean mustPingPushNotificationsBackend() { if (BioWiki.hasValidWPComCredentials(mContext) == false) return false; if (background == false) return false; background = false; if (lastPingDate == null) return false; //first startup Date now = new Date(); long nowInMilliseconds = now.getTime(); long lastPingDateInMilliseconds = lastPingDate.getTime(); int secondsPassed = (int) (nowInMilliseconds - lastPingDateInMilliseconds)/(1000); if (secondsPassed >= DEFAULT_TIMEOUT) { lastPingDate = now; return true; } return false; } */ @Override public void onActivityResumed(Activity arg0) { /* if(mustPingPushNotificationsBackend()) { //uhhh ohhh! if (BioWiki.hasValidWPComCredentials(mContext)) { String token = null; try { // Register for Google Cloud Messaging GCMRegistrar.checkDevice(mContext); GCMRegistrar.checkManifest(mContext); token = GCMRegistrar.getRegistrationId(mContext); String gcmId = Config.GCM_ID; if (gcmId == null || token == null || token.equals("") ) { AppLog.e(AppLog.T.NOTIFS, "Could not ping the PNs backend, Token or gmcID not found"); return; } else { // Send the token to WP.com NotificationUtils.registerDeviceForPushNotifications(mContext, token); } } catch (Exception e) { AppLog.e(AppLog.T.NOTIFS, "Could not ping the PNs backend: " + e.getMessage()); } } } */ } @Override public void onActivityCreated(Activity arg0, Bundle arg1) { } @Override public void onActivityDestroyed(Activity arg0) { } @Override public void onActivityPaused(Activity arg0) { lastPingDate = new Date(); } @Override public void onActivitySaveInstanceState(Activity arg0, Bundle arg1) { } @Override public void onActivityStarted(Activity arg0) { } @Override public void onActivityStopped(Activity arg0) { } } }