Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Make recently checked-in venues selectable #51

Merged
merged 6 commits into from
Jan 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ android {
}

resConfigs "en", "ja"

javaCompileOptions {
annotationProcessorOptions {
arguments += ["room.schemaLocation": "$projectDir/schemas"]
}
}
}

buildTypes {
Expand Down Expand Up @@ -117,4 +123,11 @@ dependencies {
// For encrypting and decrypting credentials
implementation 'com.google.crypto.tink:tink-android:1.7.0'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.4.1"

// Room
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-paging:$room_version"
implementation "androidx.room:room-ktx:$room_version"
implementation 'com.github.anboralabs:spatia-room:0.2.4'
}
64 changes: 64 additions & 0 deletions app/schemas/pub.yusuke.interscheckin.AppDatabase/1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "e1ce587d7ca33db324d2788059075aa5",
"entities": [
{
"tableName": "visited_venues",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `categoriesString` TEXT, `location` BLOB NOT NULL, `icon_name` TEXT, `icon_url` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "categoriesString",
"columnName": "categoriesString",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "location",
"columnName": "location",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "iconName",
"columnName": "icon_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "iconUrl",
"columnName": "icon_url",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"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, 'e1ce587d7ca33db324d2788059075aa5')"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import pub.yusuke.interscheckin.repositories.foursquarecheckins.FoursquarePlaces
import pub.yusuke.interscheckin.repositories.settings.FakeSettingsRepository
import pub.yusuke.interscheckin.repositories.settings.SettingsRepository
import pub.yusuke.interscheckin.repositories.userpreferences.FakeUserPreferencesRepository
import pub.yusuke.interscheckin.repositories.visitedvenues.FakeVisitedVenueDao
import pub.yusuke.interscheckin.repositories.visitedvenues.VisitedVenueDao
import javax.inject.Singleton

@Module
Expand Down Expand Up @@ -41,4 +43,10 @@ interface FakeMainApplicationModule {
fun bindFoursquarePlacesRepository(
foursquarePlacesRepository: FakeFoursquarePlacesRepository
): FoursquarePlacesRepository

@Singleton
@Binds
fun bindVisitedVenueDao(
visitedVenueDao: FakeVisitedVenueDao
): VisitedVenueDao
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ class FakeFoursquareCheckinsRepository @Inject constructor() : FoursquareCheckin
name = "good venue",
location = Checkin.V2Venue.Location(
state = "茨城県",
city = "つくば市"
city = "つくば市",
lat = 36.107565,
lng = 140.104733
),
categories = listOf(
Checkin.V2Venue.Category(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package pub.yusuke.interscheckin.repositories.visitedvenues

import co.anbora.labs.spatia.geometry.Point
import javax.inject.Inject

class FakeVisitedVenueDao @Inject constructor() : VisitedVenueDao {
override suspend fun findByLatLong(
latitude: Double,
longitude: Double,
nameLike: String,
limit: Long
): List<VisitedVenue> = listOf(
VisitedVenue(
id = "venue_id",
name = "An visited Venue",
categoriesString = "Intersection",
location = Point(0.0, 0.0),
iconName = "Intersection",
iconUrl = "https://example.com/visited_venue.png"
)
)

override suspend fun insertVisitedVenues(visitedVenues: List<VisitedVenue>) {}

override suspend fun deleteAll() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.performTouchInput
import androidx.navigation.NavController
import androidx.test.espresso.action.ViewActions.longClick
import com.google.android.gms.location.FusedLocationProviderClient
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
Expand Down
20 changes: 20 additions & 0 deletions app/src/main/java/pub/yusuke/interscheckin/AppDatabase.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package pub.yusuke.interscheckin

import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import co.anbora.labs.spatia.geometry.GeometryConverters
import pub.yusuke.interscheckin.repositories.visitedvenues.VisitedVenue
import pub.yusuke.interscheckin.repositories.visitedvenues.VisitedVenueDao

@Database(
entities = [
VisitedVenue::class
],
version = 1,
exportSchema = true
)
@TypeConverters(GeometryConverters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun visitedVenueDao(): VisitedVenueDao
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import android.content.Context
import android.os.VibratorManager
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import co.anbora.labs.spatia.builder.SpatiaRoom
import com.google.android.gms.location.LocationServices
import com.google.crypto.tink.Aead
import com.google.crypto.tink.KeyTemplates
Expand All @@ -28,6 +31,7 @@ import pub.yusuke.interscheckin.repositories.settings.SettingsPreferencesSeriali
import pub.yusuke.interscheckin.repositories.settings.SettingsRepository
import pub.yusuke.interscheckin.repositories.settings.SettingsRepositoryImpl
import pub.yusuke.interscheckin.repositories.userpreferences.UserPreferencesRepositoryImpl
import pub.yusuke.interscheckin.repositories.visitedvenues.VisitedVenue
import java.io.File
import javax.inject.Singleton

Expand Down Expand Up @@ -123,4 +127,31 @@ class MainApplicationModule {
fun provideFusedLocationProviderClient(
@ApplicationContext context: Context
) = LocationServices.getFusedLocationProviderClient(context)

@Singleton
@Provides
fun provideAppDatabase(
@ApplicationContext context: Context
) = SpatiaRoom.databaseBuilder(
context,
AppDatabase::class.java,
"interscheckin"
).addCallback(object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
db.query(
"""
select
RecoverGeometryColumn('visited_venues', 'location', ${VisitedVenue.SRID}, 'POINT', 'XY');
"""
).moveToNext()
db.query("select CreateSpatialIndex('visited_venues', 'location');").moveToNext()
}
}).build()

@Singleton
@Provides
fun provideVisitedVenueDao(
appDatabase: AppDatabase
) = appDatabase.visitedVenueDao()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package pub.yusuke.interscheckin.repositories.visitedvenues

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import co.anbora.labs.spatia.geometry.Point

@Entity(tableName = VisitedVenue.TABLE_NAME)
data class VisitedVenue(
@PrimaryKey
val id: String,
val name: String,
val categoriesString: String? = null,
val location: Point,
@ColumnInfo(name = "icon_name")
val iconName: String?,
@ColumnInfo(name = "icon_url")
val iconUrl: String?
) {
companion object {
const val TABLE_NAME = "visited_venues"
const val SRID = 4326
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package pub.yusuke.interscheckin.repositories.visitedvenues

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.SkipQueryVerification

@Dao
interface VisitedVenueDao {
@Query(
"""
select
*
from
${VisitedVenue.TABLE_NAME}
where
name like '%' || :nameLike || '%'
order by
distance(
setsrid(location, ${VisitedVenue.SRID}),
makepoint(:latitude, :longitude, ${VisitedVenue.SRID})
) asc
limit :limit
"""
)
@SkipQueryVerification
suspend fun findByLatLong(
latitude: Double,
longitude: Double,
nameLike: String = "",
limit: Long = 10
): List<VisitedVenue>

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertVisitedVenues(visitedVenues: List<VisitedVenue>)

@Query("delete from ${VisitedVenue.TABLE_NAME}")
suspend fun deleteAll()
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
package pub.yusuke.interscheckin.ui.histories

import co.anbora.labs.spatia.geometry.Point
import pub.yusuke.foursquareclient.models.Checkin
import pub.yusuke.foursquareclient.models.url
import pub.yusuke.interscheckin.repositories.foursquarecheckins.FoursquareCheckinsRepository
import pub.yusuke.interscheckin.repositories.visitedvenues.VisitedVenue
import pub.yusuke.interscheckin.repositories.visitedvenues.VisitedVenueDao
import javax.inject.Inject

class HistoriesPaging @Inject constructor(
private val foursquareCheckinsRepository: FoursquareCheckinsRepository
private val foursquareCheckinsRepository: FoursquareCheckinsRepository,
private val visitedVenueDao: VisitedVenueDao
) : HistoriesContract.Paging {
override suspend fun load(
userId: Long?,
beforeTimestamp: Long?,
offset: Long?,
perPage: Long
): List<HistoriesContract.Checkin> =
foursquareCheckinsRepository.getCheckins(
): List<HistoriesContract.Checkin> {
val checkins = foursquareCheckinsRepository.getCheckins(
userId = userId,
offset = offset,
beforeTimestamp = beforeTimestamp,
limit = perPage
).translateToCheckins()
)
visitedVenueDao.insertVisitedVenues(checkins.translateToVisitedVenues())

return checkins.translateToCheckins()
}

private fun List<Checkin>.translateToCheckins(): List<HistoriesContract.Checkin> =
this.map { it.translateToCheckin() }
Expand All @@ -37,7 +45,7 @@ class HistoriesPaging @Inject constructor(
HistoriesContract.Checkin.Venue(
id = this.id,
name = this.name,
address = this.location?.let {
address = this.location.let {
listOfNotNull(it.state, it.city).joinToString(" ")
},
categoriesString = this.categories.joinToString(", ") { it.name },
Expand All @@ -49,4 +57,20 @@ class HistoriesPaging @Inject constructor(
name = this.name,
url = this.icon.url()
)

private fun List<Checkin>.translateToVisitedVenues(): List<VisitedVenue> =
map { it.translateToVisitedVenue() }

private fun Checkin.translateToVisitedVenue(): VisitedVenue =
VisitedVenue(
id = this.venue.id,
name = this.venue.name,
categoriesString = this.venue.categories.joinToString(", ") { it.name },
location = Point(
this.venue.location.lat,
this.venue.location.lng
),
iconName = this.venue.categories.firstOrNull()?.name,
iconUrl = this.venue.categories.firstOrNull()?.icon?.url()
)
}
Loading