diff --git a/android-app/app/build.gradle b/android-app/app/build.gradle index 9724b3e..8a1d2f6 100644 --- a/android-app/app/build.gradle +++ b/android-app/app/build.gradle @@ -52,11 +52,6 @@ android { } } - - buildFeatures { // https://developer.android.com/studio/releases/gradle-plugin#buildFeatures - dataBinding = true - } - buildTypes { release { // https://developer.android.com/studio/build/shrink-code @@ -85,6 +80,14 @@ android { jvmTarget = '1.8' } + buildFeatures { // https://developer.android.com/studio/releases/gradle-plugin#buildFeatures + dataBinding = true + } + + androidExtensions { // https://plugins.gradle.org/plugin/org.jetbrains.kotlin.android.extensions + features = ["parcelize"] + } + lintOptions { // https://developer.android.com/studio/write/lint.html checkAllWarnings true warningsAsErrors true diff --git a/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/analytics/Analytics.kt b/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/analytics/Analytics.kt index 3d82c6d..9f9b772 100644 --- a/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/analytics/Analytics.kt +++ b/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/analytics/Analytics.kt @@ -11,6 +11,7 @@ interface Analytics { const val SCREEN_INCIDENT_LOCATION = "IncidentLocations" const val SCREEN_INCIDENT_LIST_BY_DATE = "IncidentsByDate" const val SCREEN_INCIDENT_LIST_BY_LOCATION = "IncidentsByLocation" + const val SCREEN_INCIDENT_LIST_MOST_RECENT = "IncidentsByRecent" const val SCREEN_INCIDENT_DATE_FILTER = "FilterIncidentsByDate" const val SCREEN_MORE_INFO = "MoreInformation" const val SCREEN_ABOUT_APP = "AboutApplication" diff --git a/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/data/BrutalityIncidentRepository.kt b/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/data/BrutalityIncidentRepository.kt index 0d83147..972babc 100644 --- a/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/data/BrutalityIncidentRepository.kt +++ b/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/data/BrutalityIncidentRepository.kt @@ -23,6 +23,10 @@ class BrutalityIncidentRepository @Inject constructor( return incidentDao.getIncidentsByDate(timeStamp) } + override fun getIncidentsRecentFirst(): LiveData> { + return incidentDao.getIncidentsRecentFirst() + } + override fun getLocations(): LiveData> { return incidentDao.getUniqueStates() } diff --git a/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/data/IncidentDao.kt b/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/data/IncidentDao.kt index 44d6bd1..8dc4c22 100644 --- a/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/data/IncidentDao.kt +++ b/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/data/IncidentDao.kt @@ -33,6 +33,9 @@ interface IncidentDao { @Query("SELECT * FROM incidents WHERE DATE(date) = DATE(DATETIME(:timestamp, 'unixepoch')) ORDER BY name") fun getIncidentsByDate(timestamp: Long): LiveData> + @Query("SELECT * FROM incidents WHERE 1 ORDER BY date DESC") + fun getIncidentsRecentFirst(): LiveData> + @Query("SELECT COUNT(id) FROM incidents") suspend fun getTotalRecords(): Int diff --git a/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/data/IncidentRepository.kt b/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/data/IncidentRepository.kt index 227cae2..5b3450c 100644 --- a/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/data/IncidentRepository.kt +++ b/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/data/IncidentRepository.kt @@ -8,6 +8,7 @@ interface IncidentRepository { fun getIncidents(): LiveData> fun getStateIncidents(state: String): LiveData> fun getIncidentsByDate(timeStamp: Long): LiveData> + fun getIncidentsRecentFirst(): LiveData> fun getLocations(): LiveData> fun getTotalIncidentsOnDate(timeStamp: Long): LiveData fun getIncidentDates(): LiveData> diff --git a/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/ui/incident/IncidentViewModel.kt b/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/ui/incident/IncidentViewModel.kt index 4d916fd..af61e37 100644 --- a/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/ui/incident/IncidentViewModel.kt +++ b/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/ui/incident/IncidentViewModel.kt @@ -13,6 +13,7 @@ import com.blacklivesmatter.policebrutality.config.PREF_KEY_SHARE_CAPABILITY_REM import com.blacklivesmatter.policebrutality.data.IncidentRepository import com.blacklivesmatter.policebrutality.data.model.Incident import com.blacklivesmatter.policebrutality.ui.extensions.LiveEvent +import com.blacklivesmatter.policebrutality.ui.incident.arg.FilterType import timber.log.Timber class IncidentViewModel @ViewModelInject constructor( @@ -38,9 +39,10 @@ class IncidentViewModel @ViewModelInject constructor( } fun setArgs(navArgs: IncidentsFragmentArgs) { - navArgs.stateName?.let { selectedSate(it) } - if (navArgs.timestamp != 0L) { - selectedTimestamp(navArgs.timestamp) + when (navArgs.filterArgs.type) { + FilterType.STATE -> selectedSate(navArgs.filterArgs.stateName!!) + FilterType.DATE -> selectedTimestamp(navArgs.filterArgs.timestamp!!) + FilterType.LATEST -> selectedMostRecentIncidents() } val isMessageShown = preferences.getBoolean(PREF_KEY_SHARE_CAPABILITY_REMINDER_SHOWN, false) @@ -68,4 +70,11 @@ class IncidentViewModel @ViewModelInject constructor( _incidents.value = it } } + + private fun selectedMostRecentIncidents() { + _incidents.addSource(incidentRepository.getIncidentsRecentFirst()) { + Timber.d("Incidents Updated ") + _incidents.value = it + } + } } diff --git a/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/ui/incident/IncidentsAdapter.kt b/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/ui/incident/IncidentsAdapter.kt index fc65ebd..e50c4c8 100644 --- a/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/ui/incident/IncidentsAdapter.kt +++ b/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/ui/incident/IncidentsAdapter.kt @@ -9,9 +9,10 @@ import com.blacklivesmatter.policebrutality.R import com.blacklivesmatter.policebrutality.data.model.Incident import com.blacklivesmatter.policebrutality.databinding.ListItemIncidentCoreBinding import com.blacklivesmatter.policebrutality.ui.common.DataBoundListAdapter +import com.blacklivesmatter.policebrutality.ui.incident.arg.FilterType class IncidentsAdapter constructor( - private val isDateBasedIncidents: Boolean, + private val incidentFilterType: FilterType, private val itemClickCallback: ((Incident) -> Unit)?, private val linkClickCallback: ((String) -> Unit)? = null ) : DataBoundListAdapter( @@ -42,7 +43,7 @@ class IncidentsAdapter constructor( override fun bind(binding: ListItemIncidentCoreBinding, item: Incident) { binding.incident = item - binding.isDateBased = isDateBasedIncidents + binding.shouldShowCityAndState = incidentFilterType != FilterType.STATE val adapter = IncidentLinkAdapter(itemClickCallback = linkClickCallback) binding.linksRecyclerView.layoutManager = LinearLayoutManager(binding.root.context) diff --git a/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/ui/incident/IncidentsFragment.kt b/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/ui/incident/IncidentsFragment.kt index 8f5b583..53a4a8b 100644 --- a/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/ui/incident/IncidentsFragment.kt +++ b/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/ui/incident/IncidentsFragment.kt @@ -16,6 +16,7 @@ import com.blacklivesmatter.policebrutality.analytics.Analytics.Companion.CONTEN import com.blacklivesmatter.policebrutality.data.model.Incident import com.blacklivesmatter.policebrutality.databinding.FragmentIncidentsBinding import com.blacklivesmatter.policebrutality.ui.extensions.observeKotlin +import com.blacklivesmatter.policebrutality.ui.incident.arg.FilterType import com.blacklivesmatter.policebrutality.ui.util.IntentBuilder import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint @@ -44,7 +45,7 @@ class IncidentsFragment : Fragment() { viewModel.setArgs(navArgs) adapter = IncidentsAdapter( - isDateBasedIncidents = navArgs.isDateBased(), + incidentFilterType = navArgs.filterArgs.type, itemClickCallback = { clickedIncident -> Timber.d("Selected Incident: $clickedIncident") analytics.logSelectItem( @@ -95,12 +96,7 @@ class IncidentsFragment : Fragment() { override fun onStart() { super.onStart() - activity?.let { - analytics.logPageView( - it, if (navArgs.isDateBased()) Analytics.SCREEN_INCIDENT_LIST_BY_DATE - else Analytics.SCREEN_INCIDENT_LIST_BY_LOCATION - ) - } + activity?.let { analytics.logPageView(it, navArgs.screenName()) } } private fun showIncidentDetailsForSharing(incident: Incident) { @@ -133,9 +129,27 @@ class IncidentsFragment : Fragment() { } } - private fun IncidentsFragmentArgs.isDateBased(): Boolean = timestamp != 0L - private fun IncidentsFragmentArgs.titleResId(): Int = - if (isDateBased()) R.string.title_incidents_on_date else R.string.title_incidents_at_location + private fun IncidentsFragmentArgs.titleResId(): Int { + return when (filterArgs.type) { + FilterType.STATE -> R.string.title_incidents_at_location + FilterType.DATE -> R.string.title_incidents_on_date + FilterType.LATEST -> R.string.title_incidents_most_recent + } + } + + private fun IncidentsFragmentArgs.titleText(): String { + return when (filterArgs.type) { + FilterType.STATE -> filterArgs.stateName!! + FilterType.DATE -> filterArgs.dateText!! + FilterType.LATEST -> "" + } + } - private fun IncidentsFragmentArgs.titleText(): String = if (isDateBased()) dateText!! else stateName!! + private fun IncidentsFragmentArgs.screenName(): String { + return when (filterArgs.type) { + FilterType.STATE -> Analytics.SCREEN_INCIDENT_LIST_BY_LOCATION + FilterType.DATE -> Analytics.SCREEN_INCIDENT_LIST_BY_DATE + FilterType.LATEST -> Analytics.SCREEN_INCIDENT_LIST_MOST_RECENT + } + } } diff --git a/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/ui/incident/arg/FilterType.kt b/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/ui/incident/arg/FilterType.kt new file mode 100644 index 0000000..76a1aef --- /dev/null +++ b/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/ui/incident/arg/FilterType.kt @@ -0,0 +1,48 @@ +/* + * MIT License + * + * Copyright (c) 2020 Hossain Khan + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.blacklivesmatter.policebrutality.ui.incident.arg + +import android.os.Parcelable +import androidx.annotation.Keep +import kotlinx.android.parcel.Parcelize + +@Parcelize +@Keep +enum class FilterType : Parcelable { + /** + * Shows incidents by selected State + */ + STATE, + + /** + * Shows incidents for specific date + */ + DATE, + + /** + * Shows most recent incidents first + */ + LATEST +} diff --git a/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/ui/incident/arg/LocationFilterArgs.kt b/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/ui/incident/arg/LocationFilterArgs.kt new file mode 100644 index 0000000..3c1fc50 --- /dev/null +++ b/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/ui/incident/arg/LocationFilterArgs.kt @@ -0,0 +1,45 @@ +/* + * MIT License + * + * Copyright (c) 2020 Hossain Khan + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.blacklivesmatter.policebrutality.ui.incident.arg + +import android.os.Parcelable +import androidx.annotation.Keep +import com.blacklivesmatter.policebrutality.ui.incident.IncidentsFragment +import kotlinx.android.parcel.Parcelize + +/** + * Navigation args for [IncidentsFragment] to show different kind of filtered incidents. + * 1. [FilterType.STATE] - where `[stateName]` required and provided. + * 2. [FilterType.DATE] - where both `[timestamp]` and `[dateText]` are required and provided. + * 3. [FilterType.LATEST] - No data required, shows most recent incident first. + */ +@Parcelize +@Keep +data class LocationFilterArgs( + val type: FilterType, + val stateName: String? = null, + val timestamp: Long? = null, + val dateText: String? = null +) : Parcelable diff --git a/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/ui/incidentlocations/LocationFragment.kt b/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/ui/incidentlocations/LocationFragment.kt index 2a37f13..41c3771 100644 --- a/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/ui/incidentlocations/LocationFragment.kt +++ b/android-app/app/src/main/java/com/blacklivesmatter/policebrutality/ui/incidentlocations/LocationFragment.kt @@ -17,6 +17,8 @@ import com.blacklivesmatter.policebrutality.analytics.Analytics import com.blacklivesmatter.policebrutality.config.THE_846_DAY import com.blacklivesmatter.policebrutality.databinding.FragmentIncidentLocationsBinding import com.blacklivesmatter.policebrutality.ui.extensions.observeKotlin +import com.blacklivesmatter.policebrutality.ui.incident.arg.FilterType +import com.blacklivesmatter.policebrutality.ui.incident.arg.LocationFilterArgs import com.blacklivesmatter.policebrutality.ui.incidentlocations.LocationViewModel.NavigationEvent import com.blacklivesmatter.policebrutality.ui.incidentlocations.LocationViewModel.RefreshEvent import com.blacklivesmatter.policebrutality.ui.util.IncidentAvailabilityValidator @@ -34,6 +36,9 @@ import java.util.ArrayList import java.util.Calendar import javax.inject.Inject +/** + * Incidents by US States (location). + */ @AndroidEntryPoint class LocationFragment : Fragment() { @Inject @@ -60,7 +65,9 @@ class LocationFragment : Fragment() { Timber.d("Tapped on state item $state") analytics.logSelectItem(Analytics.CONTENT_TYPE_LOCATION, state.stateName, state.stateName) findNavController().navigate( - LocationFragmentDirections.navigationToIncidentsFragment(stateName = state.stateName) + LocationFragmentDirections.navigationToIncidentsFragment( + LocationFilterArgs(type = FilterType.STATE, stateName = state.stateName) + ) ) } adapter.submitList(emptyList()) @@ -100,8 +107,11 @@ class LocationFragment : Fragment() { Timber.d("Navigate incident list for $navigationEvent") findNavController().navigate( LocationFragmentDirections.navigationToIncidentsFragment( - timestamp = navigationEvent.timestamp, - dateText = navigationEvent.dateText + LocationFilterArgs( + type = FilterType.DATE, + timestamp = navigationEvent.timestamp, + dateText = navigationEvent.dateText + ) ) ) } @@ -115,6 +125,14 @@ class LocationFragment : Fragment() { } setupSwipeRefreshAction() + + viewDataBinding.showLatestIncidentsFab.setOnClickListener { + findNavController().navigate( + LocationFragmentDirections.navigationToIncidentsFragment( + LocationFilterArgs(type = FilterType.LATEST) + ) + ) + } } override fun onStart() { diff --git a/android-app/app/src/main/res/drawable-xxxhdpi/ic_protest_strike.webp b/android-app/app/src/main/res/drawable-xxxhdpi/ic_protest_strike.webp new file mode 100644 index 0000000..bd9e2e8 Binary files /dev/null and b/android-app/app/src/main/res/drawable-xxxhdpi/ic_protest_strike.webp differ diff --git a/android-app/app/src/main/res/layout/fragment_incident_locations.xml b/android-app/app/src/main/res/layout/fragment_incident_locations.xml index 27f04c8..1a6e51e 100644 --- a/android-app/app/src/main/res/layout/fragment_incident_locations.xml +++ b/android-app/app/src/main/res/layout/fragment_incident_locations.xml @@ -76,5 +76,20 @@ tools:itemCount="7" tools:listitem="@layout/list_item_location" /> + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/layout/list_item_incident_core.xml b/android-app/app/src/main/res/layout/list_item_incident_core.xml index de2cf20..1bb4bfa 100644 --- a/android-app/app/src/main/res/layout/list_item_incident_core.xml +++ b/android-app/app/src/main/res/layout/list_item_incident_core.xml @@ -8,7 +8,7 @@ - - - - - + android:name="filterArgs" + app:argType="com.blacklivesmatter.policebrutality.ui.incident.arg.LocationFilterArgs" /> diff --git a/android-app/app/src/main/res/values/strings.xml b/android-app/app/src/main/res/values/strings.xml index ca87218..1a16677 100644 --- a/android-app/app/src/main/res/values/strings.xml +++ b/android-app/app/src/main/res/values/strings.xml @@ -6,6 +6,7 @@ Reported Incidents Incidents at %s Incidents on %s + Latest Incidents Report Incident Additional Resources Donate for Cause @@ -40,6 +41,7 @@ Donate to support the cause George Floyd protest image with protesters and police Go back to previous screen + Show most recent incidents External links on this incident @@ -52,6 +54,7 @@ Share Incident Learn More Donate + Recent #BlackLivesMatter