MainActivity.java 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986
  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.activity;
  22. import android.app.AlertDialog;
  23. import android.content.Intent;
  24. import android.content.SharedPreferences;
  25. import android.graphics.Bitmap;
  26. import android.graphics.ColorMatrixColorFilter;
  27. import android.graphics.drawable.BitmapDrawable;
  28. import android.os.Build;
  29. import android.os.Bundle;
  30. import android.os.Handler;
  31. import android.util.Log;
  32. import android.view.Menu;
  33. import android.view.MenuItem;
  34. import android.view.SubMenu;
  35. import android.view.View;
  36. import android.view.ViewGroup;
  37. import android.webkit.WebSettings;
  38. import android.webkit.WebView;
  39. import android.widget.Button;
  40. import android.widget.ImageView;
  41. import android.widget.TextView;
  42. import androidx.annotation.NonNull;
  43. import androidx.annotation.StringRes;
  44. import androidx.appcompat.app.AppCompatActivity;
  45. import androidx.appcompat.widget.Toolbar;
  46. import androidx.recyclerview.widget.DividerItemDecoration;
  47. import androidx.recyclerview.widget.LinearLayoutManager;
  48. import androidx.recyclerview.widget.RecyclerView;
  49. import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
  50. import com.google.android.material.snackbar.Snackbar;
  51. import com.ortiz.touchview.TouchImageView;
  52. import com.wefika.horizontalpicker.HorizontalPicker;
  53. import java.io.File;
  54. import java.io.IOException;
  55. import java.io.Serializable;
  56. import java.util.ArrayList;
  57. import java.util.Arrays;
  58. import java.util.Date;
  59. import java.util.LinkedHashSet;
  60. import java.util.LinkedList;
  61. import java.util.List;
  62. import java.util.Queue;
  63. import java.util.Set;
  64. import java.util.concurrent.BlockingQueue;
  65. import java.util.concurrent.LinkedBlockingQueue;
  66. import godau.fynn.dsbdirect.BuildConfig;
  67. import godau.fynn.dsbdirect.R;
  68. import godau.fynn.dsbdirect.activity.fragments.MainSettingsFragment;
  69. import godau.fynn.dsbdirect.download.DownloadManager;
  70. import godau.fynn.dsbdirect.download.NewsQuery;
  71. import godau.fynn.dsbdirect.download.exception.LoginFailureException;
  72. import godau.fynn.dsbdirect.download.exception.NoContentException;
  73. import godau.fynn.dsbdirect.download.exception.UnexpectedResponseException;
  74. import godau.fynn.dsbdirect.model.Login;
  75. import godau.fynn.dsbdirect.model.Table;
  76. import godau.fynn.dsbdirect.model.entry.Entry;
  77. import godau.fynn.dsbdirect.model.entry.ErrorEntry;
  78. import godau.fynn.dsbdirect.model.noticeboard.NoticeBoardItem;
  79. import godau.fynn.dsbdirect.persistence.FileManager;
  80. import godau.fynn.dsbdirect.persistence.LoginManager;
  81. import godau.fynn.dsbdirect.table.reader.ReaderRunnable;
  82. import godau.fynn.dsbdirect.util.Utility;
  83. import godau.fynn.dsbdirect.view.adapter.Adapter;
  84. import humanize.Humanize;
  85. import humanize.time.TimeMillis;
  86. public class MainActivity extends AppCompatActivity {
  87. private static final int REQUEST_LOGIN = 1;
  88. private Login login;
  89. private Table[] mTables;
  90. private Date mTimetabledate;
  91. private List<NoticeBoardItem> mNoticeBoardItemList;
  92. private boolean mTimetabledateDisplayed = false;
  93. private Utility u;
  94. private FileManager mFileManager;
  95. private DownloadManager mDownloadManager;
  96. private LoginManager mLoginManager;
  97. private final Queue<Runnable> mReaderTasks = new LinkedList<>();
  98. private TextView mTextView;
  99. private WebView mWebView;
  100. private Adapter mAdapter;
  101. private final BlockingQueue<Runnable> onMenuCreated = new LinkedBlockingQueue<>();
  102. private boolean mParse = true;
  103. private boolean mMerge = true;
  104. private Menu mMenu;
  105. private SwipeRefreshLayout mSwipeLayout;
  106. private Thread menuOperationsThread = null;
  107. public static boolean filterEnabled;
  108. public static boolean initialized = false; //only set filter_enabled once
  109. private static int offlineFilterPage = 0;
  110. @Override
  111. protected void onCreate(Bundle savedInstanceState) {
  112. super.onCreate(savedInstanceState);
  113. u = new Utility(MainActivity.this);
  114. u.stylize();
  115. long drawTimeStart = System.currentTimeMillis();
  116. setContentView(R.layout.activity_main);
  117. // Style horizontal picker
  118. findViewById(R.id.page).setBackgroundColor(u.getColorPrimary());
  119. long drawTimeEnd = System.currentTimeMillis();
  120. Log.d("DRAW", "rendering layout took " + (drawTimeEnd - drawTimeStart) + " milliseconds");
  121. final SharedPreferences sharedPreferences = u.getSharedPreferences();
  122. mParse = sharedPreferences.getBoolean("parse", true);
  123. mMerge = sharedPreferences.getBoolean("merge", true);
  124. Toolbar toolbar = findViewById(R.id.toolbar);
  125. toolbar.setBackgroundColor(u.getColorPrimary());
  126. setSupportActionBar(toolbar);
  127. mTextView = findViewById(R.id.text);
  128. // Get managers
  129. mFileManager = new FileManager(MainActivity.this);
  130. mDownloadManager = DownloadManager.getDownloadManager(this);
  131. mLoginManager = new LoginManager(MainActivity.this);
  132. int previousVersion = sharedPreferences.getInt("version", BuildConfig.VERSION_CODE);
  133. // Migrate 1.8.1 (version code 13) users to avoid login screen
  134. if (previousVersion <= 13) {
  135. sharedPreferences.edit().putBoolean("login", true).apply();
  136. }
  137. // Delete useless auth token from versions up to 2.4.1 (version code 22)
  138. if (previousVersion <= 22) {
  139. sharedPreferences.edit().remove("token").apply();
  140. }
  141. // Migrate login from up to version 2.5.5 (version code 29)
  142. if (previousVersion <= 29) {
  143. mLoginManager.addLogin(new Login(
  144. sharedPreferences.getString("id", ""),
  145. sharedPreferences.getString("pass", "")
  146. ));
  147. sharedPreferences.edit().remove("pass").apply();
  148. }
  149. // Migrate shortcodes stored in a StringSet preference from up to version 2.6 (version code 32)
  150. if (previousVersion <= 32) {
  151. Set<String> shortcodeSet = sharedPreferences.getStringSet("shortcodes", new LinkedHashSet<String>());
  152. String[] shortcodes = shortcodeSet.toArray(new String[shortcodeSet.size()]);
  153. Arrays.sort(shortcodes);
  154. StringBuilder shortcodesRaw = new StringBuilder();
  155. Arrays.sort(shortcodes); //sort alphabetical
  156. for (int i = 0; i < shortcodes.length; i++) {
  157. shortcodesRaw.append(shortcodes[i].replace("\n", ""));
  158. if(i != shortcodes.length - 1)
  159. shortcodesRaw.append("\n");
  160. }
  161. SharedPreferences.Editor editor = sharedPreferences.edit();
  162. editor.remove("shortcodes");
  163. editor.putString("shortcodes", shortcodesRaw.toString());
  164. editor.apply();
  165. }
  166. // Inform users about name change since version 2.6 or 2.6.1 (which was not tagged in master – version code 33)
  167. if (previousVersion <= 33) {
  168. new AlertDialog.Builder(MainActivity.this)
  169. .setTitle(R.string.migration_dsbdirect_rename)
  170. .setMessage(R.string.migration_dsbdirect_rename_message)
  171. .setPositiveButton(R.string.ok, null)
  172. .setNeutralButton(R.string.email_the_dev, (dialog, which) ->
  173. MainSettingsFragment.emailTheDev(MainActivity.this)
  174. )
  175. .show();
  176. }
  177. // Inform about depreciation of self update after version 3.2.1 (version code 38)
  178. if (previousVersion <= 38 && BuildConfig.FLAVOR.equals("notabug")) {
  179. new AlertDialog.Builder(this)
  180. .setTitle(R.string.migration_depreciation_self_update)
  181. .setMessage(R.string.migration_depreciation_self_update_message)
  182. .setPositiveButton(R.string.ok, null)
  183. .show();
  184. }
  185. // Migrate users to most recent version by wiping news after update
  186. if (previousVersion < BuildConfig.VERSION_CODE) {
  187. Log.d("MIGRATION", "wiping news");
  188. NewsQuery.wipeNews(MainActivity.this);
  189. }
  190. // Start loading or show login screen
  191. if (mLoginManager.canLogin()) {
  192. login = mLoginManager.getActiveLogin();
  193. if (!initialized) {
  194. filterEnabled = u.getSharedPreferences().getBoolean("filter", false);
  195. initialized = true;
  196. }
  197. if (mSwipeLayout == null) {
  198. mSwipeLayout = findViewById(R.id.swipe_layout);
  199. mSwipeLayout.setOnRefreshListener(this::recreate);
  200. }
  201. // Start loading
  202. new Thread(() -> getContent()).start();
  203. } else {
  204. // Login required
  205. Intent loginIntent = new Intent(MainActivity.this, LoginActivity.class);
  206. startActivityForResult(loginIntent, REQUEST_LOGIN);
  207. }
  208. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
  209. u.schedulePolling();
  210. }
  211. // Save version code for potential What's new stuff or migrations
  212. sharedPreferences.edit().putInt("version", BuildConfig.VERSION_CODE).apply();
  213. }
  214. private void openNoticeBoard() {
  215. Intent intent = new Intent(MainActivity.this, NoticeBoardActivity.class);
  216. if (mNoticeBoardItemList != null && !mNoticeBoardItemList.isEmpty())
  217. intent.putExtra(NoticeBoardActivity.EXTRA_NOTICE_BOARD_ITEMS, (Serializable) mNoticeBoardItemList);
  218. startActivity(intent);
  219. }
  220. private void networkErrorToUi(Exception e) {
  221. runOnUiThread(() -> {
  222. Snackbar
  223. .make(findViewById(R.id.root),
  224. getString(R.string.you_are_offline),
  225. Snackbar.LENGTH_SHORT)
  226. .show();
  227. // Enter offline mode
  228. offlineMode();
  229. });
  230. e.printStackTrace();
  231. }
  232. private void getContent() {
  233. try {
  234. mTables = mDownloadManager.downloadTables(login);
  235. // Start download of notices and news
  236. new Thread(() -> {
  237. try {
  238. mNoticeBoardItemList = mDownloadManager.downloadNoticeBoardItems(login);
  239. // Display notices button, even if no content otherwise
  240. if (mNoticeBoardItemList.size() > 0) {
  241. onMenuCreated.add(() -> mMenu.add(R.string.action_notices)
  242. .setIcon(R.drawable.ic_newspaper)
  243. .setOnMenuItemClickListener(item -> {
  244. openNoticeBoard();
  245. return true;
  246. }).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS));
  247. }
  248. } catch (IOException e) {
  249. runOnUiThread(() -> Snackbar
  250. .make(findViewById(R.id.contentCoordinator),
  251. R.string.network_request_notice_board_generic_error_snackbar,
  252. Snackbar.LENGTH_LONG)
  253. .show());
  254. e.printStackTrace();
  255. }
  256. }).start();
  257. // No content
  258. if (mTables.length == 0) {
  259. throw new NoContentException();
  260. }
  261. // Find out whether all tables might be parsable
  262. boolean findAllHtml = true;
  263. for (Table table : mTables) {
  264. if (!table.isHtml()) {
  265. findAllHtml = false;
  266. break;
  267. }
  268. }
  269. final boolean allHtml = findAllHtml;
  270. // It's not possible to merge if not all pages are html
  271. if (!allHtml) mMerge = false;
  272. runOnUiThread(() -> {
  273. mTextView.setText(getString(R.string.timetable_uri_acquired));
  274. // Find date
  275. mTimetabledate = mTables[0].getPublishedDate();
  276. if (mMerge && mParse && allHtml) {
  277. // All tables can be displayed at once
  278. displayMultipleHtmlTimetables(new ArrayList<>(Arrays.asList(mTables)));
  279. // Ensure pagePicker is gone
  280. findViewById(R.id.page).setVisibility(View.GONE);
  281. } else {
  282. // Every table has to be displayed separately
  283. // Display the first table
  284. displayTimetable(mTables[0]);
  285. // Let user view other tables if necessary
  286. if (mTables.length > 1) {
  287. HorizontalPicker pagePicker = findViewById(R.id.page);
  288. pagePicker.setVisibility(View.VISIBLE);
  289. // Display table titles as values in horizontal picker
  290. String[] titles = new String[mTables.length];
  291. for (int i = 0; i < mTables.length; i++) {
  292. titles[i] = mTables[i].getTitle();
  293. }
  294. pagePicker.setValues(titles);
  295. pagePicker.setOnItemSelectedListener(index -> displayTimetable(mTables[index]));
  296. pagePicker.setOnItemClickedListener(index ->
  297. displayTimetabledate(R.string.timetable_published, mTables[index].getPublishedDate())
  298. );
  299. }
  300. }
  301. });
  302. } catch (final UnexpectedResponseException e) {
  303. runOnUiThread(() -> {
  304. if (e instanceof LoginFailureException) {
  305. mTextView.setText(R.string.network_login_denied);
  306. final Button reauth = findViewById(R.id.reauthenticate);
  307. reauth.setVisibility(View.VISIBLE);
  308. reauth.setOnClickListener(v ->
  309. startActivityForResult(new Intent(MainActivity.this, LoginActivity.class), REQUEST_LOGIN)
  310. );
  311. } else if (e instanceof NoContentException) {
  312. mTextView.setText(R.string.network_no_content);
  313. // Don't display fix button
  314. return;
  315. } else {
  316. mTextView.setText(R.string.network_invalid_response);
  317. }
  318. // Display fix button
  319. final Button fix = findViewById(R.id.fix);
  320. fix.setVisibility(View.VISIBLE);
  321. fix.setOnClickListener(v -> {
  322. fix.setVisibility(View.GONE);
  323. new Thread(new NewsQuery(MainActivity.this, mDownloadManager)).start();
  324. });
  325. });
  326. e.printStackTrace();
  327. } catch (IOException e) {
  328. networkErrorToUi(e);
  329. }
  330. }
  331. /**
  332. * Displays timetable, no matter whether it has already been downloaded or not.
  333. * @param table The table to be displayed
  334. */
  335. private void displayTimetable(final Table table) {
  336. new Thread(() -> {
  337. try {
  338. if (table.isHtml()) {
  339. // Possibly init WebView early
  340. final SharedPreferences sharedPreferences = u.getSharedPreferences();
  341. if (sharedPreferences.getBoolean("renderWebViewEarly", false)) {
  342. // Since current thread is not on main thread, this is async
  343. runOnUiThread(() -> {
  344. Log.d("DRAW", "(possibly) initializing WebView while getting file");
  345. initWebView();
  346. // We don't know whether it should be rendered early next time again
  347. sharedPreferences
  348. .edit()
  349. .putBoolean("renderWebViewEarly", false)
  350. .apply();
  351. });
  352. }
  353. final String html = mFileManager.getHtmlTable(table, mDownloadManager);
  354. runOnUiThread(() -> displayHtml(html));
  355. } else {
  356. final Bitmap image = mFileManager.getImageTable(table, mDownloadManager);
  357. runOnUiThread(() -> displayImage(image));
  358. }
  359. } catch (final IOException e) {
  360. runOnUiThread(() -> networkErrorToUi(e));
  361. e.printStackTrace();
  362. }
  363. }).start();
  364. }
  365. /**
  366. * Displays file available offline already
  367. *
  368. * @param file File to be displayed
  369. * @param isHtml Whether the file is html
  370. */
  371. private void displayFile(File file, boolean isHtml) {
  372. if (isHtml) {
  373. String html = mFileManager.readHtmlFile(file);
  374. displayHtml(html);
  375. } else {
  376. Bitmap bitmap = mFileManager.readBitmapFile(file);
  377. displayImage(bitmap);
  378. }
  379. }
  380. private void displayHtml(final String response) {
  381. // In case an image had been shown previously, hide it
  382. ImageView image = findViewById(R.id.tableimage);
  383. image.setVisibility(View.GONE);
  384. if (mParse) {
  385. // Parse table
  386. // Create runnable
  387. final ReaderRunnable readerRunnable = new ReaderRunnable(MainActivity.this, response,
  388. login.getId(), mReaderTasks
  389. );
  390. readerRunnable.addHandlers(
  391. new Handler(msg -> {
  392. // Display parsed entries
  393. displayEntries(readerRunnable.getResult());
  394. String schoolName = readerRunnable.getSchoolName();
  395. if (schoolName != null) {
  396. // Display school name as window title if configured via preference, is debug build or multiple logins are configured
  397. if (u.getSharedPreferences().getBoolean(Utility.SUPER_SECRET_SETTING_FORCE_SCHOOL_NAME_AS_WINDOW_TITLE, getResources().getBoolean(R.bool.school_name_as_window_title_default))
  398. || mLoginManager.getLoginCount() > 1) {
  399. setTitle(schoolName);
  400. }
  401. // Set login display name to school name if doesn't already have one
  402. if (!mLoginManager.getActiveLogin().hasDisplayName()) {
  403. mLoginManager.getActiveLogin().setDisplayName(schoolName);
  404. mLoginManager.write();
  405. }
  406. }
  407. return false;
  408. }),
  409. new Handler(msg -> {
  410. // Display error message
  411. displayErrorEntry();
  412. return false;
  413. })
  414. );
  415. // Start and add thread
  416. mReaderTasks.add(readerRunnable);
  417. // If the task we just added is the only task queued
  418. if (mReaderTasks.size() == 1) {
  419. // Start this task in a new thread
  420. new Thread(readerRunnable).start();
  421. }
  422. } else {
  423. ((TextView) findViewById(R.id.date)).setText("");
  424. // Render HTML code in WebView
  425. initWebView();
  426. mWebView.setVisibility(View.VISIBLE);
  427. // Save that the WebView should be rendered early next time
  428. u.getSharedPreferences()
  429. .edit()
  430. .putBoolean("renderWebViewEarly", true)
  431. .apply();
  432. try {
  433. mWebView.loadDataWithBaseURL(null, response, "text/html", "UTF-8", null);
  434. } catch (NullPointerException e) {
  435. e.printStackTrace();
  436. mWebView.setVisibility(View.GONE);
  437. mTextView.setText(getString(R.string.error_displaying));
  438. mTextView.setVisibility(View.VISIBLE);
  439. }
  440. }
  441. potentiallyDisplayTimetabledate();
  442. }
  443. private void initWebView() {
  444. if (mWebView == null) {
  445. long webViewInitStart = System.currentTimeMillis();
  446. mWebView = new WebView(this);
  447. mWebView.setLayoutParams(new ViewGroup.LayoutParams(
  448. ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
  449. );
  450. ((ViewGroup) findViewById(R.id.nestedScroll)).addView(mWebView);
  451. WebSettings webSettings = mWebView.getSettings();
  452. webSettings.setBlockNetworkLoads(true);
  453. webSettings.setJavaScriptEnabled(false);
  454. mWebView.setVisibility(View.GONE);
  455. Log.d("DRAW", "initialized WebView within " + (System.currentTimeMillis() - webViewInitStart)
  456. + " milliseconds");
  457. }
  458. }
  459. private void displayErrorEntry() {
  460. ArrayList<Entry> error = new ArrayList<>();
  461. error.add(new ErrorEntry(this));
  462. displayEntries(error);
  463. }
  464. private void displayEntries(final ArrayList<Entry> entries) {
  465. RecyclerView recyclerView = findViewById(R.id.table);
  466. if (recyclerView.getLayoutManager() == null) {
  467. recyclerView.setLayoutManager(new LinearLayoutManager(this));
  468. if (u.getSharedPreferences().getString("layout", Adapter.LAYOUT_CARDS)
  469. .equals(Adapter.LAYOUT_LIST))
  470. // Add divider
  471. recyclerView.addItemDecoration(
  472. new DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
  473. );
  474. else
  475. recyclerView.setPadding(0, 0, 0, u.dpToPx(4));
  476. recyclerView.setItemViewCacheSize(20);
  477. }
  478. if (mAdapter == null || !mMerge) {
  479. mAdapter = new Adapter(MainActivity.this, entries);
  480. mAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY);
  481. ((TextView) findViewById(R.id.date)).setText("");
  482. recyclerView.setAdapter(mAdapter);
  483. } else {
  484. mAdapter.addAll(entries);
  485. }
  486. recyclerView.setVisibility(View.VISIBLE);
  487. if (mAdapter.getItemCount() > 0) {
  488. mTextView.setVisibility(View.GONE);
  489. final TextView dateView = findViewById(R.id.date);
  490. dateView.setVisibility(View.VISIBLE);
  491. recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
  492. @Override
  493. public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
  494. try {
  495. int firstVisibleItem =
  496. ((LinearLayoutManager) recyclerView.getLayoutManager())
  497. .findFirstVisibleItemPosition();
  498. dateView.setText(u.formatDate(mAdapter.get(firstVisibleItem).getDate()));
  499. } catch (IndexOutOfBoundsException | NullPointerException e) {
  500. // does not matter because this happens when list is empty
  501. }
  502. }
  503. });
  504. } else {
  505. mTextView.setText(R.string.empty);
  506. mTextView.setVisibility(View.VISIBLE);
  507. recyclerView.setVisibility(View.GONE);
  508. }
  509. }
  510. /**
  511. * Display multiple html tables at once (concatenate them)
  512. *
  513. * @param tables Tables to be potentially downloaded and displayed
  514. */
  515. private void displayMultipleHtmlTimetables(final ArrayList<Table> tables) {
  516. new Thread(() -> {
  517. final Table table = tables.get(0);
  518. tables.remove(table);
  519. try {
  520. final String html = mFileManager.getHtmlTable(table, mDownloadManager);
  521. runOnUiThread(() -> {
  522. displayHtml(html);
  523. // Display remaining tables
  524. // This has to be done after the previous entries have been parsed to not mix up the order
  525. if (tables.size() > 0) {
  526. // To understand recursion, you first have to understand recursion
  527. displayMultipleHtmlTimetables(tables);
  528. }
  529. });
  530. } catch (IOException e) {
  531. // Missing just a part!
  532. runOnUiThread(this::displayErrorEntry);
  533. e.printStackTrace();
  534. }
  535. }).start();
  536. }
  537. private void displayImage(Bitmap bitmap) {
  538. // Hide text
  539. mTextView.setVisibility(View.GONE);
  540. // Hide list, date text and webView in case they had previously been displayed
  541. RecyclerView recyclerView = findViewById(R.id.table);
  542. TextView dateText = findViewById(R.id.date);
  543. dateText.setText("");
  544. dateText.setVisibility(View.GONE);
  545. recyclerView.setVisibility(View.GONE);
  546. if (mWebView != null) {
  547. mWebView.setVisibility(View.GONE);
  548. }
  549. // Zoom out
  550. TouchImageView image = findViewById(R.id.tableimage);
  551. image.resetZoom();
  552. BitmapDrawable drawable = new BitmapDrawable(bitmap);
  553. // Invert colors
  554. if (u.getSharedPreferences().getBoolean("invertImages", false)) {
  555. // Thanks, https://stackoverflow.com/a/17871384
  556. final float[] NEGATIVE = {
  557. -1.0f, 0, 0, 0, 255, // red
  558. 0, -1.0f, 0, 0, 255, // green
  559. 0, 0, -1.0f, 0, 255, // blue
  560. 0, 0, 0, 1.0f, 0 // alpha
  561. };
  562. drawable.setColorFilter(new ColorMatrixColorFilter(NEGATIVE));
  563. }
  564. // Show image
  565. image.setImageDrawable(drawable);
  566. image.setVisibility(View.VISIBLE);
  567. potentiallyDisplayTimetabledate();
  568. }
  569. private void offlineMode() {
  570. // Change title to notify user offline mode is enabled
  571. getSupportActionBar().setTitle(R.string.app_name_offline);
  572. // Check checkbox
  573. onMenuCreated.add(() -> {
  574. mMenu.findItem(R.id.action_view_history).setChecked(true);
  575. setTempFilterIcon(mMenu);
  576. });
  577. // Don't merge
  578. mMerge = false;
  579. // Find every file, sorted descending by last modified
  580. final File[] files = mFileManager.getFilesSorted().toArray(new File[0]);
  581. if (files.length == 0) {
  582. // We're done
  583. return;
  584. }
  585. // Read every file to a Table
  586. mTables = new Table[files.length];
  587. for (int i = 0; i < files.length; i++) {
  588. File file = files[i];
  589. String url = file.getName().replaceAll("\\d+-", "");
  590. Date date;
  591. // Who knows what files might be in our file system
  592. try {
  593. String time = file.getName().split("-")[0];
  594. date = new Date(Long.parseLong(time));
  595. } catch (NumberFormatException e) {
  596. e.printStackTrace();
  597. date = new Date();
  598. }
  599. boolean isHtml;
  600. try {
  601. String[] fileNameParts = file.getName().split("\\.");
  602. String suffix = fileNameParts[fileNameParts.length - 1];
  603. isHtml = suffix.contains("htm");
  604. } catch (ArrayIndexOutOfBoundsException e) {
  605. // In this case, there was no dot in the filename
  606. isHtml = false;
  607. }
  608. mTables[i] = new Table(url, date, isHtml, Humanize.naturalTime(date));
  609. }
  610. // Don't do it. It's dumb
  611. //mTimetabledate = mTables[0].getPublishedDate();
  612. // Display the first table
  613. HorizontalPicker pagePicker = findViewById(R.id.page);
  614. pagePicker.setSelectedItem(0);
  615. if(offlineFilterPage != 0) {
  616. displayFile(files[offlineFilterPage], mTables[offlineFilterPage].isHtml());
  617. displayTimetabledate(R.string.timetable_published, mTables[offlineFilterPage].getPublishedDate());
  618. pagePicker.setSelectedItem(offlineFilterPage);
  619. }else
  620. displayFile(files[0], mTables[0].isHtml());
  621. // Let user view other files
  622. if (mTables.length > 1) {
  623. pagePicker.setVisibility(View.VISIBLE);
  624. // Initialize CharSequence[] for setting them as values
  625. CharSequence[] values = new CharSequence[mTables.length];
  626. for (int i = 0; i < mTables.length; i++) {
  627. values[i] = Humanize.naturalTime(mTables[i].getPublishedDate());
  628. }
  629. pagePicker.setValues(values);
  630. pagePicker.setOnItemSelectedListener(index -> {
  631. displayFile(files[index], mTables[index].isHtml());
  632. offlineFilterPage = index;
  633. });
  634. pagePicker.setOnItemClickedListener(index ->
  635. displayTimetabledate(R.string.timetable_published, mTables[index].getPublishedDate())
  636. );
  637. }
  638. }
  639. private void potentiallyDisplayTimetabledate() {
  640. // Only do this once
  641. if (!mTimetabledateDisplayed && mTimetabledate != null) {
  642. displayTimetabledate(R.string.timetable_last_changed, mTimetabledate);
  643. mTimetabledateDisplayed = true;
  644. }
  645. }
  646. private void displayTimetabledate(@StringRes int message, Date mTimetabledate) {
  647. String ago = Humanize.naturalTime(mTimetabledate, TimeMillis.HOUR);
  648. if (ago.isEmpty()) {
  649. ago = Humanize.naturalTime(mTimetabledate, TimeMillis.SECOND);
  650. }
  651. if (System.currentTimeMillis() - mTimetabledate.getTime() < 60 * 1000 || ago.isEmpty()) {
  652. ago = getString(R.string.timetable_updated_now);
  653. }
  654. Log.d("TIMETABLEDATE", ago);
  655. Snackbar
  656. .make(findViewById(R.id.contentCoordinator),
  657. getString(message, ago),
  658. Snackbar.LENGTH_LONG)
  659. .show();
  660. }
  661. @Override
  662. public boolean onCreateOptionsMenu(Menu menu) {
  663. getMenuInflater().inflate(R.menu.menu, menu);
  664. if (getResources().getBoolean(R.bool.news_check_button)) {
  665. menu.add(Menu.NONE, 4, Menu.NONE, R.string.action_check_news);
  666. }
  667. // Add all inactive logins as a submenu if there is one
  668. if (mLoginManager.getLoginCount() > 1) {
  669. SubMenu loginMenu = menu.addSubMenu(R.string.action_switch_login);
  670. for (Login l :
  671. mLoginManager.getInactiveLogins()) {
  672. loginMenu.add(Menu.NONE, Integer.parseInt(l.getId()), Menu.NONE, l.getDisplayName());
  673. }
  674. loginMenu.add(Menu.NONE, 5, Menu.NONE, R.string.action_add_login).setIcon(R.drawable.ic_add_black_24dp);
  675. }
  676. setTempFilterIcon(menu);
  677. // Pass it around!
  678. mMenu = menu;
  679. // Start thread which executes onMenuCreated queue
  680. menuOperationsThread = new Thread(() -> {
  681. try {
  682. for (;;) {
  683. Runnable r = onMenuCreated.take();
  684. runOnUiThread(r);
  685. }
  686. } catch (InterruptedException e) {
  687. e.printStackTrace();
  688. }
  689. });
  690. menuOperationsThread.start();
  691. return super.onCreateOptionsMenu(menu);
  692. }
  693. @Override
  694. public boolean onOptionsItemSelected(MenuItem item) {
  695. int itemId = item.getItemId();
  696. if (itemId == R.id.action_reload) {
  697. recreate();
  698. } else if (itemId == R.id.action_temp_filters) {//enable/disable filters for current session
  699. filterEnabled = !filterEnabled;
  700. if (mMenu.findItem(R.id.action_view_history).isChecked())
  701. offlineMode();
  702. else
  703. recreate();
  704. } else if (itemId == R.id.action_view_history) {
  705. if (item.isChecked()) {
  706. offlineFilterPage = 0;
  707. recreate();
  708. } else {
  709. item.setChecked(true);
  710. offlineMode();
  711. }
  712. } else if (itemId == R.id.action_settings) {
  713. Intent settingsIntent = new Intent(MainActivity.this, SettingsActivity.class);
  714. // Pass along whether plan contains html files and whether plan only consists of html files
  715. if (mTables != null) {
  716. // At least one file must be html
  717. boolean containsHtml = false;
  718. boolean allHtml = true;
  719. for (Table table : mTables) {
  720. if (table.isHtml()) {
  721. // This table is html, therefore the plan contains html
  722. containsHtml = true;
  723. } else {
  724. // This table is not html, therefore it can't be that all pages are html
  725. allHtml = false;
  726. }
  727. }
  728. settingsIntent.putExtra(SettingsActivity.EXTRA_CONTAINS_HTML, containsHtml);
  729. settingsIntent.putExtra(SettingsActivity.EXTRA_HTML_ONLY, allHtml);
  730. }
  731. startActivityForResult(settingsIntent,
  732. SettingsActivity.class.getName().length() // I needed a constant number, so I chose something simple & easy to remember
  733. );
  734. } else if (itemId == 5) {
  735. startActivityForResult(new Intent(MainActivity.this, LoginActivity.class), REQUEST_LOGIN);
  736. } else if (itemId == 4) {
  737. new Thread(new NewsQuery(MainActivity.this, mDownloadManager)).start();
  738. } else {
  739. // Find out whether this id fits any login id
  740. for (Login l :
  741. mLoginManager.getLogins()) {
  742. if (item.getItemId() == Integer.parseInt(l.getId())) {
  743. Log.d("LOGINSWITCH", "to login " + l.getDisplayName());
  744. mLoginManager.setActiveLogin(l);
  745. recreate();
  746. return true;
  747. }
  748. }
  749. return super.onOptionsItemSelected(item);
  750. }
  751. return true;
  752. }
  753. @Override
  754. public void recreate() {
  755. super.recreate();
  756. // Reset pagePicker position
  757. ((HorizontalPicker) findViewById(R.id.page)).setSelectedItem(0);
  758. if (mSwipeLayout != null) {
  759. mSwipeLayout.setRefreshing(false);
  760. }
  761. }
  762. @Override
  763. protected void onNewIntent(Intent intent) {
  764. super.onNewIntent(intent);
  765. recreate();
  766. }
  767. @Override
  768. protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  769. //int settingsActivityRequestCode = SettingsActivity.class.getName().length(); // A way to get a cool number
  770. // Whatever request this is happened, we want to reload if it was successful
  771. if (resultCode == RESULT_OK) {
  772. recreate(); // Because we can't really refresh very well
  773. } else if (resultCode == RESULT_CANCELED && requestCode == REQUEST_LOGIN) {
  774. // Login was cancelled, quit if not debug and there are no logins
  775. if (getResources().getBoolean(R.bool.authenticate_quit_on_cancel) && mLoginManager.getLoginCount() < 1)
  776. finish();
  777. }
  778. super.onActivityResult(requestCode, resultCode, data);
  779. }
  780. private void setTempFilterIcon(@NonNull Menu menu){
  781. if(u.getSharedPreferences().getBoolean("filter", false)&&u.getSharedPreferences().getBoolean("parse", false)){
  782. if(filterEnabled){
  783. menu.findItem(R.id.action_temp_filters).setIcon(R.drawable.ic_visibility_24px);
  784. menu.findItem(R.id.action_temp_filters).setTitle(R.string.action_temp_filters_disable);
  785. } else{
  786. menu.findItem(R.id.action_temp_filters).setIcon(R.drawable.ic_visibility_off_24px);
  787. menu.findItem(R.id.action_temp_filters).setTitle(R.string.action_temp_filters_enable);
  788. }
  789. menu.findItem(R.id.action_temp_filters).setVisible(true);
  790. }
  791. }
  792. @Override
  793. protected void onDestroy() {
  794. if (menuOperationsThread != null) {
  795. menuOperationsThread.interrupt();
  796. }
  797. super.onDestroy();
  798. }
  799. }