Skip to content

Commit ed125b0

Browse files
committed
Enable details screen for user's notes
1 parent 96054bb commit ed125b0

24 files changed

+544
-39
lines changed

app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ dependencies {
154154
kapt "androidx.room:room-compiler:$roomVersion"
155155
implementation "androidx.room:room-ktx:$roomVersion"
156156
implementation "androidx.room:room-rxjava2:$roomVersion"
157+
implementation 'com.squareup.picasso:picasso:2.71828'
157158

158159
// Unit tests
159160
testImplementation 'junit:junit:4.13'

app/src/main/java/ro/code4/monitorizarevot/adapters/NoteDelegationAdapter.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ import ro.code4.monitorizarevot.adapters.delegates.SectionDelegate
77
import ro.code4.monitorizarevot.adapters.helper.ListItem
88
import ro.code4.monitorizarevot.adapters.helper.NoteListItem
99
import ro.code4.monitorizarevot.adapters.helper.SectionListItem
10+
import ro.code4.monitorizarevot.data.model.Note
1011

11-
class NoteDelegationAdapter : AsyncListDifferDelegationAdapter<ListItem>(DIFF_CALLBACK) {
12+
class NoteDelegationAdapter(
13+
private val noteListener: (Note) -> Unit
14+
) : AsyncListDifferDelegationAdapter<ListItem>(DIFF_CALLBACK) {
1215
init {
1316
delegatesManager
1417
.addDelegate(SectionDelegate())
15-
.addDelegate(NoteDelegate())
18+
.addDelegate(NoteDelegate(noteListener))
1619
}
1720

1821
companion object {
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package ro.code4.monitorizarevot.adapters
2+
3+
import android.content.Context
4+
import android.content.Intent
5+
import android.content.pm.PackageManager
6+
import android.view.LayoutInflater
7+
import android.view.View
8+
import android.view.ViewGroup
9+
import android.widget.FrameLayout
10+
import android.widget.ImageView
11+
import android.widget.TextView
12+
import android.widget.Toast
13+
import androidx.core.content.FileProvider
14+
import androidx.recyclerview.widget.RecyclerView
15+
import com.squareup.picasso.Picasso
16+
import ro.code4.monitorizarevot.R
17+
import ro.code4.monitorizarevot.ui.notes.NoteAttachment
18+
import ro.code4.monitorizarevot.ui.notes.NoteDetails
19+
import java.io.File
20+
21+
class NoteDetailsAdapter(
22+
context: Context
23+
) : RecyclerView.Adapter<NoteDetailsViewHolder>() {
24+
25+
private val layoutInflater = LayoutInflater.from(context)
26+
private var details: NoteDetails? = null
27+
28+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) {
29+
TYPE_NOTE_TEXT -> NoteDetailsViewHolderText(
30+
layoutInflater.inflate(
31+
R.layout.item_note_details_text,
32+
parent,
33+
false
34+
)
35+
)
36+
TYPE_NOTE_IMAGE -> NoteImageViewHolderDetails(
37+
layoutInflater.inflate(
38+
R.layout.item_note_details_image,
39+
parent,
40+
false
41+
)
42+
)
43+
else -> throw IllegalArgumentException("Unknown note row type requested!")
44+
}
45+
46+
override fun onBindViewHolder(holder: NoteDetailsViewHolder, position: Int) {
47+
holder.bind(details, position)
48+
}
49+
50+
override fun getItemViewType(position: Int) = when (position) {
51+
0 -> TYPE_NOTE_TEXT
52+
else -> TYPE_NOTE_IMAGE
53+
}
54+
55+
override fun getItemCount() = (details?.let { 1 + (details?.attachedFiles?.size ?: 0) } ?: 0)
56+
57+
fun updateAdapter(noteDetails: NoteDetails) {
58+
this.details = noteDetails
59+
notifyDataSetChanged()
60+
}
61+
62+
companion object {
63+
const val TYPE_NOTE_TEXT = 1
64+
const val TYPE_NOTE_IMAGE = 2
65+
}
66+
}
67+
68+
sealed class NoteDetailsViewHolder(
69+
rowView: View
70+
) : RecyclerView.ViewHolder(rowView) {
71+
72+
abstract fun bind(noteDetails: NoteDetails?, position: Int)
73+
}
74+
75+
class NoteDetailsViewHolderText(
76+
private val rowView: View
77+
) : NoteDetailsViewHolder(rowView) {
78+
private val formQuestionIdentifier: TextView =
79+
rowView.findViewById(R.id.formAndQuestionIdentifier)
80+
private val noteText: TextView = rowView.findViewById(R.id.noteText)
81+
private val noteDate: TextView = rowView.findViewById(R.id.noteDate)
82+
83+
override fun bind(noteDetails: NoteDetails?, position: Int) {
84+
noteDetails?.let {
85+
formQuestionIdentifier.text = noteDetails.codes?.let { codes ->
86+
rowView.context.getString(
87+
R.string.note_details_codes, codes.formCode, codes.questionCode
88+
)
89+
} ?: ""
90+
noteText.text = it.description
91+
noteDate.text = it.date
92+
}
93+
}
94+
}
95+
96+
class NoteImageViewHolderDetails(
97+
private val rowView: View
98+
) : NoteDetailsViewHolder(rowView) {
99+
100+
private val noteVideoNotice: FrameLayout = rowView.findViewById(R.id.noteVideoNoticeContainer)
101+
private val noteImage: ImageView = rowView.findViewById(R.id.noteImage)
102+
103+
override fun bind(noteDetails: NoteDetails?, position: Int) {
104+
noteDetails?.let {
105+
// the position is always offset by 1(note text information always occupies the first item in
106+
// the RecyclerView)
107+
val actualPosition = position - 1
108+
val attachedFile = it.attachedFiles[actualPosition]
109+
val isAFile = attachedFile.uri.scheme?.let { scheme -> scheme != "https" } ?: true
110+
if (attachedFile.isVideo) {
111+
noteImage.visibility = View.GONE
112+
noteVideoNotice.visibility = View.VISIBLE
113+
rowView.setOnClickListener {
114+
setupExternalVideoPreview(rowView.context, attachedFile, isAFile)
115+
}
116+
} else {
117+
rowView.setOnClickListener(null)
118+
noteImage.visibility = View.VISIBLE
119+
noteVideoNotice.visibility = View.GONE
120+
val requestCreator = if (isAFile) {
121+
Picasso.get().load(File(attachedFile.uri.toString()))
122+
} else {
123+
Picasso.get().load(attachedFile.uri)
124+
}
125+
requestCreator.into(noteImage)
126+
}
127+
}
128+
}
129+
130+
private fun setupExternalVideoPreview(
131+
context: Context, attachedFile: NoteAttachment, isAFile: Boolean
132+
) = with(context) {
133+
val intent = Intent(Intent.ACTION_VIEW).apply {
134+
type = "video/*"
135+
data = if (isAFile) {
136+
kotlin.runCatching {
137+
FileProvider.getUriForFile(
138+
this@with, packageName, File(attachedFile.uri.toString())
139+
)
140+
}.getOrNull()
141+
} else {
142+
attachedFile.uri
143+
}
144+
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
145+
}
146+
if (packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
147+
startActivity(intent)
148+
} else {
149+
Toast.makeText(
150+
this, getString(R.string.note_video_previewer_missing), Toast.LENGTH_SHORT
151+
).show()
152+
}
153+
}
154+
}

app/src/main/java/ro/code4/monitorizarevot/adapters/delegates/NoteDelegate.kt

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,24 @@ package ro.code4.monitorizarevot.adapters.delegates
22

33
import android.view.LayoutInflater
44
import android.view.View
5-
import android.view.View.VISIBLE
65
import android.view.ViewGroup
76
import androidx.recyclerview.widget.RecyclerView
7+
import com.google.android.material.card.MaterialCardView
88
import com.hannesdorfmann.adapterdelegates4.AbsListItemAdapterDelegate
99
import kotlinx.android.extensions.LayoutContainer
1010
import kotlinx.android.synthetic.main.item_note.*
1111
import ro.code4.monitorizarevot.R
1212
import ro.code4.monitorizarevot.adapters.helper.ListItem
1313
import ro.code4.monitorizarevot.adapters.helper.NoteListItem
14-
import ro.code4.monitorizarevot.helper.formatDateTime
14+
import ro.code4.monitorizarevot.data.model.Note
15+
import ro.code4.monitorizarevot.helper.formatNoteDateTime
1516

16-
class NoteDelegate : AbsListItemAdapterDelegate<NoteListItem, ListItem, NoteDelegate.ViewHolder>() {
17+
class NoteDelegate(
18+
private val noteSelectedListener: (Note) -> Unit
19+
) : AbsListItemAdapterDelegate<NoteListItem, ListItem, NoteDelegate.ViewHolder>() {
1720
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder =
1821
ViewHolder(
22+
noteSelectedListener,
1923
LayoutInflater.from(parent.context).inflate(R.layout.item_note, parent, false)
2024
)
2125

@@ -34,22 +38,30 @@ class NoteDelegate : AbsListItemAdapterDelegate<NoteListItem, ListItem, NoteDele
3438
holder.bind(item)
3539
}
3640

37-
class ViewHolder(override val containerView: View) :
38-
RecyclerView.ViewHolder(containerView),
39-
LayoutContainer {
41+
class ViewHolder(
42+
private val noteSelectedListener: (Note) -> Unit,
43+
override val containerView: View
44+
) : RecyclerView.ViewHolder(containerView), LayoutContainer {
4045
private lateinit var item: NoteListItem
46+
private val noteRowContainer =
47+
containerView.findViewById<MaterialCardView>(R.id.noteRowContainer)
4148

4249
fun bind(noteListItem: NoteListItem) {
4350
item = noteListItem
44-
51+
noteRowContainer.setOnClickListener { noteSelectedListener(noteListItem.note) }
52+
formAndQuestionIdentifier.text = item.codes?.let { codes ->
53+
containerView.context.getString(
54+
R.string.note_details_codes, codes.formCode, codes.questionCode
55+
)
56+
} ?: ""
4557
with(item.note) {
4658
/* questionId?.let {
4759
noteQuestionText.visibility = VISIBLE
4860
// TODO add question text here
4961
// noteQuestionText.text = "Add question $questionId text here."
5062
}*/
5163
noteText.text = description
52-
noteDate.text = date.formatDateTime()
64+
noteDate.text = date.formatNoteDateTime()
5365
}
5466
}
5567
}

app/src/main/java/ro/code4/monitorizarevot/adapters/helper/ListItem.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import ro.code4.monitorizarevot.data.model.Note
55
import ro.code4.monitorizarevot.data.model.Question
66
import ro.code4.monitorizarevot.data.pojo.FormWithSections
77
import ro.code4.monitorizarevot.data.pojo.QuestionWithAnswers
8+
import ro.code4.monitorizarevot.ui.notes.NoteFormQuestionCodes
89

910
sealed class ListItem
1011
class QuestionListItem(val question: Question) : ListItem()
@@ -19,4 +20,4 @@ class SectionListItem(@param:StringRes val titleResourceId: Int, vararg val form
1920

2021
class FormListItem(val formWithSections: FormWithSections) : ListItem()
2122
class AddNoteListItem : ListItem()
22-
class NoteListItem(val note: Note) : ListItem()
23+
class NoteListItem(val note: Note, val codes: NoteFormQuestionCodes?) : ListItem()

app/src/main/java/ro/code4/monitorizarevot/data/model/Note.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import androidx.room.Entity
44
import androidx.room.ForeignKey
55
import androidx.room.Index
66
import androidx.room.PrimaryKey
7+
import org.parceler.Parcel
78
import java.util.*
89

910
@Entity(
@@ -19,6 +20,7 @@ import java.util.*
1920
)],
2021
indices = [Index(value = ["countyCode", "pollingStationNumber", "questionId"], unique = false)]
2122
)
23+
@Parcel(Parcel.Serialization.FIELD)
2224
class Note {
2325
@PrimaryKey(autoGenerate = true)
2426
var id: Int = 0
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package ro.code4.monitorizarevot.data.model.response
2+
3+
import com.google.gson.annotations.Expose
4+
5+
class PostNoteResponse {
6+
@Expose
7+
lateinit var filesAddress: List<String>
8+
}

app/src/main/java/ro/code4/monitorizarevot/helper/Constants.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ object Constants {
55
const val DATE_TIME_FORMAT = "dd.MM.yyyy HH:mm"
66
const val DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"
77
const val DATE_FORMAT_SIMPLE = "dd.MM.yyyy"
8+
const val DATA_NOTE_FORMAT = "dd/MM HH:mm"
89
const val FORM = "form"
910
const val QUESTION = "question"
11+
const val NOTE = "note"
1012

1113
const val REQUEST_CODE_RECORD_VIDEO = 1001
1214
const val REQUEST_CODE_TAKE_PHOTO = 1002

app/src/main/java/ro/code4/monitorizarevot/helper/Utils.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ fun Date.formatDateTime(): String {
117117
return formatter.format(this)
118118
}
119119

120+
fun Date.formatNoteDateTime(): String {
121+
val formatter = SimpleDateFormat(Constants.DATA_NOTE_FORMAT, Locale.getDefault())
122+
return formatter.format(this)
123+
}
124+
120125
fun String?.getDate(): Long? {
121126
if (this == null) {
122127
return null

app/src/main/java/ro/code4/monitorizarevot/modules/Modules.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import ro.code4.monitorizarevot.ui.forms.questions.QuestionsViewModel
2727
import ro.code4.monitorizarevot.ui.guide.GuideViewModel
2828
import ro.code4.monitorizarevot.ui.login.LoginViewModel
2929
import ro.code4.monitorizarevot.ui.main.MainViewModel
30+
import ro.code4.monitorizarevot.ui.notes.NoteDetailsViewModel
3031
import ro.code4.monitorizarevot.ui.notes.NoteViewModel
3132
import ro.code4.monitorizarevot.ui.onboarding.OnboardingViewModel
3233
import ro.code4.monitorizarevot.ui.section.PollingStationViewModel
@@ -110,6 +111,7 @@ val viewModelsModule = module {
110111
viewModel { QuestionsViewModel() }
111112
viewModel { QuestionsDetailsViewModel() }
112113
viewModel { NoteViewModel() }
114+
viewModel { NoteDetailsViewModel() }
113115
viewModel { GuideViewModel() }
114116
viewModel { SplashScreenViewModel() }
115117
}

app/src/main/java/ro/code4/monitorizarevot/repositories/Repository.kt

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import android.annotation.SuppressLint
44
import android.os.AsyncTask
55
import android.util.Log
66
import androidx.lifecycle.LiveData
7+
import com.google.gson.Gson
8+
import com.google.gson.reflect.TypeToken
79
import io.reactivex.Observable
810
import io.reactivex.Single
911
import io.reactivex.android.schedulers.AndroidSchedulers
@@ -21,11 +23,9 @@ import ro.code4.monitorizarevot.data.model.*
2123
import ro.code4.monitorizarevot.data.model.answers.AnsweredQuestion
2224
import ro.code4.monitorizarevot.data.model.answers.SelectedAnswer
2325
import ro.code4.monitorizarevot.data.model.response.LoginResponse
26+
import ro.code4.monitorizarevot.data.model.response.PostNoteResponse
2427
import ro.code4.monitorizarevot.data.model.response.VersionResponse
25-
import ro.code4.monitorizarevot.data.pojo.AnsweredQuestionPOJO
26-
import ro.code4.monitorizarevot.data.pojo.FormWithSections
27-
import ro.code4.monitorizarevot.data.pojo.PollingStationInfo
28-
import ro.code4.monitorizarevot.data.pojo.SectionWithQuestions
28+
import ro.code4.monitorizarevot.data.pojo.*
2929
import ro.code4.monitorizarevot.helper.Constants
3030
import ro.code4.monitorizarevot.helper.createMultipart
3131
import ro.code4.monitorizarevot.services.ApiInterface
@@ -50,6 +50,8 @@ class Repository : KoinComponent {
5050
retrofit.create(ApiInterface::class.java)
5151
}
5252

53+
private val postTypeToken = object : TypeToken<PostNoteResponse>() {}.type
54+
5355
private var syncInProgress = false
5456
fun login(user: User): Observable<LoginResponse> = loginInterface.login(user)
5557

@@ -330,11 +332,17 @@ class Repository : KoinComponent {
330332
note.description.createMultipart("Text")
331333
).doOnNext {
332334
note.synced = true
335+
note.uriPath = combineApiFilesUrls(it)
333336
db.noteDao().updateNote(note)
334337
noteFiles?.forEach { uploadedFile -> uploadedFile.delete() }
335338
}
336339
}
337340

341+
private fun combineApiFilesUrls(response: ResponseBody): String? = kotlin.runCatching {
342+
val parsedResponse = Gson().fromJson<PostNoteResponse>(response.charStream(), postTypeToken)
343+
parsedResponse.filesAddress.joinToString(separator = Constants.FILES_PATHS_SEPARATOR)
344+
}.getOrNull()
345+
338346
@SuppressLint("CheckResult")
339347
fun syncData() {
340348
if (!syncInProgress) {
@@ -408,5 +416,6 @@ class Repository : KoinComponent {
408416
db.pollingStationDao().deleteAll()
409417
}
410418
}
419+
411420
}
412421

0 commit comments

Comments
 (0)