Skip to content

Commit

Permalink
Merge pull request #8 from alexmercerind/feat/history-search
Browse files Browse the repository at this point in the history
feat: search in `HistoryFragment`
  • Loading branch information
alexmercerind authored Nov 29, 2023
2 parents fe7014d + 168c621 commit 012a707
Show file tree
Hide file tree
Showing 17 changed files with 341 additions and 102 deletions.
13 changes: 8 additions & 5 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ android {
applicationId = "com.alexmercerind.audire"
minSdk = 21
targetSdk = 34
versionCode = 1
versionName = "1.0"
versionCode = 2
versionName = "1.1"
}

buildFeatures {
Expand All @@ -22,12 +22,14 @@ android {

buildTypes {
release {
isDebuggable = false
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
}
debug {
isDebuggable = true
}
}
compileOptions {
Expand All @@ -50,6 +52,7 @@ dependencies {
implementation("androidx.navigation:navigation-ui-ktx:$navVersion")

val roomVersion = "2.6.0"
implementation("androidx.room:room-ktx:$roomVersion")
implementation("androidx.room:room-runtime:$roomVersion")
annotationProcessor("androidx.room:room-compiler:$roomVersion")
ksp("androidx.room:room-compiler:$roomVersion")
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
android:exported="false" />
<activity
android:name=".ui.MainActivity"
android:exported="true">
android:exported="true"
android:windowSoftInputMode="adjustNothing">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import com.alexmercerind.audire.models.HistoryItem
import com.alexmercerind.audire.ui.HistoryViewModel
import com.alexmercerind.audire.ui.MusicActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

@OptIn(DelicateCoroutinesApi::class)
class HistoryItemAdapter(
private val items: List<HistoryItem>, private val historyViewModel: HistoryViewModel
val items: List<HistoryItem>, private val historyViewModel: HistoryViewModel
) : RecyclerView.Adapter<HistoryItemAdapter.HistoryItemViewHolder>() {

inner class HistoryItemViewHolder(val binding: HistoryItemBinding) :
Expand Down Expand Up @@ -56,15 +58,15 @@ class HistoryItemAdapter(
MaterialAlertDialogBuilder(
root.context, R.style.Base_Theme_Audire_MaterialAlertDialog
).setTitle(R.string.remove_history_item_title).setMessage(
context.getString(
R.string.remove_history_item_message, items[position].title
)
).setPositiveButton(R.string.yes) { dialog, _ ->
dialog.dismiss()
GlobalScope.launch(Dispatchers.IO) { historyViewModel.delete(items[position]) }
}.setNegativeButton(R.string.no) { dialog, _ ->
dialog.dismiss()
}.show()
context.getString(
R.string.remove_history_item_message, items[position].title
)
).setPositiveButton(R.string.yes) { dialog, _ ->
dialog.dismiss()
GlobalScope.launch(Dispatchers.IO) { historyViewModel.delete(items[position]) }
}.setNegativeButton(R.string.no) { dialog, _ ->
dialog.dismiss()
}.show()
true
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package com.alexmercerind.audire.db

import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.alexmercerind.audire.models.HistoryItem
import kotlinx.coroutines.flow.Flow

@Dao
interface HistoryItemDao {
Expand All @@ -17,5 +17,8 @@ interface HistoryItemDao {
fun delete(historyItem: HistoryItem)

@Query("SELECT * FROM history_item ORDER BY timestamp DESC")
fun getAll(): LiveData<List<HistoryItem>>
fun getAll(): Flow<List<HistoryItem>>

@Query("SELECT * FROM history_item WHERE LOWER(title) LIKE '%' || :term || '%' ORDER BY timestamp DESC")
suspend fun search(term: String): List<HistoryItem>
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,13 @@ import com.alexmercerind.audire.models.HistoryItem

class HistoryRepository(private val application: Application) {
fun insert(historyItem: HistoryItem) =
HistoryItemDatabase(application)
.historyItemDao()
.insert(historyItem)
HistoryItemDatabase(application).historyItemDao().insert(historyItem)

fun delete(historyItem: HistoryItem) =
HistoryItemDatabase(application)
.historyItemDao()
.delete(historyItem)
HistoryItemDatabase(application).historyItemDao().delete(historyItem)

fun getAll() =
HistoryItemDatabase(application)
.historyItemDao()
.getAll()
fun getAll() = HistoryItemDatabase(application).historyItemDao().getAll()

suspend fun search(term: String) =
HistoryItemDatabase(application).historyItemDao().search(term)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class AboutActivity : AppCompatActivity() {
companion object {
private const val GITHUB = "https://github.com/alexmercerind/audire"
private const val LICENSE = "https://github.com/alexmercerind/audire/blob/main/LICENSE"
private const val PRIVACY = "https://github.com/alexmercerind/audire/blob/main/PRIVACY"
private const val PRIVACY = "https://github.com/alexmercerind/audire/wiki/Privacy-Policy-%5BPlay-Store%5D"

private const val DEVELOPER_GITHUB = "https://github.com/alexmercerind"
private const val DEVELOPER_X = "https://x.com/alexmercerind"
Expand Down
106 changes: 86 additions & 20 deletions app/src/main/java/com/alexmercerind/audire/ui/HistoryFragment.kt
Original file line number Diff line number Diff line change
@@ -1,62 +1,128 @@
package com.alexmercerind.audire.ui

import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import com.alexmercerind.audire.R
import com.alexmercerind.audire.adapters.HistoryItemAdapter
import com.alexmercerind.audire.databinding.FragmentHistoryBinding
import com.google.android.material.appbar.MaterialToolbar
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch

class HistoryFragment : Fragment() {

private var _binding: FragmentHistoryBinding? = null
private val binding get() = _binding!!

private val historyViewModel: HistoryViewModel by viewModels()

private lateinit var imm: InputMethodManager

private val watcher = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}

override fun afterTextChanged(s: Editable?) {
historyViewModel.term = s.toString()
}
}

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
): View {
_binding = FragmentHistoryBinding.inflate(inflater, container, false)

binding.searchLinearLayout.visibility = View.GONE
binding.historyLinearLayout.visibility = View.GONE
binding.historyRecyclerView.visibility = View.GONE

binding.historyRecyclerView.layoutManager = LinearLayoutManager(context)

historyViewModel.getAll().observe(viewLifecycleOwner) {
imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager

binding.historyRecyclerView.adapter = HistoryItemAdapter(it, historyViewModel)
binding.searchTextInputLayout.visibility = View.GONE

if (it.isEmpty()) {
binding.historyLinearLayout.visibility = View.VISIBLE
binding.historyRecyclerView.visibility = View.GONE
} else {
binding.historyLinearLayout.visibility = View.GONE
binding.historyRecyclerView.visibility = View.VISIBLE
}
binding.searchTextInputLayout.setEndIconOnClickListener {
binding.searchTextInputEditText.text?.clear()
binding.primaryMaterialToolbar.visibility = View.VISIBLE
binding.searchTextInputLayout.visibility = View.GONE
binding.searchTextInputLayout.clearFocus()
imm.hideSoftInputFromWindow(binding.root.windowToken, 0)
}

binding.root.findViewById<MaterialToolbar>(R.id.primaryMaterialToolbar).setOnMenuItemClickListener {
val intent = when (it.itemId) {
R.id.settings -> Intent(context, SettingsActivity::class.java)
R.id.about -> Intent(context, AboutActivity::class.java)
else -> null
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
historyViewModel.adapter.filterNotNull().collect {
if (it.items.isEmpty()) {
if (historyViewModel.term.isEmpty()) {
// No HistoryItem(s) by default.
binding.historyLinearLayout.visibility = View.VISIBLE
binding.searchLinearLayout.visibility = View.GONE
} else {
// No HistoryItem(s) due to search.
binding.historyLinearLayout.visibility = View.GONE
binding.searchLinearLayout.visibility = View.VISIBLE
}
binding.historyRecyclerView.visibility = View.GONE
} else {
// HistoryItem(s) are present i.e. RecyclerView must be VISIBLE.
binding.historyLinearLayout.visibility = View.GONE
binding.searchLinearLayout.visibility = View.GONE
binding.historyRecyclerView.visibility = View.VISIBLE
binding.historyRecyclerView.adapter = it
}
}
}
if (intent != null) {
startActivity(intent)
}


binding.primaryMaterialToolbar.setOnMenuItemClickListener {
if (it.itemId == R.id.search) {
binding.primaryMaterialToolbar.visibility = View.GONE
binding.searchTextInputLayout.visibility = View.VISIBLE
binding.searchTextInputLayout.requestFocus()
imm.showSoftInput(binding.searchTextInputEditText, 0)
} else {
val intent = when (it.itemId) {
R.id.settings -> Intent(context, SettingsActivity::class.java)
R.id.about -> Intent(context, AboutActivity::class.java)
else -> null
}
if (intent != null) {
startActivity(intent)
}
}
true
}

return binding.root
}

override fun onStart() {
super.onStart()
binding.searchTextInputEditText.addTextChangedListener(watcher)
}

override fun onStop() {
super.onStop()
binding.searchTextInputEditText.removeTextChangedListener(watcher)
binding.searchTextInputEditText.text?.clear()

historyViewModel.term = ""
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
Expand Down
64 changes: 61 additions & 3 deletions app/src/main/java/com/alexmercerind/audire/ui/HistoryViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,74 @@ package com.alexmercerind.audire.ui

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import com.alexmercerind.audire.adapters.HistoryItemAdapter
import com.alexmercerind.audire.models.HistoryItem
import com.alexmercerind.audire.repository.HistoryRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

class HistoryViewModel(application: Application) : AndroidViewModel(application) {
/**
* HistoryItemAdapter to be used with RecyclerView.
*
* Automatically handles if a search term is entered.
*/
val adapter: StateFlow<HistoryItemAdapter?>
get() = _adapter

private val _adapter = MutableStateFlow<HistoryItemAdapter?>(null)

var term: String = ""
set(value) {
// Avoid duplicate operation in Room.
if (value == field) {
return
}

field = value
viewModelScope.launch(Dispatchers.IO) {
mutex.withLock {
_adapter.emit(
HistoryItemAdapter(
when (value) {
// Search term == "" -> Show all HistoryItem(s)
"" -> getAll().first()
// Search term != "" -> Show search HistoryItem(s)
else -> search(value.lowercase())
}, this@HistoryViewModel
)
)
}
}
}

private val mutex = Mutex()

private val repository = HistoryRepository(application)

init {
viewModelScope.launch(Dispatchers.IO) {
getAll().collect {
_adapter.emit(
HistoryItemAdapter(
it, this@HistoryViewModel
)
)
}
}
}

private fun getAll() = repository.getAll()

private suspend fun search(term: String) = repository.search(term)

fun insert(historyItem: HistoryItem) = repository.insert(historyItem)

fun delete(historyItem: HistoryItem) = repository.delete(historyItem)

fun getAll() = repository.getAll()
}
Loading

0 comments on commit 012a707

Please sign in to comment.