Utility.java 16 KB

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