package me.ccrama.redditslide; import android.app.Activity; import android.app.Application; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Typeface; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.support.multidex.MultiDexApplication; import android.text.Html; import android.text.Spannable; import android.text.SpannableString; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.util.DisplayMetrics; import android.util.Log; import android.widget.Toast; import com.afollestad.materialdialogs.AlertDialogWrapper; import com.afollestad.materialdialogs.MaterialDialog; import com.jakewharton.processphoenix.ProcessPhoenix; import com.lusfold.androidkeyvaluestore.KVStore; import com.nostra13.universalimageloader.core.ImageLoader; import net.dean.jraw.http.NetworkException; import net.dean.jraw.paginators.Sorting; import net.dean.jraw.paginators.SubmissionSearchPaginator; import net.dean.jraw.paginators.TimePeriod; import org.apache.commons.lang3.StringEscapeUtils; import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; import java.lang.ref.WeakReference; import java.net.Inet4Address; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import me.ccrama.redditslide.Activities.MainActivity; import me.ccrama.redditslide.Activities.Search; import me.ccrama.redditslide.Autocache.AutoCacheScheduler; import me.ccrama.redditslide.ImgurAlbum.AlbumUtils; import me.ccrama.redditslide.Notifications.NotificationJobScheduler; import me.ccrama.redditslide.Tumblr.TumblrUtils; import me.ccrama.redditslide.util.AdBlocker; import me.ccrama.redditslide.util.GifCache; import me.ccrama.redditslide.util.IabHelper; import me.ccrama.redditslide.util.IabResult; import me.ccrama.redditslide.util.LinkUtil; import me.ccrama.redditslide.util.LogUtil; import me.ccrama.redditslide.util.NetworkUtil; import me.ccrama.redditslide.util.UpgradeUtil; import okhttp3.Dns; import okhttp3.OkHttpClient; /** * Created by ccrama on 9/17/2015. */ public class Reddit extends MultiDexApplication implements Application.ActivityLifecycleCallbacks { public static final String EMPTY_STRING = "NOTHING"; public static final long enter_animation_time_original = 600; public static final String PREF_LAYOUT = "PRESET"; public static final String SHARED_PREF_IS_MOD = "is_mod"; public static final String SHARED_PREF_IS_OVER_18 = "is_over_18"; public static IabHelper mHelper; public static SubmissionSearchPaginator.SearchSort search = SubmissionSearchPaginator.SearchSort.RELEVANCE; public static long enter_animation_time = enter_animation_time_original; public static final int enter_animation_time_multiplier = 1; public static Authentication authentication; public static Sorting defaultSorting; public static TimePeriod timePeriod; public static SharedPreferences colors; public static SharedPreferences appRestart; public static SharedPreferences tags; public static int dpWidth; public static int notificationTime; public static boolean videoPlugin; public static NotificationJobScheduler notifications; public static boolean isLoading = false; public static final long time = System.currentTimeMillis(); public static boolean fabClear; public static ArrayList<Integer> lastposition; public static int currentPosition; public static SharedPreferences cachedData; public static final boolean noGapps = true; //for testing public static boolean over18 = true; public static boolean overrideLanguage; public static boolean isRestarting; public static AutoCacheScheduler autoCache; public static boolean peek; private final List<Listener> listeners = new ArrayList<>(); public boolean active; private ImageLoader defaultImageLoader; public static OkHttpClient client; public static void forceRestart(Context context) { if (appRestart.contains("back")) { appRestart.edit().remove("back").apply(); } appRestart.edit().putBoolean("isRestarting", true).apply(); isRestarting = true; ProcessPhoenix.triggerRebirth(context, new Intent(context, MainActivity.class)); } public static void forceRestart(Context c, boolean forceLoadScreen) { appRestart.edit().putString("startScreen", "").apply(); appRestart.edit().putBoolean("isRestarting", true).apply(); forceRestart(c); } /** * Converts px to dp * * @param px to convert to dp * @param c context of view * @return dp */ public static int pxToDp(int px, Context c) { final DisplayMetrics displayMetrics = c.getResources().getDisplayMetrics(); return Math.round(px / (displayMetrics.ydpi / DisplayMetrics.DENSITY_DEFAULT)); } /** * Converts dp to px, uses vertical density * * @param dp to convert to px * @return px */ public static int dpToPxVertical(int dp) { final DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics(); return Math.round(dp * (displayMetrics.ydpi / DisplayMetrics.DENSITY_DEFAULT)); } /** * Converts dp to px, uses horizontal density * * @param dp to convert to px * @return px */ public static int dpToPxHorizontal(int dp) { final DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics(); return Math.round(dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT)); } public static void defaultShareText(String title, String url, Context c) { url = StringEscapeUtils.unescapeHtml4(Html.fromHtml(url).toString()); Intent sharingIntent = new Intent(Intent.ACTION_SEND); sharingIntent.setType("text/plain"); /* Decode html entities */ title = StringEscapeUtils.unescapeHtml4(title); sharingIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, title); sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, url); c.startActivity(Intent.createChooser(sharingIntent, c.getString(R.string.title_share))); } public static void defaultShare(String url, Context c) { url = StringEscapeUtils.unescapeHtml4(Html.fromHtml(url).toString()); Uri webpage = LinkUtil.formatURL(url); Intent intent = new Intent(Intent.ACTION_VIEW, webpage); if (intent.resolveActivity(c.getPackageManager()) != null) { c.startActivity(intent); } } public static boolean isPackageInstalled(final Context ctx, String s) { try { final PackageManager pm = ctx.getPackageManager(); final PackageInfo pi = pm.getPackageInfo(s, 0); if (pi != null && pi.applicationInfo.enabled) return true; } catch (final Throwable ignored) { } return false; } private static boolean isPackageInstalled(final Context ctx) { try { final PackageManager pm = ctx.getPackageManager(); final PackageInfo pi = pm.getPackageInfo("me.ccrama.slideforreddittabletuiunlock", 0); if (pi != null && pi.applicationInfo.enabled) return true; } catch (final Throwable ignored) { } return false; } private static boolean isVideoPluginInstalled(final Context ctx) { try { final PackageManager pm = ctx.getPackageManager(); final PackageInfo pi = pm.getPackageInfo("ccrama.me.slideyoutubeplugin", 0); if (pi != null && pi.applicationInfo.enabled) return true; } catch (final Throwable ignored) { } return false; } public static String arrayToString(ArrayList<String> array) { if (array != null) { StringBuilder b = new StringBuilder(); for (String s : array) { b.append(s).append(","); } String f = b.toString(); if (f.length() > 0) { f = f.substring(0, f.length() - 1); } return f; } else { return ""; } } public static String arrayToString(ArrayList<String> array, String separator) { if (array != null) { StringBuilder b = new StringBuilder(); for (String s : array) { b.append(s).append(separator); } String f = b.toString(); if (f.length() > 0) { f = f.substring(0, f.length() - separator.length()); } return f; } else { return ""; } } public static ArrayList<String> stringToArray(String string) { ArrayList<String> f = new ArrayList<>(); Collections.addAll(f, string.split(",")); return f; } public static Integer getSortingId(String subreddit) { subreddit = subreddit.toLowerCase(); Sorting sort = sorting.containsKey(subreddit) ? sorting.get(subreddit) : Reddit.defaultSorting; return getSortingId(sort); } public static Integer getSortingId(Sorting sort) { switch (sort) { case HOT: return 0; case NEW: return 1; case RISING: return 2; case TOP: return 3; case CONTROVERSIAL: return 4; default: return 0; } } public static Integer getSortingIdTime(String subreddit) { subreddit = subreddit.toLowerCase(); TimePeriod time = times.containsKey(subreddit) ? times.get(subreddit) : Reddit.timePeriod; return getSortingIdTime(time); } public static Integer getSortingIdTime(TimePeriod time) { switch (time) { case HOUR: return 0; case DAY: return 1; case WEEK: return 2; case MONTH: return 3; case YEAR: return 4; case ALL: return 5; default: return 0; } } public static Integer getSortingIdSearch() { return timePeriod == TimePeriod.HOUR ? 0 : timePeriod == TimePeriod.DAY ? 1 : timePeriod == TimePeriod.WEEK ? 2 : timePeriod == TimePeriod.MONTH ? 3 : timePeriod == TimePeriod.YEAR ? 4 : 5; } public static Integer getSortingIdSearch(Search s) { return s.time == TimePeriod.HOUR ? 0 : s.time == TimePeriod.DAY ? 1 : s.time == TimePeriod.WEEK ? 2 : s.time == TimePeriod.MONTH ? 3 : s.time == TimePeriod.YEAR ? 4 : 5; } public static Integer getTypeSearch() { return search == SubmissionSearchPaginator.SearchSort.RELEVANCE ? 0 : search == SubmissionSearchPaginator.SearchSort.TOP ? 1 : search == SubmissionSearchPaginator.SearchSort.NEW ? 2 : 3; } public static String[] getSortingStrings(Context c) { String[] current = new String[]{ c.getString(R.string.sorting_hot), c.getString(R.string.sorting_new), c.getString(R.string.sorting_rising), c.getString(R.string.sorting_top), c.getString(R.string.sorting_controversial), }; return current; } public static Spannable[] getSortingSpannables(Context c, String currentSub) { return getSortingSpannables(c, getSortingId(currentSub), currentSub); } public static Spannable[] getSortingSpannables(Context c, Sorting sorting) { return getSortingSpannables(c, getSortingId(sorting), " "); } private static Spannable[] getSortingSpannables(Context c, int sortingId, String sub) { ArrayList<Spannable> spannables = new ArrayList<>(); String[] sortingStrings = getSortingStrings(c); for (int i = 0; i < sortingStrings.length; i++) { SpannableString spanString = new SpannableString(sortingStrings[i]); if (i == sortingId) { spanString.setSpan(new ForegroundColorSpan(new ColorPreferences(c).getColor(sub)), 0, spanString.length(), 0); spanString.setSpan(new StyleSpan(Typeface.BOLD), 0, spanString.length(), 0); } spannables.add(spanString); } return spannables.toArray(new Spannable[spannables.size()]); } public static String[] getSortingStringsTime(Context c) { String[] current = new String[]{ c.getString(R.string.sorting_hour), c.getString(R.string.sorting_day), c.getString(R.string.sorting_week), c.getString(R.string.sorting_month), c.getString(R.string.sorting_year), c.getString(R.string.sorting_all), }; return current; } public static Spannable[] getSortingSpannablesTime(Context c, String currentSub) { return getSortingSpannablesTime(c, getSortingIdTime(currentSub), currentSub); } public static Spannable[] getSortingSpannablesTime(Context c, TimePeriod time) { return getSortingSpannablesTime(c, getSortingIdTime(time), " "); } private static Spannable[] getSortingSpannablesTime(Context c, int sortingId, String sub) { ArrayList<Spannable> spannables = new ArrayList<>(); String[] sortingStrings = getSortingStringsTime(c); for (int i = 0; i < sortingStrings.length; i++) { SpannableString spanString = new SpannableString(sortingStrings[i]); if (i == sortingId) { spanString.setSpan(new ForegroundColorSpan(new ColorPreferences(c).getColor(sub)), 0, spanString.length(), 0); spanString.setSpan(new StyleSpan(Typeface.BOLD), 0, spanString.length(), 0); } spannables.add(spanString); } return spannables.toArray(new Spannable[spannables.size()]); } public static String[] getSortingStringsComments(Context c) { return new String[]{ c.getString(R.string.sorting_best), c.getString(R.string.sorting_top), c.getString(R.string.sorting_new), c.getString(R.string.sorting_controversial), c.getString(R.string.sorting_old), c.getString(R.string.sorting_ama), }; } public static String[] getSearch(Context c) { return new String[]{ c.getString(R.string.search_relevance), c.getString(R.string.search_top), c.getString(R.string.search_new), c.getString(R.string.search_comments) }; } public static String[] getSortingStringsSearch(Context c) { return new String[]{ c.getString(R.string.sorting_search_hour), c.getString(R.string.sorting_search_day), c.getString(R.string.sorting_search_week), c.getString(R.string.sorting_search_month), c.getString(R.string.sorting_search_year), c.getString(R.string.sorting_search_all), }; } @Override public void onLowMemory() { super.onLowMemory(); getImageLoader().clearMemoryCache(); } public ImageLoader getImageLoader() { if (defaultImageLoader == null || !defaultImageLoader.isInited()) { ImageLoaderUtils.initImageLoader(getApplicationContext()); defaultImageLoader = ImageLoaderUtils.imageLoader; } return defaultImageLoader; } public static boolean notFirst = false; @Override public void onActivityResumed(Activity activity) { doLanguages(activity); if(client == null){ OkHttpClient.Builder builder = new OkHttpClient.Builder(); builder.dns(new GfycatIpv4Dns()); client = builder.build(); } if (authentication != null && Authentication.didOnline && Authentication.authentication.getLong("expires", 0) <= Calendar.getInstance() .getTimeInMillis()) { authentication.updateToken(activity); } else if (NetworkUtil.isConnected(activity) && authentication == null) { authentication = new Authentication(this); } } @Override public void onActivityPaused(Activity activity) { } public static void setDefaultErrorHandler(Context base) { //START code adapted from https://github.com/QuantumBadger/RedReader/ final Thread.UncaughtExceptionHandler androidHandler = Thread.getDefaultUncaughtExceptionHandler(); final WeakReference<Context> cont = new WeakReference<>(base); Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { public void uncaughtException(Thread thread, Throwable t) { if (cont.get() != null) { final Context c = cont.get(); Writer writer = new StringWriter(); PrintWriter printWriter = new PrintWriter(writer); t.printStackTrace(printWriter); String stacktrace = writer.toString().replace(";", ","); if (stacktrace.contains("UnknownHostException") || stacktrace.contains( "SocketTimeoutException") || stacktrace.contains("ConnectException")) { //is offline final Handler mHandler = new Handler(Looper.getMainLooper()); mHandler.post(new Runnable() { @Override public void run() { try { new AlertDialogWrapper.Builder(c).setTitle(R.string.err_title) .setMessage(R.string.err_connection_failed_msg) .setNegativeButton(R.string.btn_close, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (!(c instanceof MainActivity)) { ((Activity) c).finish(); } } }) .setPositiveButton(R.string.btn_offline, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Reddit.appRestart.edit() .putBoolean("forceoffline", true) .apply(); Reddit.forceRestart(c); } }) .show(); } catch (Exception ignored) { } } } ); } else if (stacktrace.contains("403 Forbidden") || stacktrace.contains( "401 Unauthorized")) { //Un-authenticated final Handler mHandler = new Handler(Looper.getMainLooper()); mHandler.post(new Runnable() { @Override public void run() { try { new AlertDialogWrapper.Builder(c).setTitle(R.string.err_title) .setMessage(R.string.err_refused_request_msg) .setNegativeButton("No", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (!(c instanceof MainActivity)) { ((Activity) c).finish(); } } }) .setPositiveButton("Yes", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { authentication.updateToken((c)); } }) .show(); } catch (Exception ignored) { } } }); } else if (stacktrace.contains("404 Not Found") || stacktrace.contains( "400 Bad Request")) { final Handler mHandler = new Handler(Looper.getMainLooper()); mHandler.post(new Runnable() { @Override public void run() { try { new AlertDialogWrapper.Builder(c).setTitle(R.string.err_title) .setMessage(R.string.err_could_not_find_content_msg) .setNegativeButton("Close", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (!(c instanceof MainActivity)) { ((Activity) c).finish(); } } }) .show(); } catch (Exception ignored) { } } }); } else if (t instanceof NetworkException) { Toast.makeText(c, "Error " + ((NetworkException) t).getResponse().getStatusMessage() + ": " + (t).getMessage(), Toast.LENGTH_LONG).show(); } else if (t instanceof NullPointerException && t.getMessage() .contains( "Attempt to invoke virtual method 'android.content.Context android.view.ViewGroup.getContext()' on a null object reference")) { t.printStackTrace(); } else if (t instanceof MaterialDialog.DialogException) { t.printStackTrace(); } else if (t instanceof IllegalArgumentException && t.getMessage() .contains("pointerIndex out of range")) { t.printStackTrace(); } else { appRestart.edit() .putString("startScreen", "a") .apply(); //Force reload of data after crash incase state was not saved try { SharedPreferences prefs = c.getSharedPreferences("STACKTRACE", Context.MODE_PRIVATE); prefs.edit().putString("stacktrace", stacktrace).apply(); } catch (Throwable ignored) { } androidHandler.uncaughtException(thread, t); } } else { androidHandler.uncaughtException(thread, t); } } }); //END adaptation } @Override public void onActivityStopped(Activity activity) { } @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { doLanguages(activity); } @Override public void onActivityStarted(Activity activity) { } @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { } @Override public void onActivityDestroyed(Activity activity) { } @Override public void onCreate() { super.onCreate(); // LeakCanary.install(this); if (ProcessPhoenix.isPhoenixProcess(this)) { return; } UpgradeUtil.upgrade(getApplicationContext()); doMainStuff(); } public void doMainStuff() { Log.v(LogUtil.getTag(), "ON CREATED AGAIN"); if(client == null){ client = new OkHttpClient(); } overrideLanguage = getSharedPreferences("SETTINGS", 0).getBoolean(SettingValues.PREF_OVERRIDE_LANGUAGE, false); appRestart = getSharedPreferences("appRestart", 0); AlbumUtils.albumRequests = getSharedPreferences("albums", 0); TumblrUtils.tumblrRequests = getSharedPreferences("tumblr", 0); cachedData = getSharedPreferences("cache", 0); if (!cachedData.contains("hasReset")) { cachedData.edit().clear().putBoolean("hasReset", true).apply(); } registerActivityLifecycleCallbacks(this); Authentication.authentication = getSharedPreferences("AUTH", 0); UserSubscriptions.subscriptions = getSharedPreferences("SUBSNEW", 0); UserSubscriptions.multiNameToSubs = getSharedPreferences("MULTITONAME", 0); UserSubscriptions.pinned = getSharedPreferences("PINNED", 0); PostMatch.filters = getSharedPreferences("FILTERS", 0); ImageFlairs.flairs = getSharedPreferences("FLAIRS", 0); SettingValues.setAllValues(getSharedPreferences("SETTINGS", 0)); defaultSorting = SettingValues.defaultSorting; timePeriod = SettingValues.timePeriod; colors = getSharedPreferences("COLOR", 0); tags = getSharedPreferences("TAGS", 0); KVStore.init(this, "SEEN"); doLanguages(this); lastposition = new ArrayList<>(); new SetupIAB().execute(); if (!appRestart.contains("startScreen")) { Authentication.isLoggedIn = appRestart.getBoolean("loggedin", false); Authentication.name = appRestart.getString("name", "LOGGEDOUT"); active = true; } else { appRestart.edit().remove("startScreen").apply(); } authentication = new Authentication(this); AdBlocker.init(this); Authentication.mod = Authentication.authentication.getBoolean(SHARED_PREF_IS_MOD, false); enter_animation_time = enter_animation_time_original * enter_animation_time_multiplier; fabClear = colors.getBoolean(SettingValues.PREF_FAB_CLEAR, false); int widthDp = this.getResources().getConfiguration().screenWidthDp; int heightDp = this.getResources().getConfiguration().screenHeightDp; int fina = (widthDp > heightDp) ? widthDp : heightDp; fina += 99; if (colors.contains("tabletOVERRIDE")) { dpWidth = colors.getInt("tabletOVERRIDE", fina / 300); } else { dpWidth = fina / 300; } if (colors.contains("notificationOverride")) { notificationTime = colors.getInt("notificationOverride", 360); } else { notificationTime = 360; } SettingValues.tabletUI = isPackageInstalled(this) || FDroid.isFDroid; videoPlugin = isVideoPluginInstalled(this); GifCache.init(this); } public void doLanguages(Context c) { if (SettingValues.overrideLanguage) { Locale locale = new Locale("en_US"); Locale.setDefault(locale); Configuration config = c.getResources().getConfiguration(); config.locale = locale; c.getResources().updateConfiguration(config, null); } } public static void setSorting(String s, Sorting sort) { sorting.put(s.toLowerCase(), sort); } public static final Map<String, Sorting> sorting = new HashMap<>(); public static Sorting getSorting(String subreddit, Sorting defaultSort) { subreddit = subreddit.toLowerCase(); if (sorting.containsKey(subreddit)) { return sorting.get(subreddit); } else { return defaultSort; } } public static TimePeriod getTime(String subreddit, TimePeriod defaultTime) { subreddit = subreddit.toLowerCase(); if (times.containsKey(subreddit)) { return times.get(subreddit); } else { return defaultTime; } } public static void setTime(String s, TimePeriod sort) { times.put(s.toLowerCase(), sort); } public static final Map<String, TimePeriod> times = new HashMap<>(); public static TimePeriod getTime(String subreddit) { subreddit = subreddit.toLowerCase(); if (times.containsKey(subreddit)) { return times.get(subreddit); } else { return Reddit.timePeriod; } } public interface Listener { void onBecameForeground(); void onBecameBackground(); } private class SetupIAB extends AsyncTask<Void, Void, Void> { @Override protected Void doInBackground(Void... params) { if(mHelper == null) try { mHelper = new IabHelper(Reddit.this, SecretConstants.getBase64EncodedPublicKey(getBaseContext())); mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() { public void onIabSetupFinished(IabResult result) { if (!result.isSuccess()) { LogUtil.e("Problem setting up In-app Billing: " + result); } } }); } catch (Exception ignored) { ignored.printStackTrace(); } return null; } } //IPV6 workaround by /u/talklittle public class GfycatIpv4Dns implements Dns { @Override public List<InetAddress> lookup(String hostname) throws UnknownHostException { if (ContentType.hostContains(hostname, "gfycat.com")) { InetAddress[] addresses = InetAddress.getAllByName(hostname); if (addresses == null || addresses.length == 0) { throw new UnknownHostException("Bad host: " + hostname); } // prefer IPv4; list IPv4 first ArrayList<InetAddress> result = new ArrayList<>(); for (InetAddress address : addresses) { if (address instanceof Inet4Address) { result.add(address); } } for (InetAddress address : addresses) { if (!(address instanceof Inet4Address)) { result.add(address); } } return result; } else { return Dns.SYSTEM.lookup(hostname); } } } }