Skip to content

Commit

Permalink
Merge pull request #200 from amardeshbd/develop
Browse files Browse the repository at this point in the history
Prepare for release v2.5
  • Loading branch information
hossain-khan authored Jul 9, 2020
2 parents 22cb3e0 + d0ec8e8 commit 9a6661f
Show file tree
Hide file tree
Showing 40 changed files with 8,812 additions and 92 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Android CI

on:
push:
branches: [ master, develop ]
branches: [ main, develop ]
pull_request:
branches: [ master, develop ]
branches: [ main, develop ]

jobs:
build:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/firebase_app_dist.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ on:
push:
branches:
# https://help.github.com/en/actions/reference/events-that-trigger-workflows
# Trigger the workflow on push only for the master branch for Firebase App Distribution
- master
# Trigger the workflow on push only for the `main` branch for Firebase App Distribution
- main

jobs:
build:
Expand Down
29 changes: 17 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[![Android CI](https://github.com/amardeshbd/android-police-brutality-incidents/workflows/Android%20CI/badge.svg)](https://github.com/amardeshbd/android-police-brutality-incidents/actions?query=workflow%3A%22Android+CI%22) [![Android Lint](https://github.com/amardeshbd/android-police-brutality-incidents/workflows/Android%20Lint/badge.svg)](https://github.com/amardeshbd/android-police-brutality-incidents/actions?query=workflow%3A%22Android+Lint%22) [![codecov](https://codecov.io/gh/amardeshbd/android-police-brutality-incidents/branch/develop/graph/badge.svg)](https://codecov.io/gh/amardeshbd/android-police-brutality-incidents)

# 2020PB - Android App

Android client app for https://github.com/2020PB/police-brutality (Repository containing evidence of police brutality during the 2020 George Floyd protests)
Expand All @@ -21,18 +23,21 @@ This allows people to easily access data on the go, and allow them to easily rep

Some of the key features based on data available though API is

* Browse reported incidents by area (eg. State)
* Filter reported incidents by date
* Sync latest incidents from server though API
* Auto update latest incidents if it hasn't been updated for a while
* Visit external links of related to incidents via native app, or web browser
* Allows the user to share a single incident with all the associated information
* Allows user to report incident by opening official incident report form
* Shows list of reknown charitable organizations that work for `#BlackLivesMatter` or in related area
* Allows the users to explore more about charities or donate to them
* Provides bit more information about the `/r/2020PoliceBrutality` reddit community with social links
* Allows the user to copy popular hash tags
* Provides app's information with option to share with friends and family
* :scroll: Browse all the reported incidents
* By US State
* By specific date
* By recent incidents
* :calling: Sync latest incidents from server though API
* :arrows_counterclockwise: Auto update latest incidents if it hasn't been updated for a while
* :earth_americas: Visit external links of related to incidents via native app, or web browser
* :incoming_envelope: Allows the user to share a single incident with all the associated information
* :writing_hand: Allows user to report incident by opening official incident report form
* :eyes: Shows list of renown charitable organizations that work for `#BlackLivesMatter` or in related area
* :pray: Allows the users to explore more about charities or donate to them
* :fist_raised: Provides bit more information about the `/r/2020PoliceBrutality` reddit community with social links
* :hash: Allows the user to copy popular hash tags
* :heart: Provides app's information with option to share with friends and family
* :waxing_crescent_moon: Full dark mode support for device battery and eye

## Technical Details

Expand Down
2 changes: 1 addition & 1 deletion android-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ You can just run following gradle command to make local build

#### Firebase App Distribution

Any merge to `master` builds release build that is available in Firebase App Distribution [self-serve signup page](https://appdistribution.firebase.dev/i/5d2cb8359305f7e7).
Any merge to `main` branch builds release build that is available in Firebase App Distribution [self-serve signup page](https://appdistribution.firebase.dev/i/5d2cb8359305f7e7).


#### Release Builds
Expand Down
19 changes: 12 additions & 7 deletions android-app/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ android {
applicationId "com.blacklivesmatter.policebrutality"
minSdkVersion rootProject.minSdkVersion
targetSdkVersion rootProject.targetSdkVersion
versionCode 11
versionName "2.4"
versionCode 12
versionName "2.5"

// https://developer.android.com/studio/build/shrink-code#unused-alt-resources
resConfigs "en"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
Expand All @@ -52,11 +53,6 @@ android {
}
}


buildFeatures { // https://developer.android.com/studio/releases/gradle-plugin#buildFeatures
dataBinding = true
}

buildTypes {
release {
// https://developer.android.com/studio/build/shrink-code
Expand Down Expand Up @@ -85,6 +81,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
Expand All @@ -109,6 +113,7 @@ dependencies {
implementation "androidx.cardview:cardview:$rootProject.cardViewVersion"
implementation "androidx.preference:preference-ktx:$rootProject.preferenceVersion"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.recyclerview:recyclerview:1.1.0"

// Firebase
implementation "com.google.firebase:firebase-analytics-ktx:$rootProject.firebaseAnalyticsVersion"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,23 @@ 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"
const val SCREEN_CHARITY_ORGANIZATIONS = "CharityOrganizations"

const val ACTION_INCIDENT_REFRESH = "RefreshIncidents"
const val ACTION_INCIDENT_REPORT_NEW = "MakeIncidentReport"
const val ACTION_SHARE_APP = "ShareApplication"
const val ACTION_CHARITY_DONATE = "DonateCharity"
const val ACTION_CHARITY_DONATE_INFO = "DonateCharityInfo"
//
// Based on existing firebase analytics action naming convention, using `_` and all lowercase
// https://support.google.com/firebase/answer/6317498
//
const val ACTION_INCIDENT_REFRESH = "incidents_refresh"
const val ACTION_INCIDENT_REPORT_NEW = "incidents_report_new"
const val ACTION_INCIDENT_FILTER_DATE = "filter_by_date"
const val ACTION_INCIDENT_FILTER_RECENT = "filter_by_recent"
const val ACTION_SHARE_APP = "app_share"
const val ACTION_CHARITY_DONATE = "charity_donate"
const val ACTION_CHARITY_DONATE_INFO = "charity_org_info"

const val CONTENT_TYPE_LOCATION = "TypeLocation"
const val CONTENT_TYPE_CHARITY = "TypeCharity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ val TODAY: OffsetDateTime by lazy {
*/
const val DATABASE_NAME = "incidents-db"

/**
* Number of maximum incidents to load when showing latest incidents.
*/
const val LATEST_INCIDENT_LIMIT = 50

/**
* Fallback data file incidents, used to preload [AppDatabase].
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ class BrutalityIncidentRepository @Inject constructor(
return incidentDao.getIncidentsByDate(timeStamp)
}

override fun getIncidentsRecentFirst(limit: Int): LiveData<List<Incident>> {
return incidentDao.getIncidentsRecentFirst(limit)
}

override fun getLocations(): LiveData<List<LocationIncidents>> {
return incidentDao.getUniqueStates()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<List<Incident>>

@Query("SELECT * FROM incidents WHERE 1 ORDER BY date DESC LIMIT :limit")
fun getIncidentsRecentFirst(limit: Int): LiveData<List<Incident>>

@Query("SELECT COUNT(id) FROM incidents")
suspend fun getTotalRecords(): Int

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface IncidentRepository {
fun getIncidents(): LiveData<List<Incident>>
fun getStateIncidents(state: String): LiveData<List<Incident>>
fun getIncidentsByDate(timeStamp: Long): LiveData<List<Incident>>
fun getIncidentsRecentFirst(limit: Int): LiveData<List<Incident>>
fun getLocations(): LiveData<List<LocationIncidents>>
fun getTotalIncidentsOnDate(timeStamp: Long): LiveData<Int>
fun getIncidentDates(): LiveData<List<String>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.blacklivesmatter.policebrutality.analytics.Analytics
import com.blacklivesmatter.policebrutality.config.LATEST_INCIDENT_LIMIT
import com.blacklivesmatter.policebrutality.config.PREF_KEY_SHARE_CAPABILITY_REMINDER_SHOWN
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(
private val analytics: Analytics,
private val incidentRepository: IncidentRepository,
private val preferences: SharedPreferences
) : ViewModel() {
Expand All @@ -31,13 +35,15 @@ class IncidentViewModel @ViewModelInject constructor(

fun onShareIncidentClicked(incident: Incident) {
Timber.d("User clicked on share incident")
analytics.logShare(Analytics.CONTENT_TYPE_INCIDENT_SHARE, incident.incident_id ?: "---")
_shareIncident.value = incident
}

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)
Expand Down Expand Up @@ -65,4 +71,11 @@ class IncidentViewModel @ViewModelInject constructor(
_incidents.value = it
}
}

private fun selectedMostRecentIncidents() {
_incidents.addSource(incidentRepository.getIncidentsRecentFirst(limit = LATEST_INCIDENT_LIMIT)) {
Timber.d("Incidents Updated ")
_incidents.value = it
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Incident, ListItemIncidentCoreBinding>(
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 9a6661f

Please sign in to comment.