diff --git a/app/build.gradle b/app/build.gradle index 2190aafd..f66e1664 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -154,6 +154,7 @@ dependencies { kapt "androidx.room:room-compiler:$roomVersion" implementation "androidx.room:room-ktx:$roomVersion" implementation "androidx.room:room-rxjava2:$roomVersion" + implementation 'com.squareup.picasso:picasso:2.71828' // Unit tests testImplementation 'junit:junit:4.13' diff --git a/app/schemas/ro.code4.monitorizarevot.data.AppDatabase/5.json b/app/schemas/ro.code4.monitorizarevot.data.AppDatabase/5.json new file mode 100644 index 00000000..3bfc3cca --- /dev/null +++ b/app/schemas/ro.code4.monitorizarevot.data.AppDatabase/5.json @@ -0,0 +1,673 @@ +{ + "formatVersion": 1, + "database": { + "version": 5, + "identityHash": "2442667e56ad4886c066a799077327bd", + "entities": [ + { + "tableName": "county", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `code` TEXT NOT NULL, `name` TEXT NOT NULL, `limit` INTEGER NOT NULL, `diaspora` INTEGER, `order` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "code", + "columnName": "code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "limit", + "columnName": "limit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaspora", + "columnName": "diaspora", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_county_code", + "unique": true, + "columnNames": [ + "code" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_county_code` ON `${TABLE_NAME}` (`code`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "polling_station", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `countyCode` TEXT NOT NULL, `idPollingStation` INTEGER NOT NULL, `urbanArea` INTEGER NOT NULL, `isPollingStationPresidentFemale` INTEGER NOT NULL, `observerArrivalTime` TEXT, `observerLeaveTime` TEXT, `synced` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`countyCode`) REFERENCES `county`(`code`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "countyCode", + "columnName": "countyCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "idPollingStation", + "columnName": "idPollingStation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "urbanArea", + "columnName": "urbanArea", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPollingStationPresidentFemale", + "columnName": "isPollingStationPresidentFemale", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "observerArrivalTime", + "columnName": "observerArrivalTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "observerLeaveTime", + "columnName": "observerLeaveTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "synced", + "columnName": "synced", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_polling_station_countyCode_idPollingStation", + "unique": true, + "columnNames": [ + "countyCode", + "idPollingStation" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_polling_station_countyCode_idPollingStation` ON `${TABLE_NAME}` (`countyCode`, `idPollingStation`)" + } + ], + "foreignKeys": [ + { + "table": "county", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "countyCode" + ], + "referencedColumns": [ + "code" + ] + } + ] + }, + { + "tableName": "form_details", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `code` TEXT NOT NULL, `description` TEXT NOT NULL, `formVersion` INTEGER NOT NULL, `diaspora` INTEGER, `order` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "code", + "columnName": "code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "formVersion", + "columnName": "formVersion", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaspora", + "columnName": "diaspora", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "section", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uniqueId` TEXT NOT NULL, `code` TEXT, `description` TEXT, `formId` INTEGER NOT NULL, `orderNumber` INTEGER NOT NULL, PRIMARY KEY(`uniqueId`), FOREIGN KEY(`formId`) REFERENCES `form_details`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uniqueId", + "columnName": "uniqueId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "code", + "columnName": "code", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "formId", + "columnName": "formId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "orderNumber", + "columnName": "orderNumber", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "uniqueId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "form_details", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "formId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "question", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `text` TEXT NOT NULL, `code` TEXT NOT NULL, `questionType` INTEGER NOT NULL, `sectionId` TEXT NOT NULL, `hasNotes` INTEGER NOT NULL, `orderNumber` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`sectionId`) REFERENCES `section`(`uniqueId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "code", + "columnName": "code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "questionType", + "columnName": "questionType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sectionId", + "columnName": "sectionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasNotes", + "columnName": "hasNotes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "orderNumber", + "columnName": "orderNumber", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "section", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "sectionId" + ], + "referencedColumns": [ + "uniqueId" + ] + } + ] + }, + { + "tableName": "answer", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idOption` INTEGER NOT NULL, `text` TEXT NOT NULL, `isFreeText` INTEGER NOT NULL, `questionId` INTEGER NOT NULL, `orderNumber` INTEGER NOT NULL, PRIMARY KEY(`idOption`), FOREIGN KEY(`questionId`) REFERENCES `question`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "idOption", + "columnName": "idOption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isFreeText", + "columnName": "isFreeText", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "questionId", + "columnName": "questionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "orderNumber", + "columnName": "orderNumber", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "idOption" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "question", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "questionId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "answered_question", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `formId` INTEGER NOT NULL, `questionId` INTEGER NOT NULL, `countyCode` TEXT NOT NULL, `pollingStationNumber` INTEGER NOT NULL, `savedLocally` INTEGER NOT NULL, `synced` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`formId`) REFERENCES `form_details`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`questionId`) REFERENCES `question`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`countyCode`, `pollingStationNumber`) REFERENCES `polling_station`(`countyCode`, `idPollingStation`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "formId", + "columnName": "formId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "questionId", + "columnName": "questionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "countyCode", + "columnName": "countyCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pollingStationNumber", + "columnName": "pollingStationNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "savedLocally", + "columnName": "savedLocally", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "synced", + "columnName": "synced", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_answered_question_countyCode_pollingStationNumber_id", + "unique": true, + "columnNames": [ + "countyCode", + "pollingStationNumber", + "id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_answered_question_countyCode_pollingStationNumber_id` ON `${TABLE_NAME}` (`countyCode`, `pollingStationNumber`, `id`)" + } + ], + "foreignKeys": [ + { + "table": "form_details", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "formId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "question", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "questionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "polling_station", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "countyCode", + "pollingStationNumber" + ], + "referencedColumns": [ + "countyCode", + "idPollingStation" + ] + } + ] + }, + { + "tableName": "selected_answer", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`optionId` INTEGER NOT NULL, `value` TEXT, `countyCode` TEXT NOT NULL, `pollingStationNumber` INTEGER NOT NULL, `questionId` TEXT NOT NULL, PRIMARY KEY(`optionId`, `countyCode`, `pollingStationNumber`), FOREIGN KEY(`optionId`) REFERENCES `answer`(`idOption`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`countyCode`, `pollingStationNumber`, `questionId`) REFERENCES `answered_question`(`countyCode`, `pollingStationNumber`, `id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "optionId", + "columnName": "optionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "countyCode", + "columnName": "countyCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pollingStationNumber", + "columnName": "pollingStationNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "questionId", + "columnName": "questionId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "optionId", + "countyCode", + "pollingStationNumber" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "answer", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "optionId" + ], + "referencedColumns": [ + "idOption" + ] + }, + { + "table": "answered_question", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "countyCode", + "pollingStationNumber", + "questionId" + ], + "referencedColumns": [ + "countyCode", + "pollingStationNumber", + "id" + ] + } + ] + }, + { + "tableName": "note", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uriPath` TEXT, `description` TEXT NOT NULL, `questionId` INTEGER, `date` INTEGER NOT NULL, `countyCode` TEXT NOT NULL, `pollingStationNumber` INTEGER NOT NULL, `synced` INTEGER NOT NULL, `formCode` TEXT, `questionCode` TEXT, FOREIGN KEY(`questionId`) REFERENCES `question`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`countyCode`, `pollingStationNumber`) REFERENCES `polling_station`(`countyCode`, `idPollingStation`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uriPath", + "columnName": "uriPath", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "questionId", + "columnName": "questionId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "countyCode", + "columnName": "countyCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pollingStationNumber", + "columnName": "pollingStationNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "synced", + "columnName": "synced", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "formCode", + "columnName": "formCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "questionCode", + "columnName": "questionCode", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_note_countyCode_pollingStationNumber_questionId", + "unique": false, + "columnNames": [ + "countyCode", + "pollingStationNumber", + "questionId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_note_countyCode_pollingStationNumber_questionId` ON `${TABLE_NAME}` (`countyCode`, `pollingStationNumber`, `questionId`)" + } + ], + "foreignKeys": [ + { + "table": "question", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "questionId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "polling_station", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "countyCode", + "pollingStationNumber" + ], + "referencedColumns": [ + "countyCode", + "idPollingStation" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2442667e56ad4886c066a799077327bd')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/ro/code4/monitorizarevot/MigrationTest.kt b/app/src/androidTest/java/ro/code4/monitorizarevot/MigrationTest.kt index 40b4b501..e92bbaec 100644 --- a/app/src/androidTest/java/ro/code4/monitorizarevot/MigrationTest.kt +++ b/app/src/androidTest/java/ro/code4/monitorizarevot/MigrationTest.kt @@ -14,6 +14,7 @@ import org.junit.runner.RunWith import ro.code4.monitorizarevot.data.AppDatabase import ro.code4.monitorizarevot.data.Migrations import java.io.IOException +import java.util.* @RunWith(AndroidJUnit4::class) class MigrationTest { @@ -142,6 +143,51 @@ class MigrationTest { } } + @Test + fun migrate4To5() { + val expectedTime = Date().time + helper.createDatabase(TEST_DB, 4).use { + val values = ContentValues().apply { + put("id", 1) + put("uriPath", "/fake/path/on/disk/for/file/image/jpg") + put("description", "description for note") + put("questionId", 12) + put("date", expectedTime) + put("countyCode", "B") + put("pollingStationNumber", 55) + put("synced", false) + } + val rowId = it.insert("note", SQLiteDatabase.CONFLICT_FAIL, values) + assertTrue(rowId > 0) + } + val db = helper.runMigrationsAndValidate(TEST_DB, 5, true, Migrations.MIGRATION_4_5) + val noteDataCursor = db.query("SELECT * FROM note") + assertNotNull(noteDataCursor) + noteDataCursor.use { + // we have a single row, previously inserted + assertEquals(1, it.count) + assertTrue(it.moveToFirst()) + // at this point we expect to have exactly 10 columns for the note table + assertEquals(10, it.columnCount) + // check for the new column "formCode" and that it has the default value of null + assertNull(it.getString(it.getColumnIndex("formCode"))) + // check for the new column "questionCode" and that it has the default value of null + assertNull(it.getString(it.getColumnIndex("questionCode"))) + // check for older columns + assertEquals(1, it.getInt(it.getColumnIndex("id"))) + assertEquals( + "/fake/path/on/disk/for/file/image/jpg", + it.getString(it.getColumnIndex("uriPath")) + ) + assertEquals("description for note", it.getString(it.getColumnIndex("description"))) + assertEquals(12, it.getInt(it.getColumnIndex("questionId"))) + assertEquals(expectedTime, it.getLong(it.getColumnIndex("date"))) + assertEquals(55, it.getInt(it.getColumnIndex("pollingStationNumber"))) + assertEquals("B", it.getString(it.getColumnIndex("countyCode"))) + assertTrue(it.getInt(it.getColumnIndex("synced")) == 0) + } + } + @Test @Throws(IOException::class) fun migrateAll() { diff --git a/app/src/main/java/ro/code4/monitorizarevot/adapters/NoteDelegationAdapter.kt b/app/src/main/java/ro/code4/monitorizarevot/adapters/NoteDelegationAdapter.kt index d873aa15..19e713a8 100644 --- a/app/src/main/java/ro/code4/monitorizarevot/adapters/NoteDelegationAdapter.kt +++ b/app/src/main/java/ro/code4/monitorizarevot/adapters/NoteDelegationAdapter.kt @@ -7,12 +7,15 @@ import ro.code4.monitorizarevot.adapters.delegates.SectionDelegate import ro.code4.monitorizarevot.adapters.helper.ListItem import ro.code4.monitorizarevot.adapters.helper.NoteListItem import ro.code4.monitorizarevot.adapters.helper.SectionListItem +import ro.code4.monitorizarevot.data.model.Note -class NoteDelegationAdapter : AsyncListDifferDelegationAdapter(DIFF_CALLBACK) { +class NoteDelegationAdapter( + private val noteListener: (Note) -> Unit +) : AsyncListDifferDelegationAdapter(DIFF_CALLBACK) { init { delegatesManager .addDelegate(SectionDelegate()) - .addDelegate(NoteDelegate()) + .addDelegate(NoteDelegate(noteListener)) } companion object { diff --git a/app/src/main/java/ro/code4/monitorizarevot/adapters/NoteDetailsAdapter.kt b/app/src/main/java/ro/code4/monitorizarevot/adapters/NoteDetailsAdapter.kt new file mode 100644 index 00000000..a735295a --- /dev/null +++ b/app/src/main/java/ro/code4/monitorizarevot/adapters/NoteDetailsAdapter.kt @@ -0,0 +1,154 @@ +package ro.code4.monitorizarevot.adapters + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import androidx.core.content.FileProvider +import androidx.recyclerview.widget.RecyclerView +import com.squareup.picasso.Picasso +import ro.code4.monitorizarevot.R +import ro.code4.monitorizarevot.ui.notes.NoteAttachment +import ro.code4.monitorizarevot.ui.notes.NoteDetails +import java.io.File + +class NoteDetailsAdapter( + context: Context +) : RecyclerView.Adapter() { + + private val layoutInflater = LayoutInflater.from(context) + private var details: NoteDetails? = null + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) { + TYPE_NOTE_TEXT -> NoteDetailsViewHolderText( + layoutInflater.inflate( + R.layout.item_note_details_text, + parent, + false + ) + ) + TYPE_NOTE_IMAGE -> NoteImageViewHolderDetails( + layoutInflater.inflate( + R.layout.item_note_details_image, + parent, + false + ) + ) + else -> throw IllegalArgumentException("Unknown note row type requested!") + } + + override fun onBindViewHolder(holder: NoteDetailsViewHolder, position: Int) { + holder.bind(details, position) + } + + override fun getItemViewType(position: Int) = when (position) { + 0 -> TYPE_NOTE_TEXT + else -> TYPE_NOTE_IMAGE + } + + override fun getItemCount() = (details?.let { 1 + (details?.attachedFiles?.size ?: 0) } ?: 0) + + fun updateAdapter(noteDetails: NoteDetails) { + this.details = noteDetails + notifyDataSetChanged() + } + + companion object { + const val TYPE_NOTE_TEXT = 1 + const val TYPE_NOTE_IMAGE = 2 + } +} + +sealed class NoteDetailsViewHolder( + rowView: View +) : RecyclerView.ViewHolder(rowView) { + + abstract fun bind(noteDetails: NoteDetails?, position: Int) +} + +class NoteDetailsViewHolderText( + private val rowView: View +) : NoteDetailsViewHolder(rowView) { + private val formQuestionIdentifier: TextView = + rowView.findViewById(R.id.formAndQuestionIdentifier) + private val noteText: TextView = rowView.findViewById(R.id.noteText) + private val noteDate: TextView = rowView.findViewById(R.id.noteDate) + + override fun bind(noteDetails: NoteDetails?, position: Int) { + noteDetails?.let { + formQuestionIdentifier.text = noteDetails.codes?.let { codes -> + rowView.context.getString( + R.string.note_details_codes, codes.formCode, codes.questionCode + ) + } ?: "" + noteText.text = it.description + noteDate.text = it.date + } + } +} + +class NoteImageViewHolderDetails( + private val rowView: View +) : NoteDetailsViewHolder(rowView) { + + private val noteVideoNotice: FrameLayout = rowView.findViewById(R.id.noteVideoNoticeContainer) + private val noteImage: ImageView = rowView.findViewById(R.id.noteImage) + + override fun bind(noteDetails: NoteDetails?, position: Int) { + noteDetails?.let { + // the position is always offset by 1(note text information always occupies the first item in + // the RecyclerView) + val actualPosition = position - 1 + val attachedFile = it.attachedFiles[actualPosition] + val isAFile = attachedFile.uri.scheme?.let { scheme -> scheme != "https" } ?: true + if (attachedFile.isVideo) { + noteImage.visibility = View.GONE + noteVideoNotice.visibility = View.VISIBLE + rowView.setOnClickListener { + setupExternalVideoPreview(rowView.context, attachedFile, isAFile) + } + } else { + rowView.setOnClickListener(null) + noteImage.visibility = View.VISIBLE + noteVideoNotice.visibility = View.GONE + val requestCreator = if (isAFile) { + Picasso.get().load(File(attachedFile.uri.toString())) + } else { + Picasso.get().load(attachedFile.uri) + } + requestCreator.into(noteImage) + } + } + } + + private fun setupExternalVideoPreview( + context: Context, attachedFile: NoteAttachment, isAFile: Boolean + ) = with(context) { + val intent = Intent(Intent.ACTION_VIEW).apply { + type = "video/*" + data = if (isAFile) { + kotlin.runCatching { + FileProvider.getUriForFile( + this@with, packageName, File(attachedFile.uri.toString()) + ) + }.getOrNull() + } else { + attachedFile.uri + } + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + } + if (packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) { + startActivity(intent) + } else { + Toast.makeText( + this, getString(R.string.note_video_previewer_missing), Toast.LENGTH_SHORT + ).show() + } + } +} diff --git a/app/src/main/java/ro/code4/monitorizarevot/adapters/delegates/NoteDelegate.kt b/app/src/main/java/ro/code4/monitorizarevot/adapters/delegates/NoteDelegate.kt index 9e668fda..52e0d34d 100644 --- a/app/src/main/java/ro/code4/monitorizarevot/adapters/delegates/NoteDelegate.kt +++ b/app/src/main/java/ro/code4/monitorizarevot/adapters/delegates/NoteDelegate.kt @@ -2,46 +2,57 @@ package ro.code4.monitorizarevot.adapters.delegates import android.view.LayoutInflater import android.view.View -import android.view.View.VISIBLE import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.card.MaterialCardView import com.hannesdorfmann.adapterdelegates4.AbsListItemAdapterDelegate import kotlinx.android.extensions.LayoutContainer import kotlinx.android.synthetic.main.item_note.* import ro.code4.monitorizarevot.R import ro.code4.monitorizarevot.adapters.helper.ListItem import ro.code4.monitorizarevot.adapters.helper.NoteListItem -import ro.code4.monitorizarevot.helper.formatDateTime +import ro.code4.monitorizarevot.data.model.Note +import ro.code4.monitorizarevot.helper.formatNoteDateTime + +class NoteDelegate( + private val noteSelectedListener: (Note) -> Unit +) : AbsListItemAdapterDelegate() { -class NoteDelegate : AbsListItemAdapterDelegate() { override fun onCreateViewHolder(parent: ViewGroup): ViewHolder = ViewHolder( + noteSelectedListener, LayoutInflater.from(parent.context).inflate(R.layout.item_note, parent, false) ) override fun isForViewType( - item: ListItem, - items: MutableList, - position: Int - ): Boolean = - item is NoteListItem + item: ListItem, items: MutableList, position: Int + ): Boolean = item is NoteListItem override fun onBindViewHolder( - item: NoteListItem, - holder: ViewHolder, - payloads: MutableList + item: NoteListItem, holder: ViewHolder, payloads: MutableList ) { holder.bind(item) } - class ViewHolder(override val containerView: View) : - RecyclerView.ViewHolder(containerView), - LayoutContainer { + class ViewHolder( + private val noteSelectedListener: (Note) -> Unit, + override val containerView: View + ) : RecyclerView.ViewHolder(containerView), LayoutContainer { private lateinit var item: NoteListItem + private val noteRowContainer = + containerView.findViewById(R.id.noteRowContainer) fun bind(noteListItem: NoteListItem) { item = noteListItem - + noteRowContainer.setOnClickListener { noteSelectedListener(noteListItem.note) } + formAndQuestionIdentifier.text = + if (item.note.formCode != null && item.note.questionCode != null) { + containerView.context.getString( + R.string.note_details_codes, item.note.formCode, item.note.questionCode + ) + } else { + "" + } with(item.note) { /* questionId?.let { noteQuestionText.visibility = VISIBLE @@ -49,7 +60,7 @@ class NoteDelegate : AbsListItemAdapterDelegate = arrayOf(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4) + /** + * This migration changes the database to add the form and question codes to the Note entity. + */ + val MIGRATION_4_5 = object : Migration(4, 5) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE note ADD COLUMN `formCode` TEXT DEFAULT NULL") + database.execSQL("ALTER TABLE note ADD COLUMN `questionCode` TEXT DEFAULT NULL") + } + } + + val ALL: Array = arrayOf(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5) } \ No newline at end of file diff --git a/app/src/main/java/ro/code4/monitorizarevot/data/model/Note.kt b/app/src/main/java/ro/code4/monitorizarevot/data/model/Note.kt index 79d65cba..223a88e4 100644 --- a/app/src/main/java/ro/code4/monitorizarevot/data/model/Note.kt +++ b/app/src/main/java/ro/code4/monitorizarevot/data/model/Note.kt @@ -4,6 +4,7 @@ import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.Index import androidx.room.PrimaryKey +import org.parceler.Parcel import java.util.* @Entity( @@ -19,6 +20,7 @@ import java.util.* )], indices = [Index(value = ["countyCode", "pollingStationNumber", "questionId"], unique = false)] ) +@Parcel(Parcel.Serialization.FIELD) class Note { @PrimaryKey(autoGenerate = true) var id: Int = 0 @@ -32,10 +34,15 @@ class Note { var date: Date = Date() lateinit var countyCode: String + var pollingStationNumber = 0 var synced = false + var formCode: String? = null + + var questionCode: String? = null + override fun equals(other: Any?): Boolean = other is Note && other.id == id diff --git a/app/src/main/java/ro/code4/monitorizarevot/data/model/response/PostNoteResponse.kt b/app/src/main/java/ro/code4/monitorizarevot/data/model/response/PostNoteResponse.kt new file mode 100644 index 00000000..7ea6f51a --- /dev/null +++ b/app/src/main/java/ro/code4/monitorizarevot/data/model/response/PostNoteResponse.kt @@ -0,0 +1,8 @@ +package ro.code4.monitorizarevot.data.model.response + +import com.google.gson.annotations.Expose + +class PostNoteResponse { + @Expose + lateinit var filesAddress: List +} \ No newline at end of file diff --git a/app/src/main/java/ro/code4/monitorizarevot/helper/Constants.kt b/app/src/main/java/ro/code4/monitorizarevot/helper/Constants.kt index 5f79ca39..891f3c1a 100644 --- a/app/src/main/java/ro/code4/monitorizarevot/helper/Constants.kt +++ b/app/src/main/java/ro/code4/monitorizarevot/helper/Constants.kt @@ -5,8 +5,11 @@ object Constants { const val DATE_TIME_FORMAT = "dd.MM.yyyy HH:mm" const val DATE_FORMAT = "yyyy-MM-dd HH:mm:ss" const val DATE_FORMAT_SIMPLE = "dd.MM.yyyy" + const val DATA_NOTE_FORMAT = "dd/MM HH:mm" const val FORM = "form" const val QUESTION = "question" + const val NOTE = "note" + const val FORM_QUESTION_CODES = "form_question_codes" const val REQUEST_CODE_RECORD_VIDEO = 1001 const val REQUEST_CODE_TAKE_PHOTO = 1002 diff --git a/app/src/main/java/ro/code4/monitorizarevot/helper/Utils.kt b/app/src/main/java/ro/code4/monitorizarevot/helper/Utils.kt index 2f6ca110..a05ea43d 100644 --- a/app/src/main/java/ro/code4/monitorizarevot/helper/Utils.kt +++ b/app/src/main/java/ro/code4/monitorizarevot/helper/Utils.kt @@ -128,6 +128,11 @@ fun Date.formatDateTime(): String { return formatter.format(this) } +fun Date.formatNoteDateTime(): String { + val formatter = SimpleDateFormat(Constants.DATA_NOTE_FORMAT, Locale.getDefault()) + return formatter.format(this) +} + fun String?.getDate(): Long? { if (this == null) { return null diff --git a/app/src/main/java/ro/code4/monitorizarevot/modules/Modules.kt b/app/src/main/java/ro/code4/monitorizarevot/modules/Modules.kt index 40067482..0740c4e7 100644 --- a/app/src/main/java/ro/code4/monitorizarevot/modules/Modules.kt +++ b/app/src/main/java/ro/code4/monitorizarevot/modules/Modules.kt @@ -28,6 +28,7 @@ import ro.code4.monitorizarevot.ui.forms.questions.QuestionsViewModel import ro.code4.monitorizarevot.ui.guide.GuideViewModel import ro.code4.monitorizarevot.ui.login.LoginViewModel import ro.code4.monitorizarevot.ui.main.MainViewModel +import ro.code4.monitorizarevot.ui.notes.NoteDetailsViewModel import ro.code4.monitorizarevot.ui.notes.NoteViewModel import ro.code4.monitorizarevot.ui.onboarding.OnboardingViewModel import ro.code4.monitorizarevot.ui.section.PollingStationViewModel @@ -112,6 +113,7 @@ val viewModelsModule = module { viewModel { QuestionsViewModel() } viewModel { QuestionsDetailsViewModel() } viewModel { NoteViewModel() } + viewModel { NoteDetailsViewModel() } viewModel { GuideViewModel() } viewModel { SplashScreenViewModel() } } diff --git a/app/src/main/java/ro/code4/monitorizarevot/repositories/Repository.kt b/app/src/main/java/ro/code4/monitorizarevot/repositories/Repository.kt index fa01bfcb..4171039f 100644 --- a/app/src/main/java/ro/code4/monitorizarevot/repositories/Repository.kt +++ b/app/src/main/java/ro/code4/monitorizarevot/repositories/Repository.kt @@ -4,6 +4,8 @@ import android.annotation.SuppressLint import android.os.AsyncTask import android.util.Log import androidx.lifecycle.LiveData +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken import io.reactivex.Observable import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers @@ -22,6 +24,7 @@ import ro.code4.monitorizarevot.data.model.answers.AnsweredQuestion import ro.code4.monitorizarevot.data.model.answers.SelectedAnswer import ro.code4.monitorizarevot.data.model.response.ErrorVersionResponse import ro.code4.monitorizarevot.data.model.response.LoginResponse +import ro.code4.monitorizarevot.data.model.response.PostNoteResponse import ro.code4.monitorizarevot.data.model.response.VersionResponse import ro.code4.monitorizarevot.data.pojo.* import ro.code4.monitorizarevot.helper.Constants @@ -48,6 +51,8 @@ class Repository : KoinComponent { retrofit.create(ApiInterface::class.java) } + private val postTypeToken = object : TypeToken() {}.type + private var syncInProgress = false fun login(user: User): Observable = loginInterface.login(user) @@ -330,11 +335,17 @@ class Repository : KoinComponent { note.description.createMultipart("Text") ).doOnNext { note.synced = true + note.uriPath = combineApiFilesUrls(it) db.noteDao().updateNote(note) noteFiles?.forEach { uploadedFile -> uploadedFile.delete() } } } + private fun combineApiFilesUrls(response: ResponseBody): String? = kotlin.runCatching { + val parsedResponse = Gson().fromJson(response.charStream(), postTypeToken) + parsedResponse.filesAddress.joinToString(separator = Constants.FILES_PATHS_SEPARATOR) + }.getOrNull() + @SuppressLint("CheckResult") fun syncData() { if (!syncInProgress) { diff --git a/app/src/main/java/ro/code4/monitorizarevot/ui/forms/FormsFragment.kt b/app/src/main/java/ro/code4/monitorizarevot/ui/forms/FormsFragment.kt index a18001e6..001a7331 100644 --- a/app/src/main/java/ro/code4/monitorizarevot/ui/forms/FormsFragment.kt +++ b/app/src/main/java/ro/code4/monitorizarevot/ui/forms/FormsFragment.kt @@ -10,6 +10,7 @@ import org.koin.android.viewmodel.ext.android.viewModel import org.parceler.Parcels import ro.code4.monitorizarevot.R import ro.code4.monitorizarevot.helper.Constants.FORM +import ro.code4.monitorizarevot.helper.Constants.NOTE import ro.code4.monitorizarevot.helper.Constants.QUESTION import ro.code4.monitorizarevot.helper.changePollingStation import ro.code4.monitorizarevot.helper.replaceFragment @@ -17,6 +18,7 @@ import ro.code4.monitorizarevot.ui.base.ViewModelFragment import ro.code4.monitorizarevot.ui.forms.questions.QuestionsDetailsFragment import ro.code4.monitorizarevot.ui.forms.questions.QuestionsListFragment import ro.code4.monitorizarevot.ui.main.MainActivity +import ro.code4.monitorizarevot.ui.notes.NoteDetailsFragment import ro.code4.monitorizarevot.ui.notes.NoteFragment class FormsFragment : ViewModelFragment() { @@ -69,13 +71,20 @@ class FormsFragment : ViewModelFragment() { childFragmentManager.replaceFragment( R.id.content, NoteFragment(), - bundleOf( - Pair(QUESTION, Parcels.wrap(it)) - ), + it?.let { bundleOf(Pair(QUESTION, Parcels.wrap(it))) }, NoteFragment.TAG ) }) + viewModel.selectedNote().observe(viewLifecycleOwner, Observer { + childFragmentManager.replaceFragment( + R.id.content, + NoteDetailsFragment(), + bundleOf(Pair(NOTE, Parcels.wrap(it))), + NoteDetailsFragment.TAG + ) + }) + pollingStationBarButton.setOnClickListener { viewModel.notifyChangeRequested() (activity as AppCompatActivity).changePollingStation() diff --git a/app/src/main/java/ro/code4/monitorizarevot/ui/forms/FormsViewModel.kt b/app/src/main/java/ro/code4/monitorizarevot/ui/forms/FormsViewModel.kt index 06982fba..231b6650 100644 --- a/app/src/main/java/ro/code4/monitorizarevot/ui/forms/FormsViewModel.kt +++ b/app/src/main/java/ro/code4/monitorizarevot/ui/forms/FormsViewModel.kt @@ -13,6 +13,7 @@ import ro.code4.monitorizarevot.adapters.helper.AddNoteListItem import ro.code4.monitorizarevot.adapters.helper.FormListItem import ro.code4.monitorizarevot.adapters.helper.ListItem import ro.code4.monitorizarevot.data.model.FormDetails +import ro.code4.monitorizarevot.data.model.Note import ro.code4.monitorizarevot.data.model.Question import ro.code4.monitorizarevot.data.pojo.AnsweredQuestionPOJO import ro.code4.monitorizarevot.data.pojo.FormWithSections @@ -20,11 +21,14 @@ import ro.code4.monitorizarevot.data.pojo.PollingStationInfo import ro.code4.monitorizarevot.helper.Constants.REMOTE_CONFIG_FILTER_DIASPORA_FORMS import ro.code4.monitorizarevot.helper.completedPollingStationConfig import ro.code4.monitorizarevot.ui.base.BaseFormViewModel +import ro.code4.monitorizarevot.ui.notes.NoteFormQuestionCodes class FormsViewModel : BaseFormViewModel() { private val formsLiveData = MutableLiveData>() private val selectedFormLiveData = MutableLiveData() private val selectedQuestionLiveData = MutableLiveData>() + private val selectedNoteLiveData = MutableLiveData() + private val syncVisibilityLiveData = MediatorLiveData() private val unSyncedDataCountLiveData = MediatorLiveData() private val navigateToNotesLiveData = MutableLiveData() private val pollingStationLiveData = MutableLiveData() @@ -62,6 +66,7 @@ class FormsViewModel : BaseFormViewModel() { fun selectedForm(): LiveData = selectedFormLiveData fun selectedQuestion(): LiveData> = selectedQuestionLiveData + fun selectedNote() : LiveData = selectedNoteLiveData fun navigateToNotes(): LiveData = navigateToNotesLiveData fun pollingStation(): LiveData = pollingStationLiveData @@ -127,6 +132,12 @@ class FormsViewModel : BaseFormViewModel() { selectedQuestionLiveData.postValue(Pair(selectedFormLiveData.value!!, question)) } + fun selectNote(note: Note) { + selectedNoteLiveData.postValue(note) + } + + fun syncVisibility(): LiveData = syncVisibilityLiveData + fun unSyncedDataCount(): LiveData = unSyncedDataCountLiveData fun sync() { diff --git a/app/src/main/java/ro/code4/monitorizarevot/ui/notes/NoteDetailsFragment.kt b/app/src/main/java/ro/code4/monitorizarevot/ui/notes/NoteDetailsFragment.kt new file mode 100644 index 00000000..e571aa0c --- /dev/null +++ b/app/src/main/java/ro/code4/monitorizarevot/ui/notes/NoteDetailsFragment.kt @@ -0,0 +1,73 @@ +package ro.code4.monitorizarevot.ui.notes + +import android.content.Context +import android.graphics.Color +import android.os.Bundle +import android.view.View +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.snackbar.Snackbar +import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration +import kotlinx.android.synthetic.main.fragment_note_detail.* +import org.koin.android.viewmodel.ext.android.getSharedViewModel +import org.koin.android.viewmodel.ext.android.getViewModel +import org.parceler.Parcels +import ro.code4.monitorizarevot.R +import ro.code4.monitorizarevot.adapters.NoteDetailsAdapter +import ro.code4.monitorizarevot.data.model.Note +import ro.code4.monitorizarevot.helper.Constants +import ro.code4.monitorizarevot.helper.isOnline +import ro.code4.monitorizarevot.ui.base.ViewModelFragment +import ro.code4.monitorizarevot.ui.forms.FormsViewModel + +class NoteDetailsFragment : ViewModelFragment() { + override val screenName: Int + get() = R.string.title_note + override val layout: Int + get() = R.layout.fragment_note_detail + override lateinit var viewModel: NoteDetailsViewModel + private lateinit var baseViewModel: FormsViewModel + private lateinit var noteContentAdapter: NoteDetailsAdapter + + override fun onAttach(context: Context) { + super.onAttach(context) + viewModel = getViewModel() + baseViewModel = getSharedViewModel(from = { requireParentFragment() }) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.setData(Parcels.unwrap(arguments?.getParcelable((Constants.NOTE)))) + noteContentAdapter = NoteDetailsAdapter(requireContext()) + noteContent.apply { + layoutManager = LinearLayoutManager(requireContext()) + adapter = noteContentAdapter + addItemDecoration( + HorizontalDividerItemDecoration.Builder(requireContext()) + .color(Color.TRANSPARENT) + .sizeResId(R.dimen.small_margin).build() + ) + } + + viewModel.noteDetails.observe(viewLifecycleOwner, Observer { + if (it.isSynced) { + if (requireActivity().isOnline()) { + noteContentAdapter.updateAdapter(it) + } else { + noteContentAdapter.updateAdapter(it.copy(attachedFiles = emptyList())) + Snackbar.make( + view, + getString(R.string.note_details_missing_internet), + Snackbar.LENGTH_LONG + ).show() + } + } else { + noteContentAdapter.updateAdapter(it) + } + }) + } + + companion object { + val TAG = NoteDetailsFragment::class.java.simpleName + } +} diff --git a/app/src/main/java/ro/code4/monitorizarevot/ui/notes/NoteDetailsViewModel.kt b/app/src/main/java/ro/code4/monitorizarevot/ui/notes/NoteDetailsViewModel.kt new file mode 100644 index 00000000..6805ff78 --- /dev/null +++ b/app/src/main/java/ro/code4/monitorizarevot/ui/notes/NoteDetailsViewModel.kt @@ -0,0 +1,70 @@ +package ro.code4.monitorizarevot.ui.notes + +import android.net.Uri +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import org.parceler.Parcel +import org.parceler.ParcelConstructor +import ro.code4.monitorizarevot.data.model.Note +import ro.code4.monitorizarevot.helper.Constants +import ro.code4.monitorizarevot.helper.formatNoteDateTime +import ro.code4.monitorizarevot.ui.base.BaseViewModel + +class NoteDetailsViewModel : BaseViewModel() { + + private var note: Note? = null + private val _noteDetails = MutableLiveData() + val noteDetails: LiveData = _noteDetails + + fun setData(note: Note?) { + note?.let { + this.note = note + _noteDetails.postValue( + NoteDetails( + it.description, + note.formCode?.let { fc -> + note.questionCode?.let { qc -> NoteFormQuestionCodes(fc, qc) } + }, + it.date.formatNoteDateTime(), + unwrapNoteUrls(it), + it.synced + ) + ) + } + } + + private fun unwrapNoteUrls(note: Note): List { + val paths = note.uriPath?.split(Constants.FILES_PATHS_SEPARATOR) ?: emptyList() + return paths.filter { it.isNotEmpty() }.map { NoteAttachment(Uri.parse(it), isVideo(it)) } + } + + private fun isVideo(path: String): Boolean { + val videosExtensions = listOf("mp4", "m4a", "3gp", "ts", "flac", "amr", "ogg", "wav", "mkv") + val lastPointIndex = path.lastIndexOf(".") + return if (lastPointIndex > 0 && lastPointIndex < path.length - 1) { + val extension = path.substring((lastPointIndex + 1) until path.length) + videosExtensions.contains(extension) + } else { + false + } + } +} + +data class NoteDetails( + val description: String, + val codes: NoteFormQuestionCodes?, + val date: String, + val attachedFiles: List, + val isSynced: Boolean +) + +data class NoteAttachment( + val uri: Uri, + val isVideo: Boolean +) + +@Parcel(Parcel.Serialization.BEAN) +data class NoteFormQuestionCodes @ParcelConstructor constructor( + val formCode: String, + val questionCode: String +) \ No newline at end of file diff --git a/app/src/main/java/ro/code4/monitorizarevot/ui/notes/NoteFragment.kt b/app/src/main/java/ro/code4/monitorizarevot/ui/notes/NoteFragment.kt index c62dd6b8..f10a5a53 100644 --- a/app/src/main/java/ro/code4/monitorizarevot/ui/notes/NoteFragment.kt +++ b/app/src/main/java/ro/code4/monitorizarevot/ui/notes/NoteFragment.kt @@ -31,6 +31,7 @@ import org.koin.android.viewmodel.ext.android.viewModel import org.parceler.Parcels import ro.code4.monitorizarevot.R import ro.code4.monitorizarevot.adapters.NoteDelegationAdapter +import ro.code4.monitorizarevot.data.model.FormDetails import ro.code4.monitorizarevot.data.model.Question import ro.code4.monitorizarevot.helper.* import ro.code4.monitorizarevot.helper.Constants.REQUEST_CODE_GALLERY @@ -49,9 +50,13 @@ class NoteFragment : ViewModelFragment(), PermissionManager.Permi companion object { val TAG = NoteFragment::class.java.simpleName } + override val viewModel: NoteViewModel by viewModel() private lateinit var baseViewModel: FormsViewModel - private val noteAdapter: NoteDelegationAdapter by lazy { NoteDelegationAdapter() } + private var fqCodes: NoteFormQuestionCodes? = null + private val noteAdapter: NoteDelegationAdapter by lazy { + NoteDelegationAdapter { note -> baseViewModel.selectNote(note) } + } private lateinit var permissionManager: PermissionManager override fun onAttach(context: Context) { super.onAttach(context) @@ -73,7 +78,17 @@ class NoteFragment : ViewModelFragment(), PermissionManager.Permi baseViewModel.setTitle(it) }) - viewModel.setData(Parcels.unwrap(arguments?.getParcelable((Constants.QUESTION)))) + val selectedForm: FormDetails? = baseViewModel.selectedForm().value + val selectedQuestion: Question? = arguments?.let { + Parcels.unwrap(it.getParcelable(Constants.QUESTION)) + } + fqCodes = if (selectedForm != null && selectedQuestion != null) { + NoteFormQuestionCodes(selectedForm.code, selectedQuestion.code) + } else { + null + } + viewModel.setData(selectedQuestion, fqCodes) + viewModel.notes().observe(viewLifecycleOwner, Observer { noteAdapter.items = it }) diff --git a/app/src/main/java/ro/code4/monitorizarevot/ui/notes/NoteViewModel.kt b/app/src/main/java/ro/code4/monitorizarevot/ui/notes/NoteViewModel.kt index bd079b12..cbcc1d56 100644 --- a/app/src/main/java/ro/code4/monitorizarevot/ui/notes/NoteViewModel.kt +++ b/app/src/main/java/ro/code4/monitorizarevot/ui/notes/NoteViewModel.kt @@ -48,8 +48,11 @@ class NoteViewModel : BaseFormViewModel() { fun filesNames(): LiveData> = filesNamesLiveData fun submitCompleted(): SingleLiveEvent = submitCompletedLiveData private var selectedQuestion: Question? = null - fun setData(question: Question?) { + private var fqCodes: NoteFormQuestionCodes? = null + + fun setData(question: Question?, codes: NoteFormQuestionCodes?) { selectedQuestion = question + fqCodes = codes repository.getNotes(countyCode, pollingStationNumber, selectedQuestion) .observeOnce(listObserver) } @@ -71,6 +74,10 @@ class NoteViewModel : BaseFormViewModel() { note.countyCode = countyCode note.description = text note.uriPath = concatFilePathsOrNull() + fqCodes?.let { + note.formCode = it.formCode + note.questionCode = it.questionCode + } repository.saveNote(note) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) diff --git a/app/src/main/res/drawable/thin_border_rectangle.xml b/app/src/main/res/drawable/thin_border_rectangle.xml new file mode 100644 index 00000000..a3e960da --- /dev/null +++ b/app/src/main/res/drawable/thin_border_rectangle.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_note_detail.xml b/app/src/main/res/layout/fragment_note_detail.xml new file mode 100644 index 00000000..4e5fa2a0 --- /dev/null +++ b/app/src/main/res/layout/fragment_note_detail.xml @@ -0,0 +1,26 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_note.xml b/app/src/main/res/layout/item_note.xml index 5e46c0f9..6739a469 100644 --- a/app/src/main/res/layout/item_note.xml +++ b/app/src/main/res/layout/item_note.xml @@ -2,11 +2,12 @@ + tools:text="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book." /> + android:ellipsize="end" + app:layout_constraintEnd_toStartOf="@id/noteDate" + app:layout_constraintHorizontal_chainStyle="spread_inside" + app:layout_constraintBaseline_toBaselineOf="@id/noteDate" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/noteText" + tools:text="Form5 - Q5" /> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_chainStyle="spread_inside" + app:layout_constraintStart_toEndOf="@id/formAndQuestionIdentifier" + app:layout_constraintTop_toBottomOf="@id/noteText" + tools:text="14/05 20:29" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_note_details_image.xml b/app/src/main/res/layout/item_note_details_image.xml new file mode 100644 index 00000000..4dc096ca --- /dev/null +++ b/app/src/main/res/layout/item_note_details_image.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_note_details_text.xml b/app/src/main/res/layout/item_note_details_text.xml new file mode 100644 index 00000000..41fbef78 --- /dev/null +++ b/app/src/main/res/layout/item_note_details_text.xml @@ -0,0 +1,50 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ro-rRO/strings.xml b/app/src/main/res/values-ro-rRO/strings.xml index 73120c90..78320677 100644 --- a/app/src/main/res/values-ro-rRO/strings.xml +++ b/app/src/main/res/values-ro-rRO/strings.xml @@ -126,8 +126,11 @@ Selectează fotografia Te rugăm să instalezi un manager de fișiere. Trimite - Istoricul notelor + Videoclipul atașat nu are un preview. Apăsați aici pentru a îl vizualiza într-un player extern! + Nu există aplicații capabile să afișeze acest videoclip! + Ai nevoie de o conexiune la internet pentru a vedea fișierele atașate ale unei note sincronizate! + Formular %1$s - Întrebare %2$s Simbol clădire @@ -156,4 +159,4 @@ Politică de confidențialitate Trimiteți email prin v%1$S build %2$d aplicație dezvoltată de - + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 917bb7c6..61841dc6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -126,8 +126,11 @@ Select Photo Please install a file manager. Submit - + The attached video doesn\'t have a preview to show. Tap here to see it in an external player! + There aren\'t any apps capable of showing the video! + You need to be connected to the internet to be able to see the attached files of a synchronized note! Notes history + Form %1$s - Question %2$s Building icon diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 168b1b15..f1c3725b 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -144,6 +144,11 @@ 12sp + +