Tickets fragment in the Open Event Android App

Ticketing is one of the most important part of any event management system. In the Open Event Android App you can view all the ticket details of any event. Let’s see how this was accomplished.

This is how our TicketApi class looks. We are sending a GET request to get the list of all tickets associated with an event. Id here is the event id which is used to uniquely identify an event.

interface TicketApi {

@GET("events/{id}/tickets?include=event&fields[event]=id&page[size]=0")
fun getTickets(@Path("id") id: Long): Flowable<List<Ticket>>

}

 

In the ticket details screen we see the event details on top. We are loading these details from the event which is stored in the database, we use the event id to query the database and load these details.

This is how the events are loaded from the database. We are observing the live data variable event and calling the loadEventDetails methods as soon as the value of the variable changes.

ticketsViewModel.event.observe(this, Observer {
it?.let { loadEventDetails(it) }
})

ticketsViewModel.loadEvent(id)

 

Let’s have a look at the loadEvent function that is there in our ViewModel. It only requires one parameter that is our event id. We first check if the id is correct or not then load the events in a background thread and report the error if there is any.

fun loadEvent(id: Long) {
if (id.equals(-1)) {
throw IllegalStateException("ID should never be -1")
}
compositeDisposable.add(eventService.getEvent(id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
event.value = it
}, {
Timber.e(it, "Error fetching event %d", id)
error.value = "Error fetching event"
}))
}

 

The following function is used to load the event details with the date formatted in the appropriate manner. We first use the function getLocalizedDateTime to get the localized date in string and then format the date according to our needs.

private fun loadEventDetails(event: Event) {
rootView.eventName.text = event.name
rootView.organizerName.text = "by ${event.organizerName.nullToEmpty()}"
val dateString = StringBuilder()
val startsAt = EventUtils.getLocalizedDateTime(event.startsAt)
val endsAt = EventUtils.getLocalizedDateTime(event.endsAt)
rootView.time.text = dateString.append(EventUtils.getFormattedDate(startsAt))
.append(" - ")
.append(EventUtils.getFormattedDate(endsAt))
.append(" • ")
.append(EventUtils.getFormattedTime(startsAt))
}

 

That’s all that is needed to make the tickets screen. This how the fragment looks in the app.

Resources

  1. ReactiveX official documentation : http://reactivex.io/
  2. Vogella RxJava 2 – Tutorial : http://www.vogella.com/tutorials/RxJava/article.html
  3. Androidhive RxJava Tutorial : https://www.androidhive.info/RxJava/
Continue ReadingTickets fragment in the Open Event Android App

See events according to location in Open Event Android

In the Open Event Android App until now we were just showing a list of events from the server randomly. This does not makes sense because users are interested in events near them. So it was decided to show events according to a given location in the app. Let’s see how we achieved this.

API endpoint

The API endpoint which we are using looks something like this

https://open-event-api-dev.herokuapp.com/v1/events?sort=name&filter=[{“name”:”location-name”,”op”:”ilike”,”val”:”%India%”}]

This endpoint will return all the events that are happening in India. You can try to copy the above URL in your browser and see the output yourself.

Android Implementation

This function is used to return a list of events which are happening in the location specified by the user. It also saves these events in the local database of the app.

fun getEventsByLocation(locationName: String): Single<List<Event>> {
return eventApi.searchEvents("name", locationName).map {
eventDao.insertEvents(it)
it
}
}

 

The following piece of code gets called when the user clicks on the submit button in the soft keyboard. If the query is not empty then we store the value of the location in a variable and use it to load events according to location.

eventsViewModel.locationName = rootView.locationEdittext.text.toString()
eventsViewModel.loadLocationEvents()
return@OnEditorActionListener true

 

We are using a function named loadLocationEvents() above. Let’s have a look what this function is doing. We are using the location name that the user provides in a constant called query then we are sending a GET request to the server to get all events that are happening in that location name. All these networking request are happening in a background thread. As soon as we start the network operation we set the value of progress to true, this will show the progress bar to the user and as soon as the network request has been completed we remove the progress bar.

fun loadLocationEvents() {
val query = "[{\"name\":\"location-name\",\"op\":\"ilike\",\"val\":\"%$locationName%\"}]"

compositeDisposable.add(eventService.getEventsByLocation(query)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe({
progress.value = true
}).doFinally({
progress.value = false
}).subscribe({
events.value = it
}, {
Timber.e(it, "Error fetching events")
error.value = "Error fetching events"
}))
}

 

That’s it. We can now enter a name of the location and we will get a list of all the events that happening there

Resources

  1. ReactiveX official documentation : http://reactivex.io/
  2. Vogella RxJava 2 – Tutorial : http://www.vogella.com/tutorials/RxJava/article.html
  3. Androidhive RxJava Tutorial : https://www.androidhive.info/RxJava/
Continue ReadingSee events according to location in Open Event Android

Searching Events in Open Event Android

In the Open Event Android App searching events from the list of hundreds of events that are stored in the server is indeed an important task. As in the future there will be more events and the user might not be able to see a particular event that he might want to attend. So let’s see how the searching functionality was implemented in the app.

API endpoint?

The API endpoint which we are using looks something like this

https://open-event-api-dev.herokuapp.com/v1/events?sort=name&filter=[{“name”:”name”,”op”:”ilike”,”val”:”%Test Event%”}]

This endpoint returns all the events whose “name” field contains the words “Test Event” in ascending order and since the “ilike” operator is used the results are not case sensitive.

Android Implementation

The following lines are used to send a GET request to the server. We have two query parameters that is sort and filter. They are required so that we can get the list of events in ascending order and filter out the correct events. The function returns a list of events.

@GET("events")
fun searchEvents(@Query("sort") sort: String, @Query("filter") eventName: String): Single<List<Event>>

 

Now this is where the magic happens. This function returns the list of events that have the same name as the user has searched and stores it in the database. So all these events are stored offline as well.

fun getSearchEvents(eventName: String): Single<List<Event>> {
return eventApi.searchEvents("name", eventName)
.map {
eventDao.insertEvents(it)
it
}
}

 

We are using a SearchView to search the events. What happens here is that the user searches somethings and presses search, onQueryTextSubmit method gets called and it passes the search query to the ViewModel and loads the events.

searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
//Do your search
searchViewModel.searchEvent = query
searchViewModel.loadEvents()
loadEventsAgain = true
return false
}
}

 

To add the search icon in the toolbar we just need to add the following code in the XML file. We specify the id of the searchView so that we can reference it in our code. The value of showAsAction is “always” which means the searchView icon will always be visible in the toolbar.

<group android:id="@+id/search_menu">
<item
android:id="@+id/search_item"
android:icon="@drawable/ic_search_grey_24dp"
android:title="@string/search"
app:actionViewClass="android.support.v7.widget.SearchView"
app:showAsAction="always" />
</group>

 

That’s all that we require to search events in the app.

Resources

  1. SearchView Official Android Documentation https://developer.android.com/training/search/setup
  2. ViewModel Official Android Documentation https://developer.android.com/topic/libraries/architecture/viewmodel
  3. Retrofit Official Documentation  http://square.github.io/retrofit/
Continue ReadingSearching Events in Open Event Android

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 ReadingOffline support for Open Event Android with ROOM

Open Event Frontend – Settings Service

This blog illustrates how the settings of a particular user are obtained in the Open Event Frontend web app. To access the settings of the user a service has been created which fetches the settings from the endpoint provided by Open Event Server.

Let’s see why a special service was created for this.

Problem

In the first step of the event creation wizard, the user has the option to link Paypal or Stripe to accept payments. The option to accept payment through Paypal or Stripe was shown to the user without checking if it was enabled by the admin in his settings. To solve this problem, we needed to access the settings of the admin and check for the required conditions. But since queryRecord() returned a promise we had to re-render the page for the effect to show which resulted in this code:

canAcceptPayPal: computed('data.event.paymentCurrency', function() {     this.get('store').queryRecord('setting', {}) .then(setting => { this.set('canAcceptPayPal', (setting.paypalSandboxUsername || setting.paypalLiveUsername) && find(paymentCurrencies, ['code', this.get('data.event.paymentCurrency')]).paypal); this.rerender(); });

This code was setting a computed property inside it and then re-rendering which is bad programming and can result in weird bugs.

Solution

The above problem was solved by creating a service for settings. This made sense as settings would be required at other places as well. The file was called settings.js and was placed in the services folder. Let me walk you through its code.

  • Extend the default Service provided by Ember.js and initialize store, session, authManager and _lastPromise.
import Service, { inject as service } from '@ember/service';
import { observer } from '@ember/object';

export default Service.extend({

 store       : service(),
 session     : service(),
 authManager : service(),

_lastPromise: Promise.resolve(),
  • The main method which fetches results from the server is called _loadSettings(). It is an async method. It queries setting from the server and then iterates through every attribute of the setting model and stores the corresponding value from the fetched result.
/**
* Load the settings from the API and set the attributes as properties on the service
*
* @return {Promise<void>}
* @private
*/
async _loadSettings() {
 const settingsModel = await this.get('store').queryRecord('setting', {});
 this.get('store').modelFor('setting').eachAttribute(attributeName => {
   this.set(attributeName, settingsModel.get(attributeName));
 });
},
  • The initialization of the settings service is handled by initialize(). This method returns a promise.
/**
* Initialize the settings service
* @return {*|Promise<void>}
*/
initialize() {
 const promise = this._loadSettings();
 this.set('_lastPromise', promise);
 return promise;
}
  • _authenticationObserver observes for changes in authentication changes and reloads the settings as required.
/**
* Reload settings when the authentication state changes.
*/
_authenticationObserver: observer('session.isAuthenticated', function() {
 this.get('_lastPromise')
   .then(() => this.set('_lastPromise', this._loadSettings()))
   .catch(() => this.set('_lastPromise', this._loadSettings()));
}),

The service we created can be directly used in the app to fetch the settings for the user. To solve the Paypal and Stripe payment problem described above, we use it as follows:

canAcceptPayPal: computed('data.event.paymentCurrency', 'settings.paypalSandboxUsername', 'settings.paypalLiveUsername', function() {
 return (this.get('settings.paypalSandboxUsername') || this.get('settings.paypalLiveUsername')) && find(paymentCurrencies, ['code', this.get('data.event.paymentCurrency')]).paypal;
}),

canAcceptStripe: computed('data.event.paymentCurrency', 'settings.stripeClientId', function() {
 return this.get('settings.stripeClientId') && find(paymentCurrencies, ['code', this.get('data.event.paymentCurrency')]).stripe;
}),

Thus, there is no need to re-render the page and dangerously set the property inside its computed method.

References

Continue ReadingOpen Event Frontend – Settings Service

Open Event Server – Export Speakers as CSV File

FOSSASIA‘s Open Event Server is the REST API backend for the event management platform, Open Event. Here, the event organizers can create their events, add tickets for it and manage all aspects from the schedule to the speakers. Also, once he/she makes his event public, others can view it and buy tickets if interested.

The organizer can see all the speakers in a very detailed view in the event management dashboard. He can see the statuses of all the speakers. The possible statuses are pending, accepted and rejected. He/she can take actions such as editing/viewing speakers.

If the organizer wants to download the list of all the speakers as a CSV file, he or she can do it very easily by simply clicking on the Export As CSV button in the top right-hand corner.

Let us see how this is done on the server.

Server side – generating the Speakers CSV file

Here we will be using the csv package provided by python for writing the csv file.

import csv
  • We define a method export_speakers_csv which takes the speakers to be exported as a CSV file as the argument.
  • Next, we define the headers of the CSV file. It is the first row of the CSV file.
def export_speakers_csv(speakers):
   headers = ['Speaker Name', 'Speaker Email', 'Speaker Session(s)',
              'Speaker Mobile', 'Speaker Bio', 'Speaker Organisation', 'Speaker Position']
  • A list is defined called rows. This contains the rows of the CSV file. As mentioned earlier, headers is the first row.
rows = [headers]
  • We iterate over each speaker in speakers and form a row for that speaker by separating the values of each of the columns by a comma. Here, every row is one speaker.
  • As a speaker can contain multiple sessions we iterate over each session for that particular speaker and append each session to a string. ‘;’ is used as a delimiter. This string is then added to the row. We also include the state of the session – accepted, rejected, confirmed.
  • The newly formed row is added to the rows list.
for speaker in speakers:
   column = [speaker.name if speaker.name else '', speaker.email if speaker.email else '']
   if speaker.sessions:
       session_details = ''
       for session in speaker.sessions:
           if not session.deleted_at:
               session_details += session.title + ' (' + session.state + '); '
       column.append(session_details[:-2])
   else:
       column.append('')
   column.append(speaker.mobile if speaker.mobile else '')
   column.append(speaker.short_biography if speaker.short_biography else '')
   column.append(speaker.organisation if speaker.organisation else '')
   column.append(speaker.position if speaker.position else '')
   rows.append(column)
  • rows contains the contents of the CSV file and hence it is returned.
return rows
  • We iterate over each item of rows and write it to the CSV file using the methods provided by the csv package.
with open(file_path, "w") as temp_file:
   writer = csv.writer(temp_file)
   from app.api.helpers.csv_jobs_util import export_speakers_csv
   content = export_speakers_csv(speakers)
   for row in content:
       writer.writerow(row)

Obtaining the Speakers CSV file:

Firstly, we have an API endpoint which starts the task on the server.

GET - /v1/events/{event_identifier}/export/speakers/csv

Here, event_identifier is the unique ID of the event. This endpoint starts a celery task on the server to export the speakers of the event as a CSV file. It returns the URL of the task to get the status of the export task. A sample response is as follows:

{
  "task_url": "/v1/tasks/b7ca7088-876e-4c29-a0ee-b8029a64849a"
}

The user can go to the above-returned URL and check the status of his/her Celery task. If the task completed successfully he/she will get the download URL. The endpoint to check the status of the task is:

and the corresponding response from the server –

{
  "result": {
    "download_url": "/v1/events/1/exports/http://localhost/static/media/exports/1/zip/OGpMM0w2RH/event1.zip"
  },
  "state": "SUCCESS"
}

The file can be downloaded from the above-mentioned URL.

Resources

Continue ReadingOpen Event Server – Export Speakers as CSV File

Bottom Navigation Bar in Open Event Android

Bottom navigation bar provides an easy access and convenient way for users to navigate in between different sections of an App where the sections are of equal importance. We use bottom navigation move between different fragments such as events screen, favorites, tickets profile and so on. This blog post will guide you on how its implemented in Open Event Android.

To create a bottom navigation bar first create items for your menu. Menu items will be rendered as elements of the bottom navigation bar. Add icon and title to your navigation item here.

   <item
       android:id=“@+id/navigation_profile”
       android:icon=“@drawable/ic_baseline_account_circle_24px”
       android:title=“@string/profile” />

After we have written menu xml for our navigation view, we will have to create a view for it. The following snippet of code creates a BottomNavigationView type view element on the screen with different attributes as specified and menu file as specified by app:menu. Make sure you have android support design dependency set up before this.

<android.support.design.widget.BottomNavigationView
           android:id=“@+id/navigation”
           android:layout_width=“match_parent”
           android:layout_height=“wrap_content”
           android:layout_gravity=“bottom”
           android:background=“?android:attr/windowBackground”
           app:itemBackground=“@android:color/white”
           android:foreground=“?attr/selectableItemBackground”
           app:itemTextColor=“@color/colorPrimaryDark”
           app:menu=“@menu/navigation” />

No that our view will be created we need to wire menu items with different fragment and set up listener for item selected events.

Inside onCreate of MainActivity we attach a ItemSelectListener with the bottom navigation bar, navigation.setOnNavigationItemSelectedListener(listener) .

private val listener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
       val fragment: Fragment
       when (item.itemId) {
           R.id.navigation_profile -> {
               supportActionBar?.title = “Profile”
               fragment = ProfileFragment()
               loadFragment(fragment)
               return@OnNavigationItemSelectedListener true
           }
           R.id.navigation_favorite -> {
               supportActionBar?.title = “Likes”
               fragment = FavoriteFragment()
               loadFragment(fragment)
               return@OnNavigationItemSelectedListener true
           }
           R.id.navigation_tickets -> {
               supportActionBar?.title = “Tickets”
               fragment = OrdersUnderUserFragment()
               loadFragment(fragment)
               return@OnNavigationItemSelectedListener true
           }
       }
       false
   }

The above listener object listens for item selection in the bottom navigation bar. Whenever any item is selected lambda function is executed and item.itemId is matched with different menu item ids. On any successful match, an instance of the fragment corresponding to the selected item is loaded.

References

Continue ReadingBottom Navigation Bar in Open Event Android

Open Event Server – Export Sessions as CSV File

FOSSASIA‘s Open Event Server is the REST API backend for the event management platform, Open Event. Here, the event organizers can create their events, add tickets for it and manage all aspects from the schedule to the speakers. Also, once he/she makes his event public, others can view it and buy tickets if interested.

The organizer can see all the sessions in a very detailed view in the event management dashboard. He can see the statuses of all the sessions. The possible statuses are pending, accepted, confirmed and rejected. He/she can take actions such as accepting/rejecting the sessions.

If the organizer wants to download the list of all the sessions as a CSV file, he or she can do it very easily by simply clicking on the Export As CSV button in the top right-hand corner.

Let us see how this is done on the server.

Server side – generating the Sessions CSV file

Here we will be using the csv package provided by python for writing the csv file.

import csv
  • We define a method export_sessions_csv which takes the sessions to be exported as a CSV file as the argument.
  • Next, we define the headers of the CSV file. It is the first row of the CSV file.
def export_sessions_csv(sessions):
   headers = ['Session Title', 'Session Speakers',
              'Session Track', 'Session Abstract', 'Created At', 'Email Sent']
  • A list is defined called rows. This contains the rows of the CSV file. As mentioned earlier, headers is the first row.
rows = [headers]
  • We iterate over each session in sessions and form a row for that session by separating the values of each of the columns by a comma. Here, every row is one session.
  • As a session can contain multiple speakers we iterate over each speaker for that particular session and append each speaker to a string. ‘;’ is used as a delimiter. This string is then added to the row.
  • The newly formed row is added to the rows list.
for session in sessions:
   if not session.deleted_at:
       column = [session.title + ' (' + session.state + ')' if session.title else '']
       if session.speakers:
           in_session = ''
           for speaker in session.speakers:
               if speaker.name:
                   in_session += (speaker.name + '; ')
           column.append(in_session[:-2])
       else:
           column.append('')
       column.append(session.track.name if session.track and session.track.name else '')
       column.append(strip_tags(session.short_abstract) if session.short_abstract else '')
       column.append(session.created_at if session.created_at else '')
       column.append('Yes' if session.is_mail_sent else 'No')
       rows.append(column)
  • rows contains the contents of the CSV file and hence it is returned.
return rows
  • We iterate over each item of rows and write it to the CSV file using the methods provided by the csv package.
writer = csv.writer(temp_file)
from app.api.helpers.csv_jobs_util import export_sessions_csv
content = export_sessions_csv(sessions)
for row in content:
   writer.writerow(row)

Obtaining the Sessions CSV file:

Firstly, we have an API endpoint which starts the task on the server.

GET - /v1/events/{event_identifier}/export/sessions/csv

Here, event_identifier is the unique ID of the event. This endpoint starts a celery task on the server to export the sessions of the event as a CSV file. It returns the URL of the task to get the status of the export task. A sample response is as follows:

{
  "task_url": "/v1/tasks/b7ca7088-876e-4c29-a0ee-b8029a64849a"
}

The user can go to the above-returned URL and check the status of his/her Celery task. If the task completed successfully he/she will get the download URL. The endpoint to check the status of the task is:

and the corresponding response from the server –

{
  "result": {
    "download_url": "/v1/events/1/exports/http://localhost/static/media/exports/1/zip/OGpMM0w2RH/event1.zip"
  },
  "state": "SUCCESS"
}

The file can be downloaded from the above-mentioned URL.

Resources

Continue ReadingOpen Event Server – Export Sessions as CSV File

Changing Profile Image in Open Event Android

A user can create a custom Avatar in Open Event Android. We use Picasso Library for easy image loading and caching in Open Event Android. This blog post will help you understand how its implemented in Open Event Android.

These are the steps we follow :

  • Create UI element which allows the user to pick an avatar
  • Checking and requesting Read External Storage permission
  • Compress and Encode Image
  • Use Picasso to display image on UI
  • Upload it on the server on user’s command

We will discuss the above steps in detail in the following section.

Create UI element which allows the user to pick an avatar

To display the selected avatar image as well as to fire image pick action when clicked we need a UI element. We display a placeholder in case no avatar image is provide, in this case, its ic_account_circle_grey_24dp which lies in drawable folder. Rest of the code is self explanatory.

<ImageView
       android:id=“@+id/profilePhoto”
       android:layout_width=“@dimen/item_image_view_large”
       android:layout_height=“@dimen/item_image_view_large”
       android:layout_gravity=“center_horizontal”
       android:contentDescription=“Profile Photo”
       android:padding=“@dimen/padding_large”
       app:srcCompat=“@drawable/ic_account_circle_grey_24dp” />

Once we have created the UI we need to wire it with the action to pick an image, the following code snippet does that. When this ImageView is clicked permissionGranted variable is checked which keeps a track of read external storage permission if this is true file chooser is displayed to allows user to select an image else an dialog for requesting permissions is shown.

rootView.profilePhoto.setOnClickListener { v ->
           if (permissionGranted) {
               showFileChooser()
           } else {
               requestPermissions(READ_STORAGE, REQUEST_CODE)
           }
       }

Checking and requesting Read External Storage permission

As discussed in the above section permission check is performed when ImageView is clicked and if not granted a dialog request for read external storage is displayed. Initially, the boolean permissionGranted is set to false which means clicking on ImageView fires requestPermission method with permissions array and request code and by overriding the onRequestPermissionResult method we can look at request status.

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
       if (requestCode == REQUEST_CODE) {
           if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
               permissionGranted = true
               Toast.makeText(context, “Permission to Access External Storage Granted !”, Toast.LENGTH_SHORT).show()
               showFileChooser()
           } else {
               Toast.makeText(context, “Permission to Access External Storage Denied :(“, Toast.LENGTH_SHORT).show()
           }
       }
   }

onRequestPermissionsResult is called everytime requestPermission finishes, to check for which request it’s called we can check the Request code if it matches with the one we passed in our requestPermissions method that means the grant result carries information about our request. We can check the grantResult and if permission is granted we set our permissionGranted variable to true and display the file chooser menu.

Compress and Encode Image

Once the user picks the image from the file chooser we need to convert it into an input stream and we encode it into our JPEG format and specify the image quality. The following code snippet will help you understand how this happens.

val imageUri = intentData.data
           var imageStream: InputStream? = null
           try {
               imageStream = activity?.contentResolver?.openInputStream(imageUri)
           } catch (e: FileNotFoundException) {
               Timber.d(e, “File Not Found Exception”)
           }
w
           val selectedImage = BitmapFactory.decodeStream(imageStream)
           encodedImage = encodeImage(selectedImage)

           Picasso.get()
                   .load(imageUri)
                   .placeholder(R.drawable.ic_person_black_24dp)
                   .transform(CircleTransform())
                   .into(rootView.profilePhoto)
       }

The above code is executed after picking the image from file chooser. The image is first converted into input stream (this process is put inside a try catch because the conversion process may result in file not found exception) and then BItmapFactory’s decodeStream is called on it, all this is done so that we can convert URI into to bitmap now this bitmap is converted into JPEG format with 100 percent quality and then into a Base64 type string using the function encodeImage.

Finally, we use Picasso standard functions to load the image into UI we also specify placeholder image which is displayed if anything fails or image is loading.

Upload Image on the server on user’s command

If the user wish to keep the selected image as its avatar he can click on update button in open event android. This button fires a post request which uploads the image data to open event server followed by a patch request to add the URL to User information.

@POST(“upload/image”)
   fun uploadImage(@Body uploadImage: UploadImage): Single<ImageResponse>

This is the post request made to the server the body contains an UploadImage object which essentially caries a String type data variable containing the Base64 encoded jpeg bitmap. In response to the request, the server returns a URL which is then added to the User object and a patch request is made to update the user on the server

@PATCH(“users/{id}”)
   fun updateUser(@Body user: User, @Path(“id”) id: Long): Single<User>

References

Continue ReadingChanging Profile Image in Open Event Android

Hiding Multiple Attendee Recycler for Single Tickets in Open Event Android

Multiple attendee recycler allows the user to easily write in attendee details for all tickets he/she is buying. The UI for attendee details fragment can be divided into two segments, one contains the details of the person creating the whole order and followed by multiple details section for the attendee of every ticket. This is a good way to take details when multiple ticket or tickets with greater than 1 quantity is involved. However, if there is only one attendee detail to be taken instead of asking the user for order creator’s details and then first attendee details it is better to add some views in the creator segment and not ask for the same details again from the user. This blog post will help you in understanding how it’s done in Open Event Android.

Keeping a track of orders with single attendee

We keep a boolean to track whether or not the sum of quantities of tickets selected by a user is one since one attendee is to be generated per ticket quantity. And incase it is boolean variable singleTicket is set to True which is false by default initially. Following is how it can be done

singleTicket = ticketIdAndQty?.map { it.second }?.sum() == 1

The above statements basically iterate over the list of pairs of tickets and their quantity and then sums over the second element of all pairs that is the quantities, if the summation leads to 1 single ticket is set to True else false.

Adding elements to recycler adapter

The next step is to update the logic as to when blank attendees are added to recycler adapter. The idea is to check the boolean singleTicket and if it is false dont insert any attendee.

attendeeFragmentViewModel.tickets.observe(this, Observer {
               it?.let {

                   if (!singleTicket)
                       it.forEach {
                           val pos = ticketIdAndQty?.map { it.first }?.indexOf(it.id)
                           val iterations = pos?.let { it1 -> ticketIdAndQty?.get(it1)?.second } ?: 0
                           for (i in 0 until iterations)
                               attendeeRecyclerAdapter.add(Attendee(attendeeFragmentViewModel.getId()), it)
                           attendeeRecyclerAdapter.notifyDataSetChanged()
                       }
               }
           })

If a single ticket is false regular procedure to add blank attendee is carried out. The quantity of ticket is found and the same quantity of attendees are added to recycler adapter’s attendee’s list similary ticket information is also added to the recycler adapter. The id for blank attendees is taken from the attendeeViewModel’s getId function which in turn simply returns id using the authHolder’s getId function.

Modify the register method to check for the single attendee

So far what we have discussed helps us display the right no of details section on the UI. We will also have to modify the register method to take into account the above changes. Following is the implementation of what happens when the user selects register.

rootView.register.setOnClickListener {
               if (selectedPaymentOption == “Stripe”)
                   sendToken()

               val attendees = ArrayList<Attendee>()
               if (singleTicket) {
                   val pos = ticketIdAndQty?.map { it.second }?.indexOf(1)
                   val ticket = pos?.let { it1 -> ticketIdAndQty?.get(it1)?.first?.toLong() } ?: –1
                   val attendee = Attendee(id = attendeeFragmentViewModel.getId(),
                           firstname = firstName.text.toString(),
                           lastname = lastName.text.toString(),
                           city = getAttendeeField(“city”),
                           address = getAttendeeField(“address”),
                           state = getAttendeeField(“state”),
                           email = email.text.toString(),
                           ticket = TicketId(ticket),
                           event = eventId)
                   attendees.add(attendee)
               } else {
                   attendees.addAll(attendeeRecyclerAdapter.attendeeList)
               }
               val country = if (country.text.isEmpty()) country.text.toString() else null
               attendeeFragmentViewModel.createAttendees(attendees, country, selectedPaymentOption)
           }

For create attendee object and then add it to the attendees ArrayList first we need to find the ticket it is to be created for as the attendee object accepts Ticket Id as one of the parameters. The logic to get the ticket is fairly straightforward if singleTicket is true first we find out the ticket for which quantity is 1 and then we get the ticket object for the same. Finally, we take the id field of the ticket and add it to our attendee constructor. Other fields for attendee such as first name, last name, email, and country are taken from the EditText View’s rendered on the screen. Else if the single ticket is false which means there are multiple attendee details section on the fragment we simply add the recycler adapter’s attendee list into attendees ArrayList which is later provided to the order function for creating an order of it.

References

Continue ReadingHiding Multiple Attendee Recycler for Single Tickets in Open Event Android