Utility.java 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. /*
  2. * DSBDirect
  3. * Copyright (C) 2019 Fynn Godau
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. *
  18. * This software is not affiliated with heinekingmedia GmbH, the
  19. * developer of the DSB platform.
  20. */
  21. package godau.fynn.dsbdirect;
  22. import android.app.Activity;
  23. import android.app.AlertDialog;
  24. import android.app.job.JobInfo;
  25. import android.app.job.JobScheduler;
  26. import android.content.*;
  27. import android.net.Uri;
  28. import android.os.Build;
  29. import android.support.annotation.Nullable;
  30. import android.support.annotation.RequiresApi;
  31. import android.support.annotation.StyleRes;
  32. import android.support.design.widget.Snackbar;
  33. import android.util.Log;
  34. import android.view.View;
  35. import android.view.ViewGroup;
  36. import godau.fynn.dsbdirect.manager.DownloadManager;
  37. import godau.fynn.dsbdirect.table.PollingService;
  38. import godau.fynn.dsbdirect.table.reader.GoetheHamburg;
  39. import godau.fynn.dsbdirect.table.reader.Reader;
  40. import godau.fynn.dsbdirect.table.reader.Roentgen;
  41. import godau.fynn.dsbdirect.table.reader.Untis;
  42. import godau.fynn.dsbdirect.table.reader.Untis4;
  43. import org.json.JSONException;
  44. import org.json.JSONObject;
  45. import org.jsoup.nodes.Element;
  46. import org.jsoup.select.Elements;
  47. import java.io.IOException;
  48. import java.text.SimpleDateFormat;
  49. import java.util.*;
  50. import static android.content.Context.MODE_PRIVATE;
  51. public class Utility {
  52. public static final String NULL = "automatic";
  53. public static final String UNTIS = "untis";
  54. public static final String UNTIS4 = "untis4"; // technically a SUPER SECRET SETTING
  55. public static final String ROENTGEN = "roentgen";
  56. public static final String GOETHE = "goethe";
  57. public static final String DATE_FORMAT = "EEEE, d.M.yyyy";
  58. // For Gadgetbridge compatibility: Gadgetbridge seems to only push content text, not big text text
  59. public static final String SUPER_SECRET_SETTING_TEXT_AS_CONTENT_TEXT = "textAsContentText";
  60. // For debugging: display messages even if the exact same content has already been displayed
  61. public static final String SUPER_SECRET_SETTING_NOTIFY_ABOUT_NOTHING_NEW = "ignoreAlreadyDisplayed";
  62. // For debugging: also poll on weekends
  63. public static final String SUPER_SECRET_SETTING_POLL_ON_WEEKENDS = "ignoreWeekend";
  64. // For convenience: in case you find sharing annoying, you can disable it
  65. public static final String SUPER_SECRET_SETTING_HOLD_TO_SHARE = "holdToShare";
  66. private Context mContext;
  67. public Utility(Context context) {
  68. this.mContext = context;
  69. }
  70. public String formatDate(Date date) {
  71. Calendar today = Calendar.getInstance();
  72. Calendar then = Calendar.getInstance();
  73. then.setTime(date);
  74. if (then.get(Calendar.YEAR) == today.get(Calendar.YEAR)
  75. && then.get(Calendar.DAY_OF_YEAR) == today.get(Calendar.DAY_OF_YEAR)) {
  76. return mContext.getString(R.string.today);
  77. }
  78. Calendar tomorrow = Calendar.getInstance();
  79. tomorrow.add(Calendar.DAY_OF_YEAR, 1);
  80. if (then.get(Calendar.YEAR) == tomorrow.get(Calendar.YEAR)
  81. && then.get(Calendar.DAY_OF_YEAR) == tomorrow.get(Calendar.DAY_OF_YEAR)) {
  82. return mContext.getString(R.string.tomorrow);
  83. }
  84. Calendar yesterday = Calendar.getInstance();
  85. yesterday.add(Calendar.DAY_OF_YEAR, -1);
  86. if (then.get(Calendar.YEAR) == yesterday.get(Calendar.YEAR)
  87. && then.get(Calendar.DAY_OF_YEAR) == yesterday.get(Calendar.DAY_OF_YEAR)) {
  88. return mContext.getString(R.string.yesterday);
  89. }
  90. // Date is neither today, yesterday nor tomorrow, format it
  91. SimpleDateFormat simpleDateFormat = new SimpleDateFormat(DATE_FORMAT, Locale.getDefault());
  92. return simpleDateFormat.format(date);
  93. }
  94. public @Nullable
  95. Reader getReader(String html, String id) {
  96. // User preference overwrite hardcoded preference overwrite dynamic decision
  97. switch (getParserIdentifier(id)) {
  98. case UNTIS:
  99. return new Untis(html);
  100. case UNTIS4:
  101. return new Untis4(html);
  102. case ROENTGEN:
  103. return new Roentgen(html);
  104. case GOETHE:
  105. return new GoetheHamburg(html);
  106. }
  107. // Dynamic decision: check whether this might be a Untis table
  108. if (html.contains("Untis Stundenplan Software")) {
  109. return new Untis(html);
  110. }
  111. // There is no reader if we got here
  112. return null;
  113. }
  114. public String getParserIdentifier(String id) {
  115. // Consider user setting
  116. String userSetting = getParserUserSetting();
  117. if (!userSetting.equals(NULL)) {
  118. // userSetting overwrites hardcoded preference by id
  119. id = userSetting;
  120. }
  121. switch (id) {
  122. case "162161": // Alexander-von-Humboldt-Gymnasium Schweinfurt
  123. case UNTIS:
  124. return UNTIS;
  125. case UNTIS4: // Please don't use this
  126. return UNTIS4;
  127. case ROENTGEN:
  128. case "219261": // Röntgen-Gymnasium Würzburg
  129. return ROENTGEN;
  130. case GOETHE:
  131. case "189802": // Goethe-Gymnasium Hamburg
  132. return GOETHE;
  133. default:
  134. return NULL;
  135. }
  136. }
  137. public String getParserUserSetting() {
  138. return getSharedPreferences().getString("parser", "automatic");
  139. }
  140. public static Date parseLastUpdatedDate(String input) {
  141. String[] dateParts = input.split(" ");
  142. String[] dateDigits = dateParts[0].split("\\.");
  143. String[] timeDigits = dateParts[1].split(":");
  144. return new Date(Integer.parseInt(dateDigits[2]) - 1900 /* years start at 1900 */, Integer.parseInt(dateDigits[1]) - 1 /* months start with 0 */,
  145. Integer.parseInt(dateDigits[0]), Integer.parseInt(timeDigits[0]), Integer.parseInt(timeDigits[1])); // because SimpleDateTime did deliver good results, again
  146. }
  147. public void checkForUpdate(final boolean verbose, final DownloadManager downloadManager) {
  148. // Thanks, https://stackoverflow.com/a/5069354
  149. final Activity activity = (Activity) mContext;
  150. final View rootView = ((ViewGroup) activity
  151. .findViewById(android.R.id.content)).getChildAt(0);
  152. Log.d("LOCALE", Locale.getDefault().toString());
  153. new Thread(new Runnable() {
  154. @Override
  155. public void run() {
  156. try {
  157. // Request current version metadata
  158. JSONObject jsonObject = downloadManager.downloadUpdateCheck();
  159. int currentVersion = BuildConfig.VERSION_CODE;
  160. // Let's parse
  161. int latestVersion = jsonObject.getInt("versionCode");
  162. final String versionName = jsonObject.getString("versionName");
  163. final String whatsNew;
  164. // Localize changelog to German (hardcoded and uncool)
  165. if (Locale.getDefault().toString().contains("de")) {
  166. whatsNew = jsonObject.getString("whatsNewDE");
  167. } else {
  168. whatsNew = jsonObject.getString("whatsNew");
  169. }
  170. final String uri = jsonObject.getString("url");
  171. if (latestVersion > currentVersion) {
  172. activity.runOnUiThread(new Runnable() {
  173. @Override
  174. public void run() {
  175. new AlertDialog.Builder(mContext)
  176. .setPositiveButton(R.string.download, new DialogInterface.OnClickListener() {
  177. @Override
  178. public void onClick(DialogInterface dialog, int which) {
  179. Intent intent;
  180. switch (mContext.getString(R.string.update_method)) {
  181. default:
  182. case "self":
  183. // Download file provided in updateCheck
  184. intent = new Intent(Intent.ACTION_VIEW)
  185. .setDataAndType(Uri.parse(uri), "application/vnd.android.package-archive");
  186. break;
  187. case "fdroid":
  188. // Launch fdroid website link and hope fdroid client handles it
  189. String fdroidUri = mContext.getString(R.string.uri_fdroid, mContext.getPackageName());
  190. intent = new Intent(Intent.ACTION_VIEW, Uri.parse(fdroidUri));
  191. break;
  192. case "market":
  193. // Launch market to download update
  194. String marketUri = mContext.getString(R.string.uri_market, mContext.getPackageName());
  195. intent = new Intent(Intent.ACTION_VIEW, Uri.parse(marketUri));
  196. break;
  197. }
  198. mContext.startActivity(intent);
  199. }
  200. })
  201. .setNegativeButton(R.string.dismiss, null)
  202. .setTitle(mContext.getString(R.string.update_popup_title, versionName))
  203. .setMessage(whatsNew)
  204. .show();
  205. }
  206. });
  207. } else {
  208. if (verbose) {
  209. activity.runOnUiThread(new Runnable() {
  210. @Override
  211. public void run() {
  212. Snackbar
  213. .make(rootView, R.string.up_to_date, Snackbar.LENGTH_SHORT)
  214. .show();
  215. }
  216. });
  217. }
  218. }
  219. } catch (IOException | JSONException e) {
  220. activity.runOnUiThread(new Runnable() {
  221. @Override
  222. public void run() {
  223. Snackbar.make(rootView, R.string.network_versioncheck_failed, Snackbar.LENGTH_SHORT)
  224. .show();
  225. }
  226. });
  227. e.printStackTrace();
  228. }
  229. }
  230. }).start();
  231. }
  232. public SharedPreferences getSharedPreferences() {
  233. return mContext.getSharedPreferences("default", MODE_PRIVATE);
  234. }
  235. public String getUpdateCheckFrequency() {
  236. return getSharedPreferences().getString("update_check_frequency",
  237. mContext.getString(R.string.update_check_frequency_default));
  238. }
  239. public boolean shouldCheckForUpdates() {
  240. switch (getUpdateCheckFrequency()) {
  241. case "always":
  242. return true;
  243. case "sometimes":
  244. return new Random().nextInt(10) == 0;
  245. case "never":
  246. default:
  247. return false;
  248. }
  249. }
  250. public void resetToken() {
  251. // Reset token
  252. getSharedPreferences().edit().putString("token", null).apply();
  253. }
  254. @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
  255. public void schedulePolling() {
  256. // Check whether polling is enabled
  257. SharedPreferences sharedPreferences = getSharedPreferences();
  258. if (sharedPreferences.getBoolean("poll", false)) {
  259. // Polling is enabled
  260. ComponentName serviceComponent = new ComponentName(mContext, PollingService.class);
  261. JobInfo.Builder builder = new JobInfo.Builder(0, serviceComponent);
  262. builder.setMinimumLatency(PollingService.getNextPollingTimeDelay(sharedPreferences)); // Poll after at least this time
  263. builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); // Only execute once network is connected
  264. builder.setPersisted(true);
  265. JobScheduler jobScheduler = (JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE);
  266. jobScheduler.schedule(builder.build());
  267. } else {
  268. // Cancel potential job
  269. JobScheduler jobScheduler = (JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE);
  270. jobScheduler.cancel(0);
  271. }
  272. }
  273. public void saveAuthToken(String authToken) {
  274. final SharedPreferences sharedPreferences = getSharedPreferences();
  275. sharedPreferences.edit()
  276. .putString("token", authToken)
  277. .apply();
  278. }
  279. public static String smartConcatenate(String[] strings, String comma) {
  280. StringBuilder result = new StringBuilder();
  281. // Remove empty strings
  282. ArrayList<String> list = new ArrayList<>(Arrays.asList(strings));
  283. for (int i = list.size() - 1; i >= 0; i--) {
  284. if (list.get(i) == null || list.get(i).isEmpty()) {
  285. list.remove(i);
  286. }
  287. }
  288. // Append remaining strings to result
  289. for (int i = 0; i < list.size(); i++) {
  290. String string = list.get(i);
  291. result.append(string);
  292. if (i != list.size() - 1) {
  293. // This is not the last iteration yet
  294. result.append(comma);
  295. }
  296. }
  297. return String.valueOf(result);
  298. }
  299. public static Calendar zeroOClock(Calendar calendar) {
  300. calendar.set(Calendar.HOUR_OF_DAY, 0);
  301. calendar.set(Calendar.MINUTE, 0);
  302. calendar.set(Calendar.SECOND, 0);
  303. calendar.set(Calendar.MILLISECOND, 0);
  304. return calendar;
  305. }
  306. public static String smartConcatenate(int[] integers, String comma) {
  307. String[] strings = new String[integers.length];
  308. for (int i = 0; i < integers.length; i++) {
  309. strings[i] = String.valueOf(integers[i]);
  310. }
  311. return smartConcatenate(strings, comma);
  312. }
  313. public static String smartConcatenate(Object[] objects, String comma) {
  314. // Probably a little inefficient, but it probably doesn't matter
  315. String[] strings = new String[objects.length];
  316. for (int i = 0; i < objects.length; i++) {
  317. strings[i] = String.valueOf(objects[i]);
  318. }
  319. return smartConcatenate(strings, comma);
  320. }
  321. public static String smartConcatenate(Elements elements, String comma) {
  322. String[] strings = new String[elements.size()];
  323. for (int i = 0; i < elements.size(); i++) {
  324. Element e = elements.get(i);
  325. strings[i] = e.text().replaceAll("\\\\n", "\n");
  326. }
  327. return smartConcatenate(strings, comma);
  328. }
  329. public @StyleRes
  330. int getStyle() {
  331. String preference = getSharedPreferences().getString("style", "light");
  332. switch (preference) {
  333. case "light":
  334. default:
  335. return R.style.Light;
  336. case "dark":
  337. return R.style.Dark;
  338. case "black":
  339. return R.style.Black;
  340. }
  341. }
  342. }