Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Usage history part 3/3: Add history screen #47

Merged
merged 7 commits into from
Nov 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-parcelize'
id 'org.jetbrains.kotlin.android'
id 'com.google.devtools.ksp'
id 'com.google.dagger.hilt.android'
Expand Down Expand Up @@ -82,6 +83,7 @@ dependencies {

implementation(libs.libphonenumber)
implementation(libs.vcard4android)
implementation(libs.paging)

implementation(libs.coroutines.android)
testImplementation(libs.coroutines.test)
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,8 @@
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
android:name=".screens.history.HistoryActivity"
android:label="@string/label_history" />
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,14 @@ interface DetailedActivityDao {
"""
)
fun getRecentDetailedActivities(): Flow<List<DetailedActivity>>

@Query(
"""
SELECT *
FROM activities
ORDER BY occurred_at DESC
LIMIT :pageSize OFFSET :pageNumber * :pageSize
"""
)
suspend fun getDetailedActivities(pageSize: Int, pageNumber: Int): List<DetailedActivity>
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package org.vinaygopinath.launchchat.helpers

import android.content.res.Resources
import android.os.Build
import android.text.Html
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.SpannedString
import androidx.annotation.StringRes
import org.vinaygopinath.launchchat.R
import org.vinaygopinath.launchchat.models.Action
Expand Down Expand Up @@ -41,7 +46,7 @@ class DetailedActivityHelper @Inject constructor(
return detailedActivity.actions.isNotEmpty()
}

fun getFirstActionText(detailedActivity: DetailedActivity): String? {
fun getFirstActionText(detailedActivity: DetailedActivity): Spanned? {
return if (isFirstActionVisible(detailedActivity)) {
getActionText(detailedActivity.actions.first())
} else {
Expand All @@ -53,19 +58,21 @@ class DetailedActivityHelper @Inject constructor(
return detailedActivity.actions.size >= 2
}

fun getSecondActionText(detailedActivity: DetailedActivity): String? {
fun getSecondActionText(detailedActivity: DetailedActivity): Spanned? {
return if (isSecondActionVisible(detailedActivity)) {
getActionText(detailedActivity.actions[1])
} else {
null
}
}

private fun getActionText(action: Action): String {
return resources.getString(
R.string.action_label,
action.phoneNumber,
getActionTypeDisplayName(action)
private fun getActionText(action: Action): Spanned {
return getSpannedText(
resources.getString(
R.string.action_label,
action.phoneNumber,
getActionTypeDisplayName(action)
)
)
}

Expand All @@ -90,4 +97,41 @@ class DetailedActivityHelper @Inject constructor(
null
}
}

fun getActionsText(detailedActivity: DetailedActivity): Spanned? {
val actions = detailedActivity.actions

return if (actions.isEmpty()) {
SpannedString(resources.getString(R.string.no_action_label))
} else {
actions.joinToSpannedString("\n") { action -> getActionText(action) }
}
}

private fun getSpannedText(text: String) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Html.fromHtml(text, Html.FROM_HTML_MODE_COMPACT)
} else {
Html.fromHtml(text)
}

private fun <T> Iterable<T>.joinToSpannedString(
separator: CharSequence = ", ",
prefix: CharSequence = "",
postfix: CharSequence = "",
limit: Int = -1,
truncated: CharSequence = "...",
transform: ((T) -> CharSequence)? = null
): SpannedString {
return SpannedString(
joinTo(
SpannableStringBuilder(),
separator,
prefix,
postfix,
limit,
truncated,
transform
)
)
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package org.vinaygopinath.launchchat.models

import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
import java.time.Instant

@Parcelize
@Entity(
tableName = "activities",
indices = [
Expand All @@ -18,7 +21,7 @@ data class Activity(
@ColumnInfo val source: Source,
@ColumnInfo val message: String?,
@ColumnInfo("occurred_at") val occurredAt: Instant
) {
) : Parcelable {
enum class Source {
TEL,
SMS,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.vinaygopinath.launchchat.repositories

import androidx.paging.PagingSource
import androidx.paging.PagingState
import kotlinx.coroutines.flow.Flow
import org.vinaygopinath.launchchat.daos.DetailedActivityDao
import org.vinaygopinath.launchchat.models.DetailedActivity
Expand All @@ -12,4 +14,50 @@ class DetailedActivityRepository @Inject constructor(
fun getRecentDetailedActivities(): Flow<List<DetailedActivity>> {
return detailedActivityDao.getRecentDetailedActivities()
}

fun getNewPagingSource(): DetailedActivityPagingSource {
return DetailedActivityPagingSource(detailedActivityDao)
}

class DetailedActivityPagingSource(
private val detailedActivityDao: DetailedActivityDao
) : PagingSource<Int, DetailedActivity>() {
override fun getRefreshKey(state: PagingState<Int, DetailedActivity>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, DetailedActivity> {
val currentPage = params.key ?: 0
return try {
val detailedActivities = detailedActivityDao.getDetailedActivities(
pageSize = params.loadSize,
pageNumber = currentPage
)
val nextKey = if (detailedActivities.isEmpty()) {
null
} else {
currentPage + (params.loadSize) / PAGE_SIZE
}

LoadResult.Page(
data = detailedActivities,
nextKey = nextKey,
prevKey = if (currentPage == 0) {
null
} else {
currentPage - 1
}
)
} catch (exception: Exception) {
LoadResult.Error(exception)
}
}

companion object {
const val PAGE_SIZE = 10
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package org.vinaygopinath.launchchat.screens.history

import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.setPadding
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.vinaygopinath.launchchat.R
import org.vinaygopinath.launchchat.helpers.DetailedActivityHelper
import org.vinaygopinath.launchchat.models.DetailedActivity
import org.vinaygopinath.launchchat.screens.main.MainActivity
import javax.inject.Inject

@AndroidEntryPoint
class HistoryActivity : AppCompatActivity() {

private val viewModel: HistoryViewModel by viewModels()

private lateinit var recyclerView: RecyclerView

@Inject
lateinit var detailedActivityHelper: DetailedActivityHelper

private val historyAdapter by lazy {
HistoryAdapter(
detailedActivityHelper,
object : HistoryAdapter.HistoryClickListener {
override fun onClick(detailedActivity: DetailedActivity) {
startActivity(
MainActivity.getHistoryIntent(
this@HistoryActivity,
detailedActivity.activity
)
)
}
}
)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(
RecyclerView(this).apply {
layoutParams = RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT,
RecyclerView.LayoutParams.MATCH_PARENT
)
}.also {
recyclerView = it
}
)

initializeView()
initializeObservers()
}

private fun initializeView() {
with(recyclerView) {
val linearLayoutManager = LinearLayoutManager(this@HistoryActivity)
layoutManager = linearLayoutManager
setPadding(resources.getDimensionPixelSize(R.dimen.padding_medium))
adapter = historyAdapter
addItemDecoration(
DividerItemDecoration(
this@HistoryActivity,
linearLayoutManager.orientation
)
)
}
}

private fun initializeObservers() {
lifecycleScope.launch {
viewModel.detailedActivities.collectLatest { pagingData ->
historyAdapter.submitData(pagingData)
}
}
}

companion object {
fun getIntent(context: Context): Intent = Intent(context, HistoryActivity::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package org.vinaygopinath.launchchat.screens.history

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil.ItemCallback
import androidx.recyclerview.widget.RecyclerView.NO_POSITION
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.google.android.material.textview.MaterialTextView
import org.vinaygopinath.launchchat.R
import org.vinaygopinath.launchchat.helpers.DetailedActivityHelper
import org.vinaygopinath.launchchat.models.DetailedActivity

class HistoryAdapter(
private val helper: DetailedActivityHelper,
private val listener: HistoryClickListener
) : PagingDataAdapter<DetailedActivity, HistoryAdapter.HistoryViewHolder>(
DetailedActivityDiffCallback()
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryViewHolder {
val view = LayoutInflater.from(parent.context).inflate(
R.layout.list_item_history,
parent,
false
)
val viewHolder = HistoryViewHolder(view)
view.setOnClickListener {
val position = viewHolder.bindingAdapterPosition
if (position != NO_POSITION) {
getItem(position)?.let { listener.onClick(it) }
}
}

return viewHolder
}

override fun onBindViewHolder(holder: HistoryViewHolder, position: Int) {
if (position == NO_POSITION) {
return
}

getItem(position)?.let { detailedActivity ->
holder.titleText.setText(helper.getSourceDisplayName(detailedActivity))
holder.timestampText.text = helper.getActivityShortTimestamp(detailedActivity)
holder.contentText.text = helper.getActivityContent(detailedActivity)
holder.actionsText.text = helper.getActionsText(detailedActivity)
}
}

inner class HistoryViewHolder(view: View) : ViewHolder(view) {
val titleText: MaterialTextView =
view.findViewById(R.id.history_list_title)
val timestampText: MaterialTextView =
view.findViewById(R.id.history_list_timestamp)
val contentText: MaterialTextView =
view.findViewById(R.id.history_list_content)
val actionsText: MaterialTextView =
view.findViewById(R.id.history_list_actions)
}

class DetailedActivityDiffCallback : ItemCallback<DetailedActivity>() {
override fun areItemsTheSame(
oldItem: DetailedActivity,
newItem: DetailedActivity
) = oldItem.activity.id == newItem.activity.id

override fun areContentsTheSame(
oldItem: DetailedActivity,
newItem: DetailedActivity
) = oldItem == newItem

}

interface HistoryClickListener {
fun onClick(activity: DetailedActivity)
}
}
Loading