Connecting SUSI iOS App to SUSI Smart Speaker

SUSI Smart Speaker is an Open Source speaker with many exciting features. The user needs an Android or iOS device to set up the speaker. You can refer this post for initial connection to SUSI Smart Speaker. In this post, we will see how a user can connect SUSI Smart Speaker to iOS devices (iPhone/iPad).

Implementation –

The first step is to detect whether an iOS device connects to SUSI.AI hotspot or not. For this, we match the currently connected wifi SSID with SUSI.AI hotspot SSID. If it matches, we show the connected device in Device Activity to proceed further with setups.

Choosing Room –

Room name is basically the location of your SUSI Smart Speaker in the home. You may have multiple SUSI Smart Speaker in different rooms, so the purpose of adding the room is to differentiate between them.

When the user clicks on Wi-Fi displayed cell, it starts the initial setups. We are using didSelectRowAt method of UITableViewDelegate to get which cell is selected. On clicking the displayed Wi-Fi cell, a popup is open with a Room Location Text field.

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row == 0, let speakerSSID = fetchSSIDInfo(), speakerSSID == ControllerConstants.DeviceActivity.susiSSID {
// Open a popup to select Rooms
presentRoomsPopup()
}
}

When the user clicks the Next button, we send the speaker room location to the local server of the speaker by the following API endpoint with room name as a parameter:

http://10.0.0.1:5000/speaker_config/

Refer this post for getting more detail about how choosing room work and how it is implemented in SUSI iOS.

Sharing Wi-Fi Credentials –

On successfully choosing the room, we present a popup that asks the user to enter the Wi-Fi credentials of previously connected Wi-Fi so that we can connect our Smart Speaker to the wifi which can provide internet connection to play music and set commands over the speaker.

We present a popup with a text field for entering wifi password.

When the user clicks the Next button, we share the wifi credentials to wifi by the following API endpoint:

http://10.0.0.1:5000/wifi_credentials/

With the following params-

  1. Wifissid – Connected Wi-Fi SSID
  2. Wifipassd – Connected Wi-Fi password

In this API endpoint, we are sharing wifi SSID and wifi password with Smart Speaker. If the credentials successfully accepted by speaker than we present a popup for user SUSI account password, otherwise we again present Enter Wifi Credentials popup.

Client.sharedInstance.sendWifiCredentials(params) { (success, message) in
DispatchQueue.main.async {
self.alertController.dismiss(animated: true, completion: nil)
if success {
self.presentUserPasswordPopup()
} else {
self.view.makeToast("", point: self.view.center, title: message, image: nil, completion: { didTap in
UIApplication.shared.endIgnoringInteractionEvents()
self.presentWifiCredentialsPopup()
})
}
}
}

 

Sharing SUSI Account Credentials –

In the method above we have seen that when SUSI Smart Speaker accept the wifi credentials, we proceed further with SUSI account credentials. We open a popup to Enter user’s SUSI account password:

When the user clicks the Next button, we use following API endpoint to share user’s SUSI account credentials to SUSI Smart Speaker:

http://10.0.0.1:5000/auth/

With the following params-

  1. email
  2. password

User email is already saved in the device so the user doesn’t have to type it again. If the user credentials successfully accepted by speaker then we proceed with configuration process otherwise we open up Enter Password popup again.

Client.sharedInstance.sendAuthCredentials(params) { (success, message) in
DispatchQueue.main.async {
self.alertController.dismiss(animated: true, completion: nil)
if success {
self.setConfiguration()
} else {
self.view.makeToast("", point: self.view.center, title: message, image: nil, completion: { didTap in
UIApplication.shared.endIgnoringInteractionEvents()
self.presentUserPasswordPopup()
})
}
}
}

 

Setting Configuration –

After successfully sharing SUSI account credentials, following API endpoint is using for setting configuration.

http://10.0.0.1:5000/config/

With the following params-

  1. sst
  2. tts
  3. hotword
  4. wake

The success of this API call makes successfully connection between user iOS Device and SUSI Smart Speaker.

Client.sharedInstance.setConfiguration(params) { (success, message) in
DispatchQueue.main.async {
if success {
// Successfully Configured
self.isSetupDone = true
self.view.makeToast(ControllerConstants.DeviceActivity.doneSetupDetailText)
} else {
self.view.makeToast("", point: self.view.center, title: message, image: nil, completion: { didTap in
UIApplication.shared.endIgnoringInteractionEvents()
})
}
}
}

After successful connection-

 

Resources –

  1. Apple’s Documentation of tableView(_:didSelectRowAt:) API
  2. Initial Setups for Connecting SUSI Smart Speaker with iPhone/iPad
  3. SUSI Linux Link: https://github.com/fossasia/susi_linux
  4. Adding Option to Choose Room for SUSI Smart Speaker in iOS App
Continue Reading
Adding Option to Choose Room for SUSI Smart Speaker in iOS App
SAMSUNG CAMERA PICTURES

Adding Option to Choose Room for SUSI Smart Speaker in iOS App

SUSI Smart Speaker is an open source smart speaker that supports many features. The user can use Android or iOS to connect their device with SUSI Smart Speaker. During initial installation, it is asking from use to enter the Room name. Room name is basically the location of your SUSI Smart Speaker in the home. You may have multiple SUSI Smart Speaker in different rooms, so the purpose of adding the room is to differentiate between them. You can find useful instructions for the initial connection between the iOS device and SUSI Smart Speaker here. It this post, we will see how the adding rooms feature implemented for SUSI iOS.

When the user enters into the Device Activity screen, we check if the iOS device is connected to SUSI.AI Wi-Fi hotspot or not. If the device is connected to SUSI Smart Speaker, it shows the Wi-Fi displayed SSID in Device Activity Screen. On clicking the displayed Wi-Fi cell, a popup is open with a Room Location Text field. The user can enter Room location and by clicking the Next button, proceed further with the setup.

In the popup, there is also an option for choosing rooms, where the list of most common room names is displayed and the user can choose room name from the list.

Presenting Room Picker View Controller –

func presentRoomsPicker() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
if let roomVC = storyboard.instantiateViewController(withIdentifier: "RoomPickerVC") as? RoomPickerController {
roomVC.deviceActivityVC = self
let roomNVC = AppNavigationController(rootViewController: roomVC)
self.present(roomNVC, animated: true)
}
}

Room Picker View Controller is UITableViewController that display the rooms names in table cells. Some of the most common rooms names displayed are:

let rooms: [String] = ["Bedroom", "Kitchen", "Family Room", "Entryway", "Living Room", "Front Yard", "Guest Room", "Dining Room", "Computer Room", "Downstairs", "Front Porch", "Garage", "Hallway", "Driveway"]

 

Presenting Room Cell –

We are using cellForRowAt method to present the cell.

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "roomsCell", for: indexPath)
cell.textLabel?.text = rooms[indexPath.row]
cell.imageView?.image = ControllerConstants.Images.roomsIcon
return cell
}

 

Selecting the room from cell –

When the user clicks on the cell, first willSelectRowAt method use to display the right icon in the accessory view that shows which cell is selected.

if let oldIndex = tableView.indexPathForSelectedRow {
tableView.cellForRow(at: oldIndex)?.accessoryType = .none
}
tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark

We are storing the selected room in the following variable and selecting it by using didSelectRowAt method of UITableView.

var selectedRoom: String?
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedRoom = rooms[indexPath.row]
}

In Room Picker Screen, the user has two option, Cancel and Done. If the user clicks the Cancel, we dismiss the Room Picker screen and display the popup with the empty room location text field and with Choose Room option. If the user clicks the Done button, we store the picked room in UserDefaults shared instance and dismiss Room Picker screen with a different popup which has already filled room location in the text field and without choose room option in the popup as in the image below. By clicking the next, the user proceeds with the further setup.

Resources –

  1. Apple’s Documentations on UserDefaults.
  2. Initial Setups for Connecting SUSI Smart Speaker with iPhone/iPad
  3. Apple’s Documentations on tableView(_:cellForRowAt:)
  4. Apple’s Documentations on tableView(_:willSelectRowAt:)
  5. Apple’s Documentations on tableView(_:didSelectRowAt:)
Continue Reading

Selecting Multiple Tickets in Open Event Android

Open Event Android allows the user to select multiple tickets for an event this blog post will guide you on how its done. Ticket Id and quantity of ticket selected is stored in the form of List of pair. The first element of the List is the Id of the ticket and the second element is its quantity.

private var tickeIdAndQty = ArrayList<Pair<Int, Int>>()

Whenever the user selects any ticket using the dropdown the following snippet of code is executed. handleTicketSelect takes id and quantity of the selected ticket and search if any pair with passed id as the first element exists in the ticketIdAndQty list, if exists it updates that record else it inserts a new pair with given id and quantity into the list.

private fun handleTicketSelect(id: Int, quantity: Int) {
       val pos = ticketIdAndQty.map { it.first }.indexOf(id)
       if (pos == –1) {
           ticketIdAndQty.add(Pair(id, quantity))
       } else {
           ticketIdAndQty[pos] = Pair(id, quantity)
       }

When the user selects register the ticketIdAndQty list which contains details of selected tickets is added to the bundle along with the event id and are passed to attendee fragment. Attendee fragment allows the user to fill in other details such as country, payment option, first name etc.

Later attendee fragment with the help of attendee view model creates individual attendee on the server

rootView.register.setOnClickListener {
           if (!ticketsViewModel.totalTicketsEmpty(ticketIdAndQty)) {
               val fragment = AttendeeFragment()
               val bundle = Bundle()
               bundle.putLong(EVENT_ID, id)
               bundle.putSerializable(TICKET_ID_AND_QTY, ticketIdAndQty)
               fragment.arguments = bundle
               activity?.supportFragmentManager
                       ?.beginTransaction()
                       ?.replace(R.id.rootLayout, fragment)
                       ?.addToBackStack(null)
                       ?.commit()
           } else {
               handleNoTicketsSelected()
           }
       }

When the register is selected an attendee is created for every ticket id inside ticketIdandQty list wherever the second element of the pair that is quantity is greater than zero. FFirst name last name, email and other information is taken from the text views populated on the screen and attendee object is created using these data. Lastly, createAttendee view model function is called passing the generated attendee, country, and selected payment option. The former calls service layer function and created an attendee by making a post request to the server.

 ticketIdAndQty?.forEach {
                   if (it.second > 0) {
                       val attendee = Attendee(id = attendeeFragmentViewModel.getId(),
                               firstname = firstName.text.toString(),
                               lastname = lastName.text.toString(),
                               email = email.text.toString(),
                               ticket = TicketId(it.first.toLong()),
                               event = eventId)
                       val country = if (country.text.isEmpty()) country.text.toString() else null
                       attendeeFragmentViewModel.createAttendee(attendee, id, country, selectedPaymentOption)
                   }
               }

The above-discussed procedure creates one attendee for every ticket quantity which means if a user select ticket id 1 with 3 quantity and ticket id 2 with 5 as quantity total 8 attendees will be generated. Later after successfully generating the attendee’s order is created. Every order has an array of attendees along with other other related information.

Resources

Continue Reading

Offline support for Open Event Android with ROOM

So until now we were fetching data from the server and directly displaying it to the user in the Open Event Android app. There are several problems in this approach if the user changes the fragment and then returns to the same fragment he will have to fetch the data again, valuable network gets wasted. There is also no offline support. So we decided to introduce a local database in the app. ROOM was without a doubt our first choice

What is ROOM?

According to the official documentation,

The Room persistence library provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite.

The library helps you create a cache of your app’s data on a device that’s running your app. This cache, which serves as your app’s single source of truth, allows users to view a consistent copy of key information within your app, regardless of whether users have an internet connection.

Let’s get started

Integration of the ROOM database majorly consists of 3 steps

1 Create an entity

2 Create a DAO

3 Create a Database

That’s it, you are done !

Create the entity

There are just 2 requirements in order to make a model an entity

First use @Entity annotation on the model class to make it an entity. Secondly you need at least one field with @PrimaryKey annotation.

This entity class represents a table in database and all its fields are the columns of the table.

@Type("event")
@JsonNaming(PropertyNamingStrategy.KebabCaseStrategy::class)
@Entity
data class Event(
@Id(LongIdHandler::class)
@PrimaryKey
val id: Long,
val name: String,
val identifier: String,
val startsAt: String)

Create DAO

Data Access Object or DAO for short are used to tell the database how to to put the data.

We can use the following annotations to perform simple SQL queries in ROOM

@Insert, @Update, @Delete for proper actions: inserting, updating and deleting records

@Query for creating queries — we can make select from the database

@Dao
interface EventDao {
@Insert(onConflict = REPLACE)
fun insertEvents(events: List<Event>)

@Insert(onConflict = REPLACE)
fun insertEvent(event: Event)

@Query("DELETE FROM Event")
fun deleteAll()

@Query("SELECT * from Event ORDER BY startsAt DESC")
fun getAllEvents(): Flowable<List<Event>>

@Query("SELECT * from Event WHERE id = :id")
fun getEvent(id: Long): Flowable<Event>
}

Create the Database

We need to create a public abstract class that extends RoomDatabase

After that annotate the class to be a Room database, declare the entities that belong in the database and set the version number. Listing the entities will create tables in the database.

Define the DAOs that work with the database. Make the database a singleton to prevent having multiple instances of the database opened at the same time. A lot has been said let’s have a look at the code now.

@Database(entities = [Event::class, User::class], version = 1)
abstract class OpenEventDatabase : RoomDatabase() {

abstract fun eventDao(): EventDao

abstract fun userDao(): UserDao
}
Room.databaseBuilder(androidApplication(),
OpenEventDatabase::class.java, "open_event_database")
.fallbackToDestructiveMigration()
.build()

The important thing here is that all operations must be done in the background thread. You can do it by using AsyncTask, Handler, RxJava or anything else.

Resources

  1. Room Official Documentation : https://developer.android.com/topic/libraries/architecture/room
  2. Google Code Lab :https://codelabs.developers.google.com/codelabs/android-room-with-a-view/#0
Continue Reading

Creating Orders in Open Event Android

An Order is generated whenever a user buys a ticket in Open Event Android. It contains all the details regarding the tickets and their quantity, also information regarding payment method and relation to list of attendees, through this blog post we will see how Orders are generated in Open Event Android. Implementing Order system can be divided into following parts

  • Writing model class to serialize/deserialize API responses
  • Creating TypeConverter for Object used in Model class
  • Creating the API interface method
  • Wiring everything together

Model Class

Model class server two purpose –

  • Entity class for storing orders in room
  • Serialize / Deserialize API response

The architecture of the Order Model Class depends upon the response returned by the API, different fields inside the Entity Class defines what different attributes an Order consists of and their data types. Since every Order has a relationship with Event and Attendee we also have to define foreign key relations with them. Given below is the implementation of the Order Class in Open Event Android.

@Type(“order”)
@JsonNaming(PropertyNamingStrategy.KebabCaseStrategy::class)
@Entity(foreignKeys = [(ForeignKey(entity = Event::class, parentColumns = [“id”], childColumns = [“event”], onDelete = ForeignKey.CASCADE)), (ForeignKey(entity = Attendee::class, parentColumns = [“id”], childColumns = [“attendees”], onDelete = ForeignKey.CASCADE))])
data class Order(
       @Id(IntegerIdHandler::class)
       @PrimaryKey
       @NonNull
       val id: Long,
       val paymentMode: String? = null,
       val country: String? = null,
       val status: String? = null,
       val amount: Float? = null,
       val orderNotes: String? = null,
       @ColumnInfo(index = true)
       @Relationship(“event”)
       var event: EventId? = null,
       @Relationship(“attendees”)
       var attendees: List<AttendeeId>? = null
)

 

We are using Jackson for serializing/deserializing JSON response, @Type(“order”) annotation tells jackson that the following object is for key order in json response. Since we are using this as our room entity class we will also have to add a @Entity annotation to this class. Order contains attendee and event id fields which are foreign keys to other entity classes, this also has to be explicitly mentioned while writing the @Entitty annotation as shown in the snippet above . All relationship must be annotated with @Relationship annotation. All the variables serves as attributes in the order table and key name for json conversions.

The fields of this class are the attributes for the Order Table. Payment mode, country, status are all made up of primitive data type and hence require no type convertors whereas we will have to specify type converter for objects like eventId and List<Attendees>

Type Converter

Type Converter allows us to store any custom object type inside room database. Essentially we break down the Object into smaller primitive data types that Object is composed of and which can be stored by room database.

To create a TypeConverter we have to add a @TypeConverter annotation to it, this tells room that this is a special function. For every custom Object, you have to create two different TypeConverter functions. One takes the Object and converts it into primitive data type and the other takes the primitive data type and constructs your custom Object from it. For the Order data class we discussed in the above section we will need two type converters, for EventId and List<Attendee> objects. We will take the example of List<Attendee>

class ListAttendeeIdConverter {
   @TypeConverter
   fun fromListAttendeeId(attendeeIdList: List<AttendeeId>): String {
       val objectMapper = ObjectMapper()
       return objectMapper.writeValueAsString(attendeeIdList)
   }
   @TypeConverter
   fun toListAttendeeId(attendeeList: String): List<AttendeeId> {
       val objectMapper = ObjectMapper()
       val mapType = object : TypeReference<List<AttendeeId>>() {}
       return objectMapper.readValue(attendeeList, mapType)
   }

A type converter shows how we can store an object in the form of primitive data type by performing some operation on it. Here we can see that List<AttendeeId> Object is converted into String (primitive data type) using jackson object mapper and similarly we will have to restore or recreate the List<AttendeeId> Object from the string converted output of the same. The first function fromListAttendeeId deals with converting Object into string type and toListAttendeeId deals with converting string output to List<AttendeeId> type Object.

Not that we have created a TypeConverter for our custom Object type we have to add it to the Open Event Database. This can be done by simply adding it to @TypeConverters list separated by commas as shown below.

@TypeConverters(EventIdConverter::class, EventTopicIdConverter::class, TicketIdConverter::class, AttendeeIdConverter::class, ListAttendeeIdConverter::class)

API Interface Method

Till now we have seen how Order body looks like and how we can store it in room database but we would also need an Order API class which specifies which endpoint to hit and what body and response type it is expecting.

Given below is the placeOrder function which hits the Order endpoint (baseURL/orders), with the body as an Order and returns a Single<Order> as a response. Since we are using retrofit for making network requests endpoint path is simply added inside @Path annotation and the body can be passed in parameters of the function using @Body annotation.

interface OrderApi {
   @POST(“orders”)
   fun placeOrder(@Body order: Order): Single<Order>
}

OrderService is the class which exposes the OrderApi functions and make it available to its ViewModel.

The placeOrder function inside the service class takes Order as body parameter, when this function is called it makes a call to the API function to place an order with the passed parameter the response of which is inserted into the database (caching of Orders) also returns the same value

class OrderService(private val orderApi: OrderApi, private val orderDao: OrderDao) {
   fun placeOrder(order: Order): Single<Order> {
       return orderApi.placeOrder(order)
               .map {
                   orderDao.insertOrder(it)
                   it
               }
   }

To create an Order from the Fragment or Activity one call implement the following function createOrder.

createOrder calls the service layer function placeOrder and subscribes to this observable. Here we are observing on main thread because all the UI related changes has to be done from the main thread. As soon as we subscribe to the function we also sets progress.value = true this allows us to show a progress bar on the UI this is changed to false once the response is received (see doFinally).

You can find the following function in the AttendeeViewModel class in Open Event Android project

fun createOrder(order: Order) {
       compositeDisposable.add(orderService.placeOrder(order)
               .subscribeOn(Schedulers.io())
               .observeOn(AndroidSchedulers.mainThread())
               .doOnSubscribe {
                   progress.value = true
               }.doFinally {
                   progress.value = false
               }.subscribe({
                   message.value = “Order created successfully!”
                   Timber.d(“Success placing order!”)
               }, {
                   message.value = “Unable to create Order!”
                   Timber.d(it, “Failed creating Order”)
               }))
   }

Whenever user fills in details of all the attendees sequence of calls and methods are invoked. Firstly attendees are created for the event with ticket details and the data as provided from the UI. On the successful generation of all the attendees ie when total ticket quantity equals the no of attendees an order object is generated with attendees as the list of attendee previously generated and other details as required, this is then passed to createOrder function which internally interacts with the service layer function to create Order.

Resources

Continue Reading

Hiding Payment Options for Free Tickets in Open Event Android

Hiding Payment Options for Free Tickets in Open Event Android

Payment Options spinner allows the user to pick any convenient payment method for an order however, Payment Options should not be shown if the total worth of order is zero or the event is free. This blog post will guide you on how its implemented in Open Event AndroidFollowing are the sequence of steps that are followed for this

  • Allow the user to select tickets and their quantity
  • Use DAO method to get the prices of the tickets selected by the user
  • Iterate through the List and keep storing the prices
  • Display Payment Selector for order with the total amount greater than zero

Given below is DAO method to get the list of prices for different tickets. The method makes a simple select statement and returns price attribute of tickets with id in passed ids. A single list of float is returned by the function.

 @Query(“SELECT price from Ticket WHERE id in (:ids)”)
   fun getTicketPriceWithIds(ids : List<Int>): Single<List<Float>>

This DAO method is then exposed to other parts (ViewModels, Fragments) using service layer class method getTicketPriceWithIds which just makes calls to DAO method and return the result provided by it. Service classes are used for separation of concerns.

fun getTicketPriceWithIds(ids: List<Int>): Single<List<Float>> {
       return ticketsDao.getTicketPriceWithIds(ids)
   }

When a list of tickets and their quantity is specified by the user the prices of the ticket are fetched using the above-explained methods. These are then stored it in a List of pair with the first element being ticket id and second its quantity.

fun updatePaymentSelectorVisibility(ticketIdAndQty: List<Pair<Int, Int>>?) {
       val ticketIds = ArrayList<Int>()
       ticketIdAndQty?.forEach { if (it.second > 0) ticketIds.add(it.first) }
       compositeDisposable.add(ticketService.getTicketPriceWithIds(ticketIds)
               .subscribeOn(Schedulers.io())
               .observeOn(AndroidSchedulers.mainThread())
               .subscribe({
                   var total = 0.toFloat()
                   it?.forEach {
                       if (it.toFloat() > 0) total += it.toFloat()
                   }
                   paymentSelectorVisibility.value = total != 0.toFloat()
               }, {
                   Timber.e(it, “Error Loading tickets!”)
               }))
   }

The above method controls the visibility boolean for payment selector, it essentially iterates over the List<Pair<Int, Int>> and add all the ticket id to a temporary list if quantity for that ticket is greater than zero after this prices for the tickets in temporary list is fetched and sum total is calculated. If this total is greater than zero the visibility boolean is set else reset.

Finally an observable is set on this boolean which automatically updates the UI whenever boolean changes accordingly.

attendeeFragmentViewModel.paymentSelectorVisibility.observe(this, Observer {
           if (it !=null && it) {
               rootView.paymentSelector.visibility = View.VISIBLE
           } else {
               rootView.paymentSelector.visibility = View.GONE
           }
        })

Resources

Continue Reading

Mapping Events to Load from Database

Mapping Events to Load from Database

In Open Event Android whenever App is started events are fetched for the location given by the user, since we have locally added isFavorite extra field for every event it is necessary to be updated for all the events returned in the API response before inserting it into our database otherwise all the favorite related information would be lost. This blog post will guide you on how its done in Open Event Android.

The sequence of steps followed

  • Take the IDs of events being saved into the database
  • Use DAO method which does “SELECT id from Event where favorite = 1 AND id in :eventIds and pass API returned eventIds to this function
  • Set the old favorited on new event objects
  • Save them in database

Let’s see how all of these steps are performed in greater details. Whenever user gives in a location following function is called

fun getEventsByLocation(locationName: String): Single<List<Event>> {
       return eventApi.searchEvents(“name”, locationName).flatMap { apiList ->
           val eventIds = apiList.map { it.id }.toList()
           eventDao.getFavoriteEventWithinIds(eventIds).flatMap { favIds ->
               updateFavorites(apiList, favIds)
           }
       }
   }

Here we are extracting all the Ids of events returned in the API response and then calling getFavoriteEventWithinIds on it. The latter takes the list of eventIds and return the Ids of events which are favorite out of them. This is then passed to the function updateFavorite along with the API returned Events. Following is the implementation of updateFavorite method.

fun updateFavorites(apiEvents: List<Event>, favEventIds: List<Long>): Single<List<Event>> {
       apiEvents.map { if (favEventIds.contains(it.id)) it.favorite = true }
       eventDao.insertEvents(apiEvents)
       val eventIds = apiEvents.map { it.id }.toList()
       return eventDao.getEventWithIds(eventIds)
   }

updateFavorite checks for all events in the list of events whether if its Id is present in the favorite event ids it sets favorite field of that event true. After the favorites for the list of events are updated they are inserted into the database using DAO method insertEvents. The last task is to take the fetch these events again from the database, to do this first we extract the Ids of the events we just inserted into the database and call the DAO method getEventsWithIds passing the extracted eventIds, getEventsWithids simply returns the Events with given Ids.

Given below are the implementations of the functions getEventWithIds and getFavoriteEventWithinIds

@Query(“SELECT * from Event WHERE id in (:ids)”)
   fun getEventWithIds(ids: List<Long>): Single<List<Event>>

@Query(“SELECT id from Event WHERE favorite = 1 AND id in (:ids)”)
   fun getFavoriteEventWithinIds(ids : List<Long>): Single<List<Long>>

getEventWithIds simply makes a select query and checks for events whose ids lies in the ids passed to the method

getFavoriteEventWithinids returns the Ids of the favorite event out of the list of event id passed to the method.

Resources

Continue Reading

Setting an Event Favorite in Open Event Android

The favorite events feature in Open Event Android allows any user to favorite an event and those events can are available in the favorite events fragment which is easily accessible from the bottom navigation bar. This blog article will walk you through on how favorite works in Open Event Android. There are a couple of different ways to do it, in Open Event Android we are keeping track of favorite events using a favorite Boolean stored along with the information of the event ie we have added a favorite boolean field inside the event entity class. However, this method has its own pros and cons. The big plus point of this method is the fact that we can simply check the favorite field of the event object to check if that particular event is favorite or not. But since we have set onConflict strategy as replace in the event insert DAO method (shown below)

 @Insert(onConflict = REPLACE)
   fun insertEvents(events: List<Event>)

This leads to a big problem when the same event is fetched or is present in the response while inserting the favorite field will be set to default value (false) and the favorite information would be lost. Hence extra care needs to be taken while updating events.

Given following is the event model class for serializing / deserializing JSON responses. Note the extra field favorite added here which helps us to provide user favorite feature locally.

@Type(“event”)
@JsonNaming(PropertyNamingStrategy.KebabCaseStrategy::class)
@Entity
data class Event(
      @Id(LongIdHandler::class)
      @PrimaryKey
      val id: Long,
      val name: String,
      val identifier: String,
      val isMapShown: Boolean = false,
      val favorite: Boolean = false
)

Since we added a new field to the Event model class, to access the favorite events we will have to create DAO methods. To fetch all the favorite events we will have to create a Query method which returns all the events wherever favorite property is set. This can be done using a simple select statement which looks for events with favorite boolean set ie. 1, the method implementation is shown below.

@Query(“SELECT * from Event WHERE favorite = 1”)
   fun getFavoriteEvents(): Flowable<List<Event>>

To set an event favorite we will have to add one more method to the EventDao. We can create a generalized method which sets according to the boolean passed as a parameter. The function implementation is shown below, setFavorite takes in EventId, Id of the event which has to be updated and the boolean favorite which is what the new value for the favorite field is. setFavorite makes an SQL update query and matches for EventId and sets favorite for that event.

@Query(“UPDATE Event SET favorite = :favorite WHERE id = :eventId”)
   fun setFavorite(eventId: Long, favorite: Boolean)

In Open Event Android we use the service class which can be used to expose the DAO methods to the rest of the project. The idea behind creating service layer is the separation of concerns this service method is then called from the view model function (following the MVVM approach). Given following is the implementation of the get and set favorite methods.

fun getFavoriteEvents(): Flowable<List<Event>> {
       return eventDao.getFavoriteEvents()
   }
fun setFavorite(eventId: Long, favourite: Boolean): Completable {
       return Completable.fromAction {
           eventDao.setFavorite(eventId, favourite)
       }
   }

The method getFavoriteEvents calls the DAO method to fetch the favorite evens and returns a flowable list of favorite events and setFavorite method generates a completable from the DAO action to favorite any event.

EventViewModel uses these methods and specifies where the observables should be observedOn and subscribedOn also defines what should happen when some error occurs. Given below is implementation for the two methods.

fun setFavorite(eventId: Long, favourite: Boolean) {
       compositeDisposable.add(eventService.setFavorite(eventId, favourite)
               .subscribeOn(Schedulers.io())
               .observeOn(AndroidSchedulers.mainThread())
               .subscribe({
                   Timber.d(“Success”)
               }, {
                   Timber.e(it, “Error”)
                   error.value = “Error”
               }))
   }

For every event, we have a floating action bar to toggle the favorite state of the same whenever user clicks on the FAB it checks the current state of the event and calls the view model function setFavorite passing the Event Id of the clicked event and the negation of current event.favorite state.

val favouriteFabClickListener = object : FavoriteFabListener {
           override fun onClick(eventId: Long, isFavourite: Boolean) {
               eventsViewModel.setFavorite(eventId, !isFavourite)
           }
       }

What we discussed in this blog post works well unless we are not inserting the events with the same event id, this will cause replace on the stored events and current favorite information will be lost. To overcome this limitation we follow the following procedure

  • Fetch event id’s of all the events returned by the API response
  • Use the DAO method to find out the id’s of favorite events out of these
  • Set the favorite in the new events where IDs are in the ids returned in the second point
  • Insert the events into the database
  • Return the events from the database with ID’s as of point one

References

Continue Reading
Close Menu
%d bloggers like this: