DashActivity.kt 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. package ml.adamsprogs.bimba.activities
  2. import android.annotation.SuppressLint
  3. import android.app.Activity
  4. import android.content.Context
  5. import android.content.DialogInterface
  6. import android.content.Intent
  7. import android.content.IntentFilter
  8. import android.os.Bundle
  9. import android.preference.PreferenceManager.getDefaultSharedPreferences
  10. import android.text.Editable
  11. import android.text.TextWatcher
  12. import android.view.ActionMode
  13. import android.view.Menu
  14. import android.view.MenuItem
  15. import android.view.View
  16. import android.view.inputmethod.InputMethodManager
  17. import androidx.appcompat.app.AlertDialog
  18. import androidx.appcompat.app.AppCompatActivity
  19. import com.google.android.material.navigation.NavigationView
  20. import com.google.android.material.snackbar.Snackbar
  21. import com.mancj.materialsearchbar.MaterialSearchBar
  22. import com.mancj.materialsearchbar.MaterialSearchBar.BUTTON_BACK
  23. import com.mancj.materialsearchbar.MaterialSearchBar.BUTTON_NAVIGATION
  24. import kotlinx.android.synthetic.main.activity_dash.*
  25. import ml.adamsprogs.bimba.*
  26. import ml.adamsprogs.bimba.collections.FavouriteStorage
  27. import ml.adamsprogs.bimba.datasources.TimetableDownloader
  28. import ml.adamsprogs.bimba.datasources.VmService
  29. import ml.adamsprogs.bimba.models.Departure
  30. import ml.adamsprogs.bimba.models.Plate
  31. import ml.adamsprogs.bimba.models.Timetable
  32. import ml.adamsprogs.bimba.models.adapters.FavouritesAdapter
  33. import ml.adamsprogs.bimba.models.adapters.SuggestionsAdapter
  34. import ml.adamsprogs.bimba.models.suggestions.EmptySuggestion
  35. import ml.adamsprogs.bimba.models.suggestions.GtfsSuggestion
  36. import ml.adamsprogs.bimba.models.suggestions.LineSuggestion
  37. import ml.adamsprogs.bimba.models.suggestions.StopSuggestion
  38. import java.text.DateFormat
  39. import java.util.*
  40. import kotlin.collections.ArrayList
  41. class DashActivity : AppCompatActivity(), MessageReceiver.OnTimetableDownloadListener,
  42. FavouritesAdapter.OnMenuItemClickListener, FavouritesAdapter.ViewHolder.OnClickListener, ProviderProxy.OnDeparturesReadyListener, SuggestionsAdapter.OnSuggestionClickListener {
  43. val context: Context = this
  44. private val receiver = MessageReceiver.getMessageReceiver()
  45. private lateinit var timetable: Timetable
  46. private var suggestions: List<GtfsSuggestion>? = null
  47. private lateinit var drawerLayout: androidx.drawerlayout.widget.DrawerLayout
  48. private lateinit var drawerView: NavigationView
  49. lateinit var favouritesList: androidx.recyclerview.widget.RecyclerView
  50. lateinit var searchView: MaterialSearchBar
  51. private lateinit var favourites: FavouriteStorage
  52. private lateinit var adapter: FavouritesAdapter
  53. private val actionModeCallback = ActionModeCallback()
  54. private var actionMode: ActionMode? = null
  55. private var isWarned = false
  56. private lateinit var providerProxy: ProviderProxy
  57. private lateinit var suggestionsAdapter: SuggestionsAdapter
  58. companion object {
  59. const val REQUEST_EDIT_FAVOURITE = 1
  60. }
  61. override fun onCreate(savedInstanceState: Bundle?) {
  62. super.onCreate(savedInstanceState)
  63. setContentView(R.layout.activity_dash)
  64. setSupportActionBar(toolbar)
  65. providerProxy = ProviderProxy(this)
  66. timetable = Timetable.getTimetable()
  67. NetworkStateReceiver.init(this)
  68. prepareFavourites()
  69. prepareListeners()
  70. startDownloaderService()
  71. drawerLayout = drawer_layout
  72. drawerView = drawer
  73. //drawer.setCheckedItem(R.id.drawer_home)
  74. drawerView.setNavigationItemSelectedListener { item ->
  75. when (item.itemId) {
  76. R.id.drawer_refresh -> {
  77. startDownloaderService(true)
  78. }
  79. R.id.drawer_settings -> {
  80. startActivity(Intent(context, SettingsActivity::class.java))
  81. }
  82. else -> {
  83. }
  84. }
  85. drawerLayout.closeDrawer(drawerView)
  86. super.onOptionsItemSelected(item)
  87. }
  88. warnTimetableValidity()
  89. showValidityInDrawer()
  90. searchView = search_view
  91. suggestionsAdapter = SuggestionsAdapter(layoutInflater, this, this)
  92. searchView.setCustomSuggestionAdapter(suggestionsAdapter)
  93. searchView.addTextChangeListener(object : TextWatcher {
  94. override fun afterTextChanged(s: Editable?) {
  95. if (searchView.isSearchEnabled) {
  96. getSuggestions(s.toString())
  97. }
  98. }
  99. override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
  100. }
  101. override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
  102. }
  103. })
  104. searchView.setOnSearchActionListener(object : MaterialSearchBar.OnSearchActionListener {
  105. override fun onButtonClicked(buttonCode: Int) {
  106. when (buttonCode) {
  107. BUTTON_NAVIGATION -> {
  108. if (drawerLayout.isDrawerOpen(drawerView))
  109. drawerLayout.closeDrawer(drawerView)
  110. else
  111. drawerLayout.openDrawer(drawerView)
  112. }
  113. BUTTON_BACK -> {
  114. searchView.disableSearch()
  115. }
  116. }
  117. }
  118. override fun onSearchStateChanged(enabled: Boolean) {
  119. }
  120. override fun onSearchConfirmed(text: CharSequence?) {
  121. getSuggestions(text as String)
  122. }
  123. })
  124. }
  125. private fun getSuggestions(query: String = "") {
  126. providerProxy.getSuggestions(query) { suggestions ->
  127. if (!suggestionsAdapter.equals(suggestions)) {
  128. if (suggestions.isEmpty()) {
  129. suggestionsAdapter.clearSuggestions()
  130. suggestionsAdapter.addSuggestion(EmptySuggestion())
  131. } else {
  132. suggestionsAdapter.updateSuggestions(suggestions)
  133. }
  134. searchView.showSuggestionsList()
  135. }
  136. }
  137. }
  138. override fun onSuggestionClickListener(suggestion: GtfsSuggestion) {
  139. val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
  140. var view = (context as DashActivity).currentFocus
  141. if (view == null) {
  142. view = View(context)
  143. }
  144. imm.hideSoftInputFromWindow(view.windowToken, 0)
  145. if (suggestion is StopSuggestion) {
  146. val intent = Intent(context, StopSpecifyActivity::class.java)
  147. intent.putExtra(StopSpecifyActivity.EXTRA_STOP_NAME, suggestion.name)
  148. startActivity(intent)
  149. } else if (suggestion is LineSuggestion) {
  150. val intent = Intent(context, LineSpecifyActivity::class.java)
  151. intent.putExtra(LineSpecifyActivity.EXTRA_LINE_ID, suggestion.name)
  152. startActivity(intent)
  153. }
  154. }
  155. override fun onRestart() {
  156. super.onRestart()
  157. favourites = FavouriteStorage.getFavouriteStorage(context)
  158. favourites.forEach {
  159. it.subscribeForDepartures(this, this)
  160. }
  161. }
  162. override fun onStop() {
  163. super.onStop()
  164. favourites.forEach {
  165. it.unsubscribeFromDepartures(this)
  166. }
  167. }
  168. private fun showValidityInDrawer() {
  169. if (timetable.isEmpty()) {
  170. drawerView.menu.findItem(R.id.drawer_validity_since).title = getString(R.string.validity_offline_unavailable)
  171. } else {
  172. val formatter = DateFormat.getDateInstance(DateFormat.SHORT)
  173. var calendar = calendarFromIsoD(timetable.getValidSince())
  174. formatter.timeZone = calendar.timeZone
  175. drawerView.menu.findItem(R.id.drawer_validity_since).title = getString(R.string.valid_since, formatter.format(calendar.time))
  176. calendar = calendarFromIsoD(timetable.getValidTill())
  177. formatter.timeZone = calendar.timeZone
  178. drawerView.menu.findItem(R.id.drawer_validity_till).title = getString(R.string.valid_till, formatter.format(calendar.time))
  179. }
  180. }
  181. private fun warnTimetableValidity() {
  182. if (isWarned)
  183. return
  184. isWarned = true
  185. if (timetable.isEmpty())
  186. return
  187. val validTill = timetable.getValidTill()
  188. val today = Calendar.getInstance().toIsoDate()
  189. val tomorrow = Calendar.getInstance().apply {
  190. this.add(Calendar.DAY_OF_MONTH, 1)
  191. }.toIsoDate()
  192. try {
  193. timetable.getServiceForToday()
  194. if (today > validTill) {
  195. notifyTimetableValidity(-1)
  196. suggestions = ArrayList()
  197. return
  198. }
  199. if (today == validTill) {
  200. notifyTimetableValidity(0)
  201. return
  202. }
  203. } catch (e: IllegalArgumentException) {
  204. notifyTimetableValidity(-1)
  205. suggestions = ArrayList()
  206. return
  207. }
  208. try {
  209. timetable.getServiceForTomorrow()
  210. if (tomorrow == validTill) {
  211. notifyTimetableValidity(1)
  212. return
  213. }
  214. } catch (e: IllegalArgumentException) {
  215. notifyTimetableValidity(1)
  216. return
  217. }
  218. }
  219. private fun notifyTimetableValidity(daysTillInvalid: Int) {
  220. val message = when (daysTillInvalid) {
  221. -1 -> getString(R.string.timetable_validity_finished)
  222. 0 -> getString(R.string.timetable_validity_today)
  223. 1 -> getString(R.string.timetable_validity_tomorrow)
  224. else -> return
  225. }
  226. AlertDialog.Builder(context)
  227. .setPositiveButton(context.getText(android.R.string.ok)
  228. ) { dialog: DialogInterface, _: Int -> dialog.cancel() }
  229. .setCancelable(true)
  230. .setMessage(message)
  231. .create().show()
  232. if (daysTillInvalid == -1) {
  233. Timetable.delete(this)
  234. }
  235. }
  236. private fun prepareFavourites() {
  237. favourites = FavouriteStorage.getFavouriteStorage(context)
  238. favourites.forEach {
  239. it.subscribeForDepartures(this, this)
  240. }
  241. val layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context)
  242. favouritesList = favourites_list
  243. adapter = FavouritesAdapter(context, favourites, this, this)
  244. favouritesList.adapter = adapter
  245. favouritesList.itemAnimator = androidx.recyclerview.widget.DefaultItemAnimator()
  246. favouritesList.layoutManager = layoutManager
  247. }
  248. override fun onDeparturesReady(departures: List<Departure>, plateId: Plate.ID?, code: Int) {
  249. favouritesList.adapter!!.notifyDataSetChanged()
  250. showError(drawer_layout, code, this)
  251. }
  252. private fun prepareListeners() {
  253. val filter = IntentFilter(TimetableDownloader.ACTION_DOWNLOADED)
  254. filter.addAction(VmService.ACTION_READY)
  255. filter.addCategory(Intent.CATEGORY_DEFAULT)
  256. registerReceiver(receiver, filter)
  257. receiver.addOnTimetableDownloadListener(context as MessageReceiver.OnTimetableDownloadListener)
  258. }
  259. private fun startDownloaderService(force: Boolean = false) {
  260. if (getDefaultSharedPreferences(this).getBoolean(getString(R.string.key_timetable_automatic_update), false) or force)
  261. startService(Intent(context, TimetableDownloader::class.java))
  262. }
  263. override fun onBackPressed() {
  264. if (drawerLayout.isDrawerOpen(drawerView)) {
  265. drawerLayout.closeDrawer(drawerView)
  266. return
  267. }
  268. if (searchView.isSearchEnabled) {
  269. searchView.disableSearch()
  270. } else {
  271. super.onBackPressed()
  272. }
  273. }
  274. override fun onResume() {
  275. super.onResume()
  276. adapter.favourites = favourites
  277. favouritesList.adapter!!.notifyDataSetChanged()
  278. }
  279. override fun onDestroy() {
  280. super.onDestroy()
  281. receiver.removeOnTimetableDownloadListener(context as MessageReceiver.OnTimetableDownloadListener)
  282. unregisterReceiver(receiver)
  283. }
  284. override fun onTimetableDownload(result: String?) {
  285. val message: String = when (result) {
  286. TimetableDownloader.RESULT_NO_CONNECTIVITY -> getString(R.string.no_connectivity_cant_update)
  287. TimetableDownloader.RESULT_UP_TO_DATE -> getString(R.string.timetable_up_to_date)
  288. TimetableDownloader.RESULT_FINISHED -> getString(R.string.timetable_downloaded)
  289. else -> getString(R.string.error_try_later)
  290. }
  291. Snackbar.make(findViewById(R.id.drawer_layout), message, Snackbar.LENGTH_LONG).show()
  292. if (result == TimetableDownloader.RESULT_FINISHED) {
  293. timetable = Timetable.getTimetable(this, true)
  294. getSuggestions(searchView.text)
  295. showValidityInDrawer()
  296. }
  297. }
  298. override fun edit(name: String): Boolean {
  299. val positionBefore = favourites.indexOf(name)
  300. val intent = Intent(this, EditFavouriteActivity::class.java)
  301. intent.putExtra(EditFavouriteActivity.EXTRA_FAVOURITE, favourites[name])
  302. intent.putExtra(EditFavouriteActivity.EXTRA_POSITION_BEFORE, positionBefore)
  303. startActivityForResult(intent, REQUEST_EDIT_FAVOURITE)
  304. return true
  305. }
  306. override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
  307. if (requestCode == REQUEST_EDIT_FAVOURITE) {
  308. if (resultCode == Activity.RESULT_OK) {
  309. val name = data!!.getStringExtra(EditFavouriteActivity.EXTRA_NEW_NAME)
  310. val positionBefore = data.getIntExtra(EditFavouriteActivity.EXTRA_POSITION_BEFORE, -1)
  311. //adapter.favourites = favourites.favouritesList
  312. if (positionBefore == -1)
  313. favouritesList.adapter!!.notifyDataSetChanged()
  314. else {
  315. val positionAfter = favourites.indexOf(name)
  316. favouritesList.adapter!!.notifyItemChanged(positionBefore)
  317. favouritesList.adapter!!.notifyItemMoved(positionBefore, positionAfter)
  318. }
  319. adapter[name]?.let {
  320. it.unsubscribeFromDepartures(context)
  321. it.subscribeForDepartures(this, context)
  322. }
  323. }
  324. }
  325. }
  326. override fun delete(name: String): Boolean {
  327. favourites.delete(name)
  328. //adapter.favourites = favourites.favouritesList
  329. favouritesList.adapter!!.notifyItemRemoved(favourites.indexOf(name))
  330. return true
  331. }
  332. @SuppressLint("MissingSuperCall")
  333. override fun onSaveInstanceState(outState: Bundle) {
  334. //hack below line to be commented to prevent crash on nougat.
  335. //super.onSaveInstanceState(outState);
  336. }
  337. override fun onItemClicked(position: Int) {
  338. if (actionMode != null) {
  339. toggleSelection(position)
  340. } else {
  341. val intent = Intent(context, StopActivity::class.java)
  342. intent.putExtra(StopActivity.SOURCE_TYPE, StopActivity.SOURCE_TYPE_FAV)
  343. intent.putExtra(StopActivity.EXTRA_FAVOURITE, favourites[position])
  344. startActivity(intent)
  345. }
  346. }
  347. override fun onItemLongClicked(position: Int): Boolean {
  348. if (actionMode == null) {
  349. actionMode = startActionMode(actionModeCallback)
  350. }
  351. toggleSelection(position)
  352. return true
  353. }
  354. private fun toggleSelection(position: Int) {
  355. adapter.toggleSelection(position)
  356. val count = adapter.getSelectedItemCount()
  357. if (count == 0) {
  358. actionMode?.finish()
  359. } else {
  360. actionMode?.title = getString(R.string.merge_favourites)
  361. actionMode?.invalidate()
  362. }
  363. }
  364. private fun clearSelection() {
  365. adapter.clearSelection()
  366. actionMode?.finish()
  367. }
  368. private inner class ActionModeCallback : ActionMode.Callback {
  369. override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
  370. menuInflater.inflate(R.menu.menu_favourite_merge, menu)
  371. return true
  372. }
  373. override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
  374. return false
  375. }
  376. override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
  377. return when (item.itemId) {
  378. R.id.action_merge -> {
  379. val selectedPositions = adapter.getSelectedItems()
  380. val selectedNames = selectedPositions.map { favourites[it]?.name }.filter { it != null }.map { it!! }
  381. (1 until selectedNames.size).forEach {
  382. selectedNames[it].let { name ->
  383. adapter.notifyItemRemoved(adapter.indexOf(name))
  384. adapter[name]?.unsubscribeFromDepartures(context)
  385. }
  386. }
  387. favourites.merge(selectedNames, context)
  388. adapter[selectedNames[0]]?.let {
  389. it.unsubscribeFromDepartures(context)
  390. it.subscribeForDepartures(this@DashActivity, context)
  391. }
  392. adapter.notifyItemChanged(adapter.indexOf(selectedNames[0]))
  393. clearSelection()
  394. true
  395. }
  396. else -> false
  397. }
  398. }
  399. override fun onDestroyActionMode(mode: ActionMode) {
  400. (favouritesList.adapter as FavouritesAdapter).clearSelection()
  401. actionMode = null
  402. }
  403. }
  404. }