diff --git a/app/src/main/java/com/rtbishop/look4sat/data/database/SatelliteDao.kt b/app/src/main/java/com/rtbishop/look4sat/data/database/SatelliteDao.kt index acc2e52d..38e30345 100644 --- a/app/src/main/java/com/rtbishop/look4sat/data/database/SatelliteDao.kt +++ b/app/src/main/java/com/rtbishop/look4sat/data/database/SatelliteDao.kt @@ -33,10 +33,7 @@ interface SatelliteDao { fun getAllSatItems(): Flow> @Query("SELECT tle FROM entries WHERE isSelected = 1") - suspend fun getSelectedSatellites(): List - - @Query("SELECT catNum FROM entries WHERE isSelected = 1") - suspend fun getSelectedCatNums(): List + fun getSelectedSatellitesFlow(): Flow> @Query("SELECT * FROM transmitters WHERE isAlive = 1 ORDER BY mode ASC") fun getAllTransmitters(): Flow> @@ -44,6 +41,12 @@ interface SatelliteDao { @Query("SELECT * FROM transmitters WHERE isAlive = 1 and catNum = :catNum") fun getTransmittersByCatNum(catNum: Int): Flow> + @Query("SELECT tle FROM entries WHERE isSelected = 1") + suspend fun getSelectedSatellites(): List + + @Query("SELECT catNum FROM entries WHERE isSelected = 1") + suspend fun getSelectedCatNums(): List + // Insert @Insert(onConflict = OnConflictStrategy.REPLACE) @@ -54,31 +57,31 @@ interface SatelliteDao { // Update - @Query("UPDATE entries SET isSelected = 0") - suspend fun clearSelection() - - @Query("UPDATE entries SET isSelected = :isSelected WHERE catNum = :catNum") - suspend fun updateSelectionByCatNum(catNum: Int, isSelected: Boolean) - @Transaction - suspend fun updateSelection(catNums: List) { - clearSelection() + suspend fun updateEntriesSelection(catNums: List, isSelected: Boolean) { + clearEntriesSelection() catNums.forEach { catNum -> - updateSelectionByCatNum(catNum, true) + updateItemSelection(catNum, true) } } + @Query("UPDATE entries SET isSelected = 0") + suspend fun clearEntriesSelection() + + @Query("UPDATE entries SET isSelected = :isSelected WHERE catNum = :catNum") + suspend fun updateItemSelection(catNum: Int, isSelected: Boolean) + // Delete - @Query("DELETE from entries") - suspend fun deleteEntries() - - @Query("DELETE from transmitters") - suspend fun deleteTransmitters() - @Transaction suspend fun deleteAllData() { deleteEntries() deleteTransmitters() } + + @Query("DELETE from entries") + suspend fun deleteEntries() + + @Query("DELETE from transmitters") + suspend fun deleteTransmitters() } diff --git a/app/src/main/java/com/rtbishop/look4sat/data/repository/SatelliteRepo.kt b/app/src/main/java/com/rtbishop/look4sat/data/repository/SatelliteRepo.kt index 592c7058..eb3672ce 100644 --- a/app/src/main/java/com/rtbishop/look4sat/data/repository/SatelliteRepo.kt +++ b/app/src/main/java/com/rtbishop/look4sat/data/repository/SatelliteRepo.kt @@ -21,12 +21,13 @@ import android.content.ContentResolver import android.net.Uri import com.github.amsacode.predict4java.Satellite import com.github.amsacode.predict4java.TLE +import com.rtbishop.look4sat.data.api.SatelliteService +import com.rtbishop.look4sat.data.database.SatelliteDao import com.rtbishop.look4sat.data.model.SatEntry +import com.rtbishop.look4sat.data.model.SatItem import com.rtbishop.look4sat.data.model.SatTrans import com.rtbishop.look4sat.data.model.TleSource import com.rtbishop.look4sat.di.IoDispatcher -import com.rtbishop.look4sat.data.database.SatelliteDao -import com.rtbishop.look4sat.data.api.SatelliteService import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow @@ -45,9 +46,21 @@ class SatelliteRepo @Inject constructor( @IoDispatcher private val ioDispatcher: CoroutineDispatcher ) { - val satDataFlow = satelliteDao.getAllSatItems() + // Query - fun getTransmittersForSat(catNum: Int): Flow> { + fun getAllSatItems(): Flow> { + return satelliteDao.getAllSatItems() + } + + fun getSelectedSatellitesFlow(): Flow> { + return satelliteDao.getSelectedSatellitesFlow() + } + + fun getAllTransmitters(): Flow> { + return satelliteDao.getAllTransmitters() + } + + fun getSatTransmitters(catNum: Int): Flow> { return satelliteDao.getTransmittersByCatNum(catNum) } @@ -55,11 +68,9 @@ class SatelliteRepo @Inject constructor( return satelliteDao.getSelectedSatellites() } - suspend fun updateEntriesSelection(catNums: List) { - satelliteDao.updateSelection(catNums) - } + // Insert - suspend fun updateSatDataFromFile(uri: Uri) { + suspend fun importSatDataFromFile(uri: Uri) { runCatching { resolver.openInputStream(uri)?.use { stream -> val entries = importEntriesFromStreams(listOf(stream)) @@ -70,20 +81,20 @@ class SatelliteRepo @Inject constructor( } } - suspend fun updateSatDataFromWeb(sources: List) { + suspend fun importSatDataFromWeb(sources: List) { coroutineScope { - launch { updateEntriesFromSources(sources) } - launch { updateTransmitters() } + launch { importEntriesFromSources(sources) } + launch { importTransmitters() } } } - private suspend fun updateEntriesFromSources(sources: List) { + private suspend fun importEntriesFromSources(sources: List) { val streams = getStreamsForSources(sources) val entries = importEntriesFromStreams(streams) insertEntriesAndRestoreSelection(entries) } - private suspend fun updateTransmitters() { + private suspend fun importTransmitters() { val transmitters = satelliteService.fetchTransmitters() satelliteDao.insertTransmitters(transmitters) } @@ -128,6 +139,22 @@ class SatelliteRepo @Inject constructor( private suspend fun insertEntriesAndRestoreSelection(entries: List) { val selectedCatNums = satelliteDao.getSelectedCatNums() satelliteDao.insertEntries(entries) - satelliteDao.updateSelection(selectedCatNums) + satelliteDao.updateEntriesSelection(selectedCatNums, true) } -} \ No newline at end of file + + // Update + + suspend fun updateEntriesSelection(catNums: List, isSelected: Boolean) { + satelliteDao.updateEntriesSelection(catNums, isSelected) + } + + suspend fun updateItemSelection(catNum: Int, isSelected: Boolean) { + satelliteDao.updateItemSelection(catNum, isSelected) + } + + // Delete + + suspend fun deleteAllData() { + satelliteDao.deleteAllData() + } +} diff --git a/app/src/main/java/com/rtbishop/look4sat/ui/SharedViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/ui/SharedViewModel.kt index 4fe76583..c08a699a 100644 --- a/app/src/main/java/com/rtbishop/look4sat/ui/SharedViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/ui/SharedViewModel.kt @@ -57,7 +57,7 @@ class SharedViewModel @Inject constructor( } } - fun getTransmittersForSat(satId: Int) = satelliteRepo.getTransmittersForSat(satId).asLiveData() + fun getTransmittersForSat(satId: Int) = satelliteRepo.getSatTransmitters(satId).asLiveData() fun calculatePasses(dateNow: Date = Date(System.currentTimeMillis())) { _passes.value = Result.InProgress diff --git a/app/src/main/java/com/rtbishop/look4sat/ui/entriesScreen/EntriesAdapter.kt b/app/src/main/java/com/rtbishop/look4sat/ui/entriesScreen/EntriesAdapter.kt index 016b3a87..2e689d4c 100644 --- a/app/src/main/java/com/rtbishop/look4sat/ui/entriesScreen/EntriesAdapter.kt +++ b/app/src/main/java/com/rtbishop/look4sat/ui/entriesScreen/EntriesAdapter.kt @@ -19,58 +19,46 @@ package com.rtbishop.look4sat.ui.entriesScreen import android.view.LayoutInflater import android.view.ViewGroup -import android.widget.SearchView +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.rtbishop.look4sat.data.model.SatItem import com.rtbishop.look4sat.databinding.ItemSatEntryBinding import java.util.* -class EntriesAdapter : RecyclerView.Adapter(), - SearchView.OnQueryTextListener { - - private val allItems = mutableListOf() - private val currentItems = mutableListOf() - private var shouldSearchAll = true - - fun getItems(): List { - return allItems +class EntriesAdapter : RecyclerView.Adapter() { + + private var allItems = listOf() + private var selectAll = true + private lateinit var entriesClickListener: EntriesClickListener + + fun submitAllItems(items: List) { + allItems = items + listDiffer.submitList(items) } - - fun setItems(items: List) { - allItems.clear() - allItems.addAll(items) - currentItems.clear() - currentItems.addAll(items) - notifyDataSetChanged() + + private fun submitCurrentItems(items: List) { + listDiffer.submitList(items) } - - fun selectAllItems() { - currentItems.forEach { it.isSelected = shouldSearchAll } - shouldSearchAll = shouldSearchAll.not() - notifyDataSetChanged() + + fun setEntriesClickListener(listener: EntriesClickListener) { + entriesClickListener = listener } - - override fun onQueryTextChange(newText: String): Boolean { - currentItems.clear() - currentItems.addAll(filterItems(allItems, newText).toMutableList()) - notifyDataSetChanged() - return false - } - - override fun onQueryTextSubmit(query: String): Boolean { - return false - } - - private fun filterItems(list: List, query: String): List { - shouldSearchAll = true - if (query.isEmpty()) return list - return try { - filterByCatNum(list, query.toInt()) - } catch (e: NumberFormatException) { - filterByName(list, query) + + fun filterItems(query: String) { + selectAll = true + if (query.isEmpty()) { + submitCurrentItems(allItems) + } else { + try { + val catNum = query.toInt() + submitCurrentItems(filterByCatNum(allItems, catNum)) + } catch (e: NumberFormatException) { + submitCurrentItems(filterByName(allItems, query)) + } } } - + private fun filterByCatNum(list: List, catNum: Int): List { return list.filter { it.catNum == catNum } } @@ -84,31 +72,52 @@ class EntriesAdapter : RecyclerView.Adapter(), } } - override fun getItemCount(): Int = currentItems.size + fun selectAllItems() { +// currentItems.forEach { it.isSelected = selectAll } +// selectAll = selectAll.not() +// notifyDataSetChanged() + +// val catNums = listDiffer.currentList.map { it.catNum } +// entriesClickListener.onSelectAllClick(catNums, selectAll) +// selectAll = selectAll.not() + } + + interface EntriesClickListener { + fun onItemClick(catNum: Int, isSelected: Boolean) + fun onSelectAllClick(catNums: List, isSelected: Boolean) + } + + private val diffCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: SatItem, newItem: SatItem): Boolean { + return oldItem.catNum == newItem.catNum + } + + override fun areContentsTheSame(oldItem: SatItem, newItem: SatItem): Boolean { + return oldItem.isSelected == newItem.isSelected + } + } + + private val listDiffer = AsyncListDiffer(this, diffCallback) + + override fun getItemCount(): Int = listDiffer.currentList.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SatItemHolder { - return SatItemHolder.from(parent) + val inflater = LayoutInflater.from(parent.context) + return SatItemHolder(ItemSatEntryBinding.inflate(inflater, parent, false)) } override fun onBindViewHolder(holder: SatItemHolder, position: Int) { - holder.bind(currentItems[position]) + holder.bind(listDiffer.currentList[position]) } - class SatItemHolder private constructor(private val binding: ItemSatEntryBinding) : + inner class SatItemHolder(private val binding: ItemSatEntryBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(item: SatItem) { binding.satEntryCheckbox.text = item.name binding.satEntryCheckbox.isChecked = item.isSelected itemView.setOnClickListener { - item.isSelected = binding.satEntryCheckbox.isChecked - } - } - - companion object { - fun from(parent: ViewGroup): SatItemHolder { - val inflater = LayoutInflater.from(parent.context) - return SatItemHolder(ItemSatEntryBinding.inflate(inflater, parent, false)) + entriesClickListener.onItemClick(item.catNum, !item.isSelected) } } } diff --git a/app/src/main/java/com/rtbishop/look4sat/ui/entriesScreen/EntriesFragment.kt b/app/src/main/java/com/rtbishop/look4sat/ui/entriesScreen/EntriesFragment.kt index 16c5b436..e63edd07 100644 --- a/app/src/main/java/com/rtbishop/look4sat/ui/entriesScreen/EntriesFragment.kt +++ b/app/src/main/java/com/rtbishop/look4sat/ui/entriesScreen/EntriesFragment.kt @@ -20,11 +20,12 @@ package com.rtbishop.look4sat.ui.entriesScreen import android.os.Bundle import android.view.View import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.widget.addTextChangedListener import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.SimpleItemAnimator import com.google.android.material.snackbar.Snackbar import com.rtbishop.look4sat.R import com.rtbishop.look4sat.data.model.Result @@ -32,24 +33,21 @@ import com.rtbishop.look4sat.data.model.TleSource import com.rtbishop.look4sat.databinding.FragmentEntriesBinding import com.rtbishop.look4sat.utility.RecyclerDivider import com.rtbishop.look4sat.utility.getNavResult -import com.rtbishop.look4sat.utility.navigateSafe import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview @FlowPreview @AndroidEntryPoint -class EntriesFragment : Fragment(R.layout.fragment_entries) { +class EntriesFragment : Fragment(R.layout.fragment_entries), EntriesAdapter.EntriesClickListener { private val viewModel: EntriesViewModel by viewModels() private val filePicker = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> - uri?.let { viewModel.updateSatDataFromFile(uri) } + uri?.let { viewModel.importSatDataFromFile(uri) } } private var binding: FragmentEntriesBinding? = null private var entriesAdapter: EntriesAdapter? = null - @ExperimentalCoroutinesApi override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentEntriesBinding.bind(view) @@ -59,24 +57,28 @@ class EntriesFragment : Fragment(R.layout.fragment_entries) { } private fun setupComponents() { - entriesAdapter = EntriesAdapter() + entriesAdapter = EntriesAdapter().apply { + setEntriesClickListener(this@EntriesFragment) + } binding?.apply { entriesRecycler.apply { setHasFixedSize(true) adapter = entriesAdapter layoutManager = LinearLayoutManager(requireContext()) + (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false addItemDecoration(RecyclerDivider(R.drawable.rec_divider_light)) } importWeb.setOnClickListener { showImportFromWebDialog() } importFile.setOnClickListener { filePicker.launch("*/*") } selectAll.setOnClickListener { entriesAdapter?.selectAllItems() } - entriesFab.setOnClickListener { navigateToPasses() } - searchBar.setOnQueryTextListener(entriesAdapter) + searchBar.addTextChangedListener { + entriesAdapter?.filterItems(it.toString()) + binding?.entriesRecycler?.scrollToPosition(0) + } searchBar.clearFocus() } } - @ExperimentalCoroutinesApi private fun setupObservers() { viewModel.satData.observe(viewLifecycleOwner, { result -> when (result) { @@ -84,7 +86,7 @@ class EntriesFragment : Fragment(R.layout.fragment_entries) { if (result.data.isEmpty()) { setEmpty() } else { - entriesAdapter?.setItems(result.data) + entriesAdapter?.submitAllItems(result.data) setLoaded() } } @@ -94,13 +96,6 @@ class EntriesFragment : Fragment(R.layout.fragment_entries) { is Result.Error -> { val errorMsg = getString(R.string.entries_update_error) Snackbar.make(requireView(), errorMsg, Snackbar.LENGTH_SHORT).show() - entriesAdapter?.let { adapter -> - if (adapter.getItems().isEmpty()) { - setEmpty() - } else { - setLoaded() - } - } } } }) @@ -108,7 +103,7 @@ class EntriesFragment : Fragment(R.layout.fragment_entries) { private fun observeSourcesResult() { getNavResult>(R.id.nav_entries, "sources") { result -> - viewModel.updateSatDataFromSources(result.map { TleSource(it) }) + viewModel.importSatDataFromSources(result.map { TleSource(it) }) } } @@ -140,15 +135,12 @@ class EntriesFragment : Fragment(R.layout.fragment_entries) { findNavController().navigate(R.id.nav_dialog_sources) } - private fun navigateToPasses() { - binding?.searchBar?.clearFocus() - entriesAdapter?.let { - val satItems = it.getItems() - if (satItems.isNotEmpty()) { - viewModel.updateItemsSelection(satItems) - requireView().findNavController().navigateSafe(R.id.action_entries_to_passes) - } - } + override fun onItemClick(catNum: Int, isSelected: Boolean) { + viewModel.updateItemSelection(catNum, isSelected) + } + + override fun onSelectAllClick(catNums: List, isSelected: Boolean) { + viewModel.updateEntriesSelection(catNums, isSelected) } override fun onDestroyView() { diff --git a/app/src/main/java/com/rtbishop/look4sat/ui/entriesScreen/EntriesViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/ui/entriesScreen/EntriesViewModel.kt index 97ce79f9..71d028f5 100644 --- a/app/src/main/java/com/rtbishop/look4sat/ui/entriesScreen/EntriesViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/ui/entriesScreen/EntriesViewModel.kt @@ -45,26 +45,26 @@ class EntriesViewModel @Inject constructor( ) : ViewModel() { private val satDataState = MutableSharedFlow>>(replay = 0) - private val satDataFlow = satelliteRepo.satDataFlow.map { Result.Success(it) } + private val satDataFlow = satelliteRepo.getAllSatItems().map { Result.Success(it) } val satData = flowOf(satDataState, satDataFlow).flattenMerge().asLiveData() - fun updateSatDataFromFile(uri: Uri) { + fun importSatDataFromFile(uri: Uri) { viewModelScope.launch { satDataState.emit(Result.InProgress) try { - satelliteRepo.updateSatDataFromFile(uri) + satelliteRepo.importSatDataFromFile(uri) } catch (exception: Exception) { satDataState.emit(Result.Error(exception)) } } } - fun updateSatDataFromSources(sources: List = prefsRepo.loadTleSources()) { + fun importSatDataFromSources(sources: List = prefsRepo.loadTleSources()) { viewModelScope.launch { satDataState.emit(Result.InProgress) val updateMillis = measureTimeMillis { try { - satelliteRepo.updateSatDataFromWeb(sources) + satelliteRepo.importSatDataFromWeb(sources) prefsRepo.saveTleSources(sources) } catch (exception: Exception) { satDataState.emit(Result.Error(exception)) @@ -74,10 +74,16 @@ class EntriesViewModel @Inject constructor( } } - fun updateItemsSelection(items: List) { + fun updateEntriesSelection(items: List, isSelected: Boolean) { viewModelScope.launch { - val selectedEntries = items.filter { it.isSelected }.map { it.catNum } - satelliteRepo.updateEntriesSelection(selectedEntries) +// val selectedEntries = items.filter { it.isSelected }.map { it.catNum } +// satelliteRepo.updateEntriesSelection(selectedEntries, isSelected) + } + } + + fun updateItemSelection(catNum: Int, isSelected: Boolean) { + viewModelScope.launch { + satelliteRepo.updateItemSelection(catNum, isSelected) } } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_entries.xml b/app/src/main/res/layout/fragment_entries.xml index 2afed24e..487d4864 100644 --- a/app/src/main/res/layout/fragment_entries.xml +++ b/app/src/main/res/layout/fragment_entries.xml @@ -51,13 +51,15 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> - @@ -97,6 +99,7 @@ android:layout_height="wrap_content" android:text="@string/entries_no_data" android:textSize="20sp" + android:visibility="invisible" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -104,14 +107,5 @@ - -