package de.tap.easy_xkcd.database; import android.app.Activity; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.AsyncTask; import android.support.customtabs.CustomTabsIntent; import android.util.Log; import android.widget.Toast; import com.tap.xkcd_reader.R; import org.json.JSONObject; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Arrays; import java.util.HashSet; import java.util.Random; import de.tap.easy_xkcd.Activities.SearchResultsActivity; import de.tap.easy_xkcd.CustomTabHelpers.BrowserFallback; import de.tap.easy_xkcd.CustomTabHelpers.CustomTabActivityHelper; import de.tap.easy_xkcd.fragments.overview.OverviewBaseFragment; import de.tap.easy_xkcd.utils.Comic; import de.tap.easy_xkcd.utils.JsonParser; import de.tap.easy_xkcd.utils.PrefHelper; import de.tap.easy_xkcd.utils.ThemePrefs; import io.realm.Realm; import io.realm.RealmResults; import io.realm.exceptions.RealmPrimaryKeyConstraintException; public class DatabaseManager { private Context context; public Realm realm; private static final String REALM_DATABASE_LOADED = "pref_realm_database_loaded"; private static final String HIGHEST_DATABASE = "highest_database"; private static final String COMIC_READ = "comic_read"; private static final String FAVORITES_MOVED = "fav_moved"; private static final String FAVORITES = "favorites"; public DatabaseManager(Context context) { this.context = context; realm = Realm.getInstance(context); } private SharedPreferences getSharedPrefs() { return context.getSharedPreferences("MainActivity", Activity.MODE_PRIVATE); } /////////////////////// FAVORITES ///////////////////////////////////////// public boolean noFavorites() { return getSharedPrefs().getString(FAVORITES, null) == null; } /** * Gets an array of all favorite comics * @return an int array containing the numbers of all favorite comics, null if there are no favorites */ public int[] getFavComics() { String fs = getSharedPrefs().getString(FAVORITES, null); if (fs == null) return null; HashSet<String> fSet = new HashSet<>(Arrays.asList(fs.split(","))); // HashSet automatically removes duplicate Items. String[] f = new String[fSet.size()]; // (There was a bug before where favorites would be added twice, fSet.toArray(f); // so this fixes it while not risking to lose any data int[] fav = new int[f.length]; for (int i = 0; i < f.length; i++) fav[i] = Integer.parseInt(f[i]); Arrays.sort(fav); return fav; } public boolean checkFavorite(int fav) { return getFavComics() != null && Arrays.binarySearch(getFavComics(), fav) >= 0; } /** * Adds or removes a favorite to realm and sharedPreferences * @param fav the comic number of the fac to be modified * @param isFav if the comic has been added or removed from favorites */ public void setFavorite(int fav, boolean isFav) { //Save to realm if (fav <= getHighestInDatabase()) { Realm realm = Realm.getInstance(context); realm.beginTransaction(); RealmComic comic = realm.where(RealmComic.class).equalTo("comicNumber", fav).findFirst(); comic.setFavorite(isFav); realm.copyToRealmOrUpdate(comic); realm.commitTransaction(); realm.close(); } //Save to sharedPrefs if (isFav) addFavorite(fav); else removeFavorite(fav); } private void addFavorite(int fav) { String favorites = getSharedPrefs().getString(FAVORITES, null); if (favorites == null) favorites = String.valueOf(fav); else favorites += "," + String.valueOf(fav); getSharedPrefs().edit().putString(FAVORITES, favorites).apply(); } private void removeFavorite(int favToRemove) { int[] old = getFavComics(); int a = Arrays.binarySearch(old, favToRemove); int[] out = new int[old.length - 1]; if (out.length != 0 && a >= 0) { System.arraycopy(old, 0, out, 0, a); System.arraycopy(old, a + 1, out, a, out.length - a); StringBuilder sb = new StringBuilder(); sb.append(out[0]); for (int i = 1; i < out.length; i++) { sb.append(","); sb.append(out[i]); } getSharedPrefs().edit().putString(FAVORITES, sb.toString()).apply(); } else { getSharedPrefs().edit().putString(FAVORITES, null).apply(); } } public void removeAllFavorites() { getSharedPrefs().edit().putString(FAVORITES, null).apply(); } /** * In previous versions the favorite list would be only accessible from MainActivity, * so this moves the favorites list to regular sharedPreferences * * @param activity should be MainActivity */ public void moveFavorites(Activity activity) { if (!getSharedPrefs().getBoolean(FAVORITES_MOVED, false)) { String fav = activity.getPreferences(Activity.MODE_PRIVATE).getString("favorites", null); getSharedPrefs().edit().putString(FAVORITES, fav).putBoolean(FAVORITES_MOVED, true).apply(); Log.d("prefHelper", "moved favorites"); } } ///////////////// READ COMICS ////////////////////////////////////////// /** * Sets a comic read or unread * @param number the comic number * @param isRead if the comic is read or unread */ public void setRead(int number, boolean isRead) { if (number <= getHighestInDatabase()) { //Save to realm realm.beginTransaction(); RealmComic comic = realm.where(RealmComic.class).equalTo("comicNumber", number).findFirst(); if (comic != null) { comic.setRead(isRead); realm.copyToRealmOrUpdate(comic); } realm.commitTransaction(); } else { //Save to sharedPrefs String read = getSharedPrefs().getString(COMIC_READ, ""); if (!read.equals("")) { read = read + "," + String.valueOf(number); } else { read = String.valueOf(number); } getSharedPrefs().edit().putString(COMIC_READ, read).apply(); } } /** * Gets an array of read comics * @return an int array containing the numbers of all read comics */ public int[] getReadComics() { String r = getSharedPrefs().getString(COMIC_READ, ""); if (r.equals("")) return new int[0]; String[] re = r.split(","); int[] read = new int[re.length]; for (int i = 0; i < re.length; i++) read[i] = Integer.parseInt(re[i]); Arrays.sort(read); return read; } public void setComicsUnread() { getSharedPrefs().edit().putString(COMIC_READ, "").apply(); realm.beginTransaction(); RealmResults<RealmComic> comics = realm.where(RealmComic.class).findAll(); for (int i = 0; i < comics.size(); i++) { RealmComic comic = comics.get(i); comic.setRead(false); realm.copyToRealmOrUpdate(comic); } realm.commitTransaction(); } /** * Interpolates the number of the next unread comic when "hide read" is checked in overview mode * @param number the selected comic * @param comics all comics in the realm database * @return the comic number that the list should scroll to */ public int getNextUnread(int number, RealmResults<RealmComic> comics) { RealmComic comic; try { comic = comics.where().greaterThan("comicNumber", number).equalTo("isRead", false).findAll().last(); } catch (IndexOutOfBoundsException e) { Log.e("top of list reached", e.getMessage()); try { comic = comics.where().lessThan("comicNumber", number).equalTo("isRead", false).findAll().first(); } catch (IndexOutOfBoundsException e2) { Log.e("all comics read", e2.getMessage()); return 1; } } return comic.getComicNumber(); } //////////////// COMIC DATABASE ////////////////////////////////////////// public class updateComicDatabase extends AsyncTask<Void, Integer, Void> { private ProgressDialog progress; private SearchResultsActivity activity; private OverviewBaseFragment fragment; private PrefHelper prefHelper; public updateComicDatabase(SearchResultsActivity activity, OverviewBaseFragment fragment, PrefHelper prefHelper) { this.activity = activity; this.fragment = fragment; this.prefHelper = prefHelper; } @Override protected void onPreExecute() { progress = new ProgressDialog(context); progress.setTitle(context.getResources().getString(R.string.update_database)); progress.setIndeterminate(false); progress.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); progress.setCancelable(false); progress.show(); } @Override protected Void doInBackground(Void... params) { Realm realm = Realm.getInstance(context); int[] read = getReadComics(); int[] fav = getFavComics(); if (!databaseLoaded()) { //Save preloaded comic data to realm String[] titles = getFile(R.raw.comic_titles).split("&&"); String[] trans = getFile(R.raw.comic_trans).split("&&"); String[] urls = getFile(R.raw.comic_urls).split("&&"); realm.beginTransaction(); for (int i = 0; i < 1645; i++) { try { RealmComic comic = realm.createObject(RealmComic.class); comic.setComicNumber(i + 1); comic.setTitle(titles[i]); comic.setTranscript(trans[i]); comic.setUrl(urls[i]); comic.setRead(read != null && Arrays.binarySearch(read, i + 1) >= 0); comic.setFavorite(fav != null && Arrays.binarySearch(fav, i + 1) >= 0); } catch (RealmPrimaryKeyConstraintException e) { Log.d("error at " + i, e.getMessage()); } } realm.commitTransaction(); setHighestInDatabase(1645); } if (prefHelper.isOnline(context)) { int newest; try { Comic comic = new Comic(0); newest = comic.getComicNumber(); } catch (IOException e) { newest = prefHelper.getNewest(); } realm.beginTransaction(); int highest = getHighestInDatabase() + 1; for (int i = highest; i <= newest; i++) { try { Comic comic = new Comic(i); RealmComic realmComic = realm.createObject(RealmComic.class); realmComic.setComicNumber(i); realmComic.setTitle(comic.getComicData()[0]); realmComic.setTranscript(comic.getTranscript()); realmComic.setUrl(comic.getComicData()[2]); realmComic.setRead(read != null && Arrays.binarySearch(read, i) >= 0); realmComic.setFavorite(fav != null && Arrays.binarySearch(fav, i) >= 0); float x = newest - highest; int y = i - highest; int p = (int) ((y / x) * 100); publishProgress(p); } catch (IOException | RealmPrimaryKeyConstraintException e) { Log.d("error at " + i, e.getMessage()); } } realm.commitTransaction(); setHighestInDatabase(newest); } setDatabaseLoaded(true); realm.close(); return null; } private String getFile(int rawId) { InputStream is = context.getResources().openRawResource(rawId); BufferedReader br = new BufferedReader(new InputStreamReader(is)); StringBuilder sb = new StringBuilder(); String line; try { while ((line = br.readLine()) != null) { sb.append(line); } } catch (IOException e) { Log.e("error:", e.getMessage()); } return sb.toString(); } protected void onProgressUpdate(Integer... pro) { progress.setProgress(pro[0]); } @Override protected void onPostExecute(Void dummy) { progress.dismiss(); if (activity != null) activity.updateDatabasePostExecute(); else fragment.updateDatabasePostExecute(); } } public int getRandomUnread() { RealmResults<RealmComic> unread = realm.where(RealmComic.class).equalTo("isRead", false).findAll(); int n = new Random().nextInt(unread.size()); return unread.get(n).getComicNumber(); } public int getHighestInDatabase() { return getSharedPrefs().getInt(HIGHEST_DATABASE, 1); } public void setHighestInDatabase(int i) { getSharedPrefs().edit().putInt(HIGHEST_DATABASE, i).apply(); } public boolean databaseLoaded() { return getSharedPrefs().getBoolean(REALM_DATABASE_LOADED, false); } public void setDatabaseLoaded(boolean loaded) { getSharedPrefs().edit().putBoolean(REALM_DATABASE_LOADED, loaded).apply(); } /** * Gets the transcript of a comic * @param number the comic number * @return the comics transcript or " " if the comic does not exist in the database */ public static String getTranscript(int number, Context context) { RealmComic comic = Realm.getInstance(context).where(RealmComic.class).equalTo("comicNumber", number).findFirst(); if (comic != null) return comic.getTranscript(); return " "; } ////////////////// WHAT IF DATABASE ///////////////////////// /** * Randall doesn't seem to update the thumbnails for what-if articles at http://what-if.xkcd.com/archive/ anymore. * @param title the title of the what-if * @return the resource id of the custom thumbnail or 0 if the article already has a thumbnail */ public int getWhatIfMissingThumbnailId(String title) { switch (title) { case "Jupiter Descending": return R.mipmap.jupiter_descending; case "Jupiter Submarine": return R.mipmap.jupiter_submarine; case "New Horizons": return R.mipmap.new_horizons; case "Proton Earth, Electron Moon": return R.mipmap.proton_earth; case "Sunbeam": return R.mipmap.sun_beam; case "Space Jetta": return R.mipmap.jetta; case "Europa Water Siphon": return R.mipmap.straw; case "Saliva Pool": return R.mipmap.question; case "Fire From Moonlight": return R.mipmap.rabbit; case "Stop Jupiter": return R.mipmap.burlap; case "Niagara Straw": return R.mipmap.barrel; case "Eat the Sun": return R.mipmap.snakemeat; case "Pizza Bird": return R.mipmap.pickup; case "Tatooine Rainbow": return R.mipmap.trig; case "Sun Bug": return R.mipmap.blocked; case "Flood Death Valley": return R.mipmap.deathsea; case "Hide the Atmosphere": return R.mipmap.cube; case "Coast-to-Coast Coasting": return R.mipmap.beach; case "Toaster vs. Freezer": return R.mipmap.setup; case "Electrofishing for Whales": return R.mipmap.setup1; default: return 0; } } /** * Shows the reddit or forum thread for comics or WhatIf * @param title of the comic or WhatIf */ public static boolean showThread(final String title, final Context context, final boolean isWhatIf) { new AlertDialog.Builder(context) .setItems(R.array.forum_thread, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { switch (which) { case 0: new GetRedditLinkTask(context).execute(title); break; case 1: new GetForumLinkTask(context, isWhatIf).execute(title); break; } } }).create().show(); return true; } private static class GetRedditLinkTask extends AsyncTask<String, Void, String> { private ProgressDialog progress; private Context context; public GetRedditLinkTask(Context context) { this.context = context; } @Override protected void onPreExecute() { progress = new ProgressDialog(context); progress.setIndeterminate(true); progress.setMessage(context.getResources().getString(R.string.loading_thread)); progress.setCancelable(false); progress.show(); } @Override protected String doInBackground(String... title) { try { return "https://www.reddit.com" + JsonParser.getJSONFromUrl("https://www.reddit.com/r/xkcd/search.json?q=" + title[0] + "&restrict_sr=on") .getJSONObject("data") .getJSONArray("children").getJSONObject(0).getJSONObject("data").getString("permalink"); } catch (Exception e) { Log.d("reddit link", "timeout, trying again..."); try { return "https://www.reddit.com" + JsonParser.getJSONFromUrl("https://www.reddit.com/r/xkcd/search.json?q=" + title[0] + "&restrict_sr=on") .getJSONObject("data") .getJSONArray("children").getJSONObject(0).getJSONObject("data").getString("permalink"); } catch (Exception e2) { Log.e("error at " + title[0], e2.getMessage()); } return ""; } } @Override protected void onPostExecute(String url) { progress.dismiss(); if (url.equals("")) Toast.makeText(context, context.getString(R.string.thread_not_found), Toast.LENGTH_SHORT).show(); else { url = url.replace("www", "m"); context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); } } } protected static class GetForumLinkTask extends AsyncTask<String, Void, String> { private ProgressDialog progress; private Context context; private boolean isWhatIf; public GetForumLinkTask(Context context, boolean isWhatIf) { this.context = context; this.isWhatIf = isWhatIf; } @Override protected void onPreExecute() { progress = new ProgressDialog(context); progress.setIndeterminate(true); progress.setMessage(context.getResources().getString(R.string.loading_thread)); progress.setCancelable(false); progress.show(); } @Override protected String doInBackground(String... title) { String end; if (isWhatIf) end = "&terms=all&author=&fid%5B%5D=60&sc=1&sf=titleonly&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Search"; //Comic forum else end = "&terms=all&author=&fid%5B%5D=7&sc=1&sf=titleonly&sr=posts&sk=t&sd=d&st=0&ch=300&t=0&submit=Search"; //What-if forum try { return Jsoup.connect("http://forums.xkcd.com/search.php?keywords=" + title[0] + end).get() .select("div#wrap").select("div#page-body") .first().select(".postbody").first().select("a[href]").first().absUrl("href"); } catch (Exception e) { Log.e("error at " + title[0], e.getMessage()); return ""; } } @Override protected void onPostExecute(String url) { progress.dismiss(); if (url.equals("")) Toast.makeText(context, context.getResources().getString(R.string.thread_not_found), Toast.LENGTH_SHORT).show(); else { CustomTabsIntent.Builder intentBuilder = new CustomTabsIntent.Builder(); intentBuilder.setToolbarColor(new ThemePrefs(context).getPrimaryColor(false)); CustomTabActivityHelper.openCustomTab(((Activity) context), intentBuilder.build(), Uri.parse(url), new BrowserFallback()); } } } }