Add feature to view slides and videos of sessions in Open Event Webapp

The Open Event Web App has two components :

  • An event website generator
  • The actual generated website output.

The web generator application can generate event websites by getting data from event JSON files and binary media files, that are stored in a compressed zip file or through an API endpoint. The JSON data format of version 1 as well as version 2, provides user an option to add the slide and video URLs of the sessions. The data from JSONs is extracted and stored in the objects for a particular session, and in the template, the data for videos and slides are rendered in their corresponding iframes.

Extracting data from event JSONs

The data is extracted from the JSONs and is stored in an object. The object containing the data is sent to the procedure which compiles the handlebars templates with that data.

JSON data format v1

video: session.video,
slides: session.slides,
audio: session.audio

 

JSON data format v2

video: session['video-url'],
slides: session['slides-url'],
Audio: session['audio-url']

 

The JSON data format for v1 and v2 are different and thus the data is extracted from the file depending on API version chosen for web app generation. The files where data extraction takes place are fold_v1.js and fold_v2.js for API v1 and v2 respectively.

Adding event emitter

Onclick event emitter on schedule division calls the procedure “loadVideoAndSlides” with the parameters corresponding to the session clicked.

<div class="schedule-track" id="{{session_id}}" onclick = "loadVideoAndSlides('{{session_id}}', '{{video}}', '{{slides}}')">
   .....
   .....
</div>

The parameters Session ID, Video URL and Slide URL are passed to the procedure which is responsible for displaying the slides and video iframes for the sessions. This resolves the problem of heavy data binding to the page, as the frames for videos and slides are loaded on page only when the session is clicked.

Procedure called on click event

The performance of web app is significantly improved by using the call and listen mechanism as only the requested videos are loaded into the document object model.

function loadVideoAndSlides(div, videoURL, slideURL){
 if(videoURL !== null && videoURL !== '') {
     $('#desc-' + div).children('div').prepend(' + div + '" class = "video-iframe col-xs-12 col-sm-12 col-md-12" src="https://www.youtube.com/embed/' + videoURL + '" frameborder="0" allowfullscreen>');
 }
 if(slideURL !== null && slideURL !== '') {
     $('#desc-' + div).children('div').prepend(' + div + '" class = "iframe col-xs-12 col-sm-12 col-md-12" frameborder="0" src="https://view.officeapps.live.com/op/embed.aspx?src=' + slideURL  +'">');
 }
}

 

The video and slide URLs passed to the procedure are used for loading the iframes from youtube and office apps or google docs respectively as shown above, and the resulting slide view is as shown below

Resources

Continue ReadingAdd feature to view slides and videos of sessions in Open Event Webapp

Adding multiple themes in Open Event Web app

Open Event Web app generator provides an option to event organizers to choose the theme for their event website. Previously it supported the default light theme for the websites, but now it supports a customized dark-theme as well inspired from the twitter dark-mode. Adding this new feature to the User Interface of the generated website, Open Event Web app provides a stylish view for the website of an event.

How we enabled multiple theme support?

The client is provided an option to choose the theme, either ‘light’ or ‘dark’ from the generator. Depending on the option selected, the data is sent to the server, the server extracts the data and while compiling the stylesheet for the pages takes into the account which theme is chosen by the client.

Adding option to choose theme

A drop-down menu is added on the generator page which lets the client choose either of the light or dark themes. The default theme is light.

<label for="theme">Choose your theme</label>
<select class="form-control" id="theme" name="theme">
 <option value="light">Light</option>
 <option value="dark">Dark</option>
</select>

Defining the color schema

The color schema is set different for both the themes, this is achieved by making two different stylesheets for the light and dark themes. The style or color of an element is assigned a variable in main css file(application.scss). Those variables are overwritten while compiling these specific files depending upon the theme chosen.

_light.scss
Defines the values of variables for light theme.

$navbar-color: #ffffff;
$link-color: $gray-dark !default;
$hover-color: $black !default;
$span-background: #efefef !default;

 

_dark.scss
Defines the values of variables for dark theme.

$navbar-color: #2b324a;
$link-color: $light-blue;
$hover-color: $light-blue;
$span-background: #333d5a;

Setting up theme field in json data

The json data to be used for compiling the templates, sets or unsets the theme field according to the theme selected by the client.

if (req.body.theme === 'light') {
 jsonData.theme = 0;
} else {
 jsonData.theme = 1;
}

 

Rendering style in template files

Now we have the value for theme selected by the client, the data used for rendering the CSS file is chosen accordingly. If the theme is light, the file containing color-scheme for light proposition is selected else the dark one. The output file contains the values for color variables according to the theme.

sass.render({
 file: __dirname + '/_scss/_themes/_' + theme + '-theme/_' + theme + '.scss',
 outFile: distHelper.distPath + '/' + appFolder + '/css/schedule.css'
}, function(err, result) {
 if (!err) {
      ...
      ... // Minifying CSS file and writing 
   callback(null);
});

The output file is schedule.css which contains the style for different pages according to the theme chosen while web app generation.

Resources

Continue ReadingAdding multiple themes in Open Event Web app

Automating Play Store releases in Open Event Android with Fastlane

In Open Event Android we are using fastlane to automate the process of releasing the app to the Play Store, otherwise without this tool we would have to sign the release apk everytime before making a release but with fastlane all we need to do is just merge the development branch with the master branch and the updated app is released in Play Store.

The first thing that we need to do is encrypt the signing keys and upload it to the repository so we will create an encrypted tar file of the signing keys and upload it to the repository.

Now we want to decrypt the tar file everytime we merge our development branch into the master branch. We will create a bash script which does the above task which looks like this.

#!/bin/sh
set -e

export DEPLOY_BRANCH=${DEPLOY_BRANCH:-master}

if [ "$TRAVIS_PULL_REQUEST" != "false" -o "$TRAVIS_REPO_SLUG" != "fossasia/open-event-android" -o "$TRAVIS_BRANCH" != "$DEPLOY_BRANCH" ]; then
 echo "We decrypt key only for pushes to the master branch and not PRs. So, skip."
exit 0
fi

# Decrypt keys
openssl aes-256-cbc -K $encrypted_59a1db41ee4d_key -iv $encrypted_59a1db41ee4d_iv -in ./scripts/secrets.tar.enc -out ./scripts/secrets.tar -d
tar xvf ./scripts/secrets.tar -C scripts/

 

We need to define all these variables written after the $ sign as our Travis environment variables.

Next thing that we need to do is sign the apk so first thing that we check in the update-apk script is if we are on the master branch. We take the unsigned apk and sign it with the following commands.

 cp open-event-master-app-playStore-release-unsigned.apk open-event-master-app-playStore-release-unaligned.apk
jarsigner -verbose -tsa http://timestamp.comodoca.com/rfc3161 -sigalg SHA1withRSA -digestalg SHA1 -keystore ../scripts/key.jks -storepass $STORE_PASS -keypass $KEY_PASS open-event-master-app-playStore-release-unaligned.apk $ALIAS

 

The following command zipaligns the app

 ${ANDROID_HOME}/build-tools/27.0.3/zipalign -v -p 4 open-event-master-app-playStore-release-unaligned.apk open-event-master-app-playStore-release.apk

 

After signing the release apk, the final step is to publish the app to the playstore. The following command will install fastlane.

gem install fastlane

 

This command will take the signed apk and upload it in alpha channel of the playstore.

fastlane supply --apk open-event-master-app-playStore-release.apk --track alpha --json_key ../scripts/fastlane.json --package_name $PACKAGE_NAME

 

That’s it! Now releasing an update to the playstore is as simple as sending a pull request to the master branch. A process that would have otherwise required few minutes has been reduced to just seconds.

Resources

  1. Fastlane Official Site: https://fastlane.tools/
  2. Fastlane Android Documentaion: https://docs.fastlane.tools/getting-started/android/release-deployment/
  3. Fastlane Repository: https://github.com/fastlane/fastlane
Continue ReadingAutomating Play Store releases in Open Event Android with Fastlane

Building Open Event Android for F-Droid

According to the official website F-Droid is an installable catalogue of FOSS (Free and Open Source Software) applications for the Android platform. Only apps that are open source and use only open source libraries are accepted in F-Droid. Let’s see what steps were taken to publish Open Event Android in F-Droid.

We need to build a different app for F-Droid because it cannot use any proprietary  libraries so instead of making a new app we made two flavors of the same app. One that would be published in playstore and the other for F-Droid. It is really easy to make different flavors of the same app. Just add the following lines in the build.gradle

flavorDimensions "default"

productFlavors {
fdroid {
dimension "default"
}

playStore {
dimension "default"
}
}

 

We specify the flavor dimension in the first line because all product flavors must have a flavor dimension and then we specify the name of our product flavors. That’s all that is required to make various flavors of an app after that we just need to remove the proprietary libraries from the F-Droid build.

When we have finished building the app we can start with the process of submitting the app to F-Droid. We need to send a Merge Request to the F-droid repository. Let’s see what all steps are required before we could submit our Merge Request to include our app in F-Droid

We need to clone the fdroid server directly from the source so that we have all the latest changes and set the path.

git clone https://gitlab.com/fdroid/fdroidserver.git
export PATH="$PATH:$PWD/fdroidserver"

 

Then clone your fork of the fdroiddata repository and open the directory. You need to write your own username here.

git clone https://gitlab.com/nikit19/fdroiddata.git
cd fdroiddata

 

The following command will make a template for you in the metadata directory, the name of the file is our application id. If you open the metadata folder you will see text files of all the apps that are published in F-Droid. If you want to publish an app in F-droid it is this file that needs to be sent in merge request. There are no other changes that needs to be done, we just have to fill the fields in the meta file correctly.

cp templates/app-minimal metadata/com.eventyay.attendee.txt

 

After filling all the details in the meta file we need to commit those changes. The following command will check if there are any syntax errors in the meta file.

fdroid readmeta

 

Finally we need to build the app. The following command builds the app from the source repository so if we have followed all the steps correctly, the app would build successfully and then we can send the merge request.

fdroid build -v -l com.eventyay.attendee

 

Resources

  1. Quick Start: https://gitlab.com/fdroid/fdroiddata/blob/master/README.md#quickstart
  2. Making merge requests: https://gitlab.com/fdroid/fdroiddata/blob/master/CONTRIBUTING.md#merge-requests

 

Continue ReadingBuilding Open Event Android for F-Droid

Implement Charges Endpoint in the Open Event Android App

In the Open Event Android App we first create an attendee and then the order when a user tries to buy a ticket but the end user will only know that the transaction is successful when money gets deducted from his account. This is where we use the charges endpoint of the Open Event Server to complete the payment. Let’s see how this is being done in the Open Event Android App.

We are sending a POST request from the app to the following endpoint. The order identifier is added in the url to uniquely identify the order for which we are charging the user. The body of the POST request contains the Charge object with the required information.

@POST("orders/{orderIdentifier}/charge")
fun chargeOrder(@Path("orderIdentifier") orderIdentifier: String, @Body charge: Charge): Single<Charge>

 

This is how the model class Charge looks like. We specify the type at the top. Then we can either send a stripe or paypal token to the server which contains all the details of the payment. Message and status fields are returned from the server after the getting a success response. The status is true if the payment has been successfully made otherwise it is false. The message field gives us the feedback about what kind of error we got if the payment was not successful otherwise we get a success response.

@Type("charge")
data class Charge(
@Id(IntegerIdHandler::class)
val id: Int,
val stripe: String? = null,
val paypal: String? = null,
val message: String? = null,
val status: Boolean? = null
)

 

This is how we send the stripe token to the server. If the card details are correct then we receive the stripe token in the onSuccess method and send it to the server using the Charges endpoint.

override fun onSuccess(token: Token) {
//Send this token to server
val charge = Charge(attendeeFragmentViewModel.getId().toInt(), token.id, null)
attendeeFragmentViewModel.completeOrder(charge)

}

 

This is the function that is being used to send the POST request. It takes a charge object as an argument and then we use that in the chargeOrder method. The orderIdentifier variable that we see in the chargeOrder function is a lateinit variable that gets initialized when an order has been returned from the server. When this request is being made in the background thread we show user the progress bar. When we receive a success response from the server, we update the value of the message variable with the value returned from the server and show it to the user. If the value of the status is true that means that the payment has been successfully completed.

fun completeOrder(charge: Charge) {
compositeDisposable.add(orderService.chargeOrder(orderIdentifier.toString(), charge)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe {
progress.value = true
}.doFinally {
progress.value = false
}.subscribe({
message.value = it.message
paymentCompleted.value = it.status

if (it.status != null && it.status) {
Timber.d("Successfully charged for the order!")
} else {
Timber.d("Failed charging the user")
}
}, {
message.value = "Payment not completed!"
Timber.d(it, "Failed charging the user")
}))
}

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 ReadingImplement Charges Endpoint in the Open Event Android App

Ticket Details in the Open Event Android App

It is important to show the user the ticket details before he makes the payment so that he is aware how much does each ticket costs. Let’s see how this is being done in the Open Event Android App.

This function will query the database with a list of ids and return the tickets with the matching ids.

@Query("SELECT * from Ticket WHERE id in (:ids)")
fun getTicketsWithIds(ids: List<Int>): Single<List<Ticket>>

 

To use the above function we first need the ids of the tickets that needs to be shown so we loop through the ticketIdAndQty variable and everytime the quantity is greater than 0 we add the id of the ticket to the array list.

val ticketIds = ArrayList<Int>()
ticketIdAndQty?.forEach {
if (it.second > 0) {
ticketIds.add(it.first)
}
}

 

The next step is to call getTicketsWithIds function in a background thread because it is a database operation and we don’t make calls to the database in the main thread. If the call to the database is successful we store the list of tickets in a variable otherwise an error message is shown to the user.

compositeDisposable.add(ticketService.getTicketsWithIds(ticketIds)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
tickets.value = it
}, {
Timber.e(it, "Error Loading tickets!")
}))

 

To display the values to the user we just have to use the ticket variable and use its various fields but subtotal is not present in the variable so we have to calculate it. We just multiply the price of the ticket  with its quantity. So we pass the adapter position in the qty array to get the quantity of the current ticket and multiply it with the price.

val subTotal: Float? = ticket.price?.toFloat()?.times(qty[adapterPosition])

 

Ticket details are only shown if the user clicks on the textview which says view so we adjust the visibility of the ticket details in this simple if else statements and also update the textview value accordingly.

if (rootView.view.text == "(view)") {
rootView.ticketDetails.visibility = View.VISIBLE
rootView.view.text = "(hide)"
} else {
rootView.ticketDetails.visibility = View.GONE
rootView.view.text = "(view)"
}

 

Resources

  1. Room official documentation :https://developer.android.com/topic/libraries/architecture/room
  2. Vogella RxJava 2 – Tutorial : http://www.vogella.com/tutorials/RxJava/article.html
  3. Androidhive RxJava Tutorial : https://www.androidhive.info/RxJava/
Continue ReadingTicket Details in the Open Event Android App

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

Adding event to overview site of Open Event Webapp

Open Event Web app has two components: The generator used for generating event website and the generic web application for any event. The overview site showcases the example events generated using web app generator. The event added to the overview site keeps on updating with every new feature added. The samples on overview site updates whenever Travis build is triggered. This blog will illustrate how to add an event to overview site.

Adding event name and links to overview site

The event to be shown on overview site is added to the ‘index’ page of overview site with the link to event and a link to image as shown below.

<div class="col-md-4 col-xs-12 col-sm-6 event-holder">
   <div class="thumbnail">
       <a href='./SparkSummit2017/index.html' target='_blank'>
           <img src='./SparkSummit17.jpg' onerror="imgError(this, true);">
           <div class="caption">
               <h4 class="name">Spark Summit 2017</h4>
           </div>
       </a>
   </div>
</div>

Adding event image

The event image is chosen and added to the same directory containing the index page for overview site. Following is the image selected for Spark Summit 2017 sample.

Copying the event image to event directory

The samples are kept updated by generating the samples with the modified changes using travis-ci. Therefore, the static files for samples are copied to event folder which is pushed to github later for build. Following is the helper function used for copying the files. The counter keeps track for number of files copied.

it('should copy all the static files', function (done) {
 let staticPath = __dirname + '/../src/backend/overviewSite/';
 let totalFiles = 17;
 let counter = 0;

 function copyStatic(fileName) {
   fs.readFile(staticPath + fileName, function (err, data) {
     if (err) {
       done(err);
       return;
     }
     fs.writeFile(dist.distPath + '/a@a.com/' + fileName, data, function (err) {
       if (err) {
         done(err);
         return;
       }
       counter++;
       if (counter === totalFiles) {
         done();
       }
     });
   });
 }

Adding tests for the event and regenerating on every build

The sample is rebuilt on every modified change in the project, so every feature added is reflected in the samples present on the overview site. The generator procedure ‘createDistDir’ is called with the required data being passed to it. Following is an example for generating Spark Summit 2017 sample and testing the sample generation in event directory.

it('should generate the Spark Summit 2017 event', function (done) {
 let data = {};

 data.body = {
   "email": 'a@a.com',
   "theme": 'light',
   "sessionMode": 'expand',
   "apiVersion": 'api_v2',
   "datasource": 'eventapi',
   "apiendpoint": 'https://raw.githubusercontent.com/fossasia/open-event/master/sample/SparkSummit17',
   "assetmode": 'download'
 };


 generator.createDistDir(data, 'Socket', function (appFolder) {
   assert.equal(appFolder, "a@a.com/SparkSummit2017");
   done();
 });

});

Resources

Continue ReadingAdding event to overview site of Open Event Webapp

Adding Logs for Request Status in Open Event Web app

Open Event Web app handles multiple requests from the client using task queue, every request from client is put in the job queue and handled one at a time. The only log shown to client was either ‘waiting’ or ‘processing’, so we need to show additional logs like request waiting number as well. The logs are shown in real time using sockets.

How to add logs?

The logs of any request are shown to the client in real time using socket emit and listener events. Whenever any data is to be displayed inside the logs, the server emits an event with the data. The socket listens to the event and appends the data received to the logs section of the view.

Creating helper for emitting data

The helper function named ‘logger’ is created which emits the event ‘buildLog’ whenever it is called with the data being passed as arguments. Every time a message is passed to this procedure, it adds it to array of objects containing the data.

'use strict';

// eslint-disable-next-line no-var
var exports = module.exports = {};
const buildLog = [];
let obj = {};
let emit, largeMsg, message;

exports.addLog = function(type, smallMessage, socket, largeMessage) {
 if (typeof largeMessage === 'undefined') {
   largeMsg = smallMessage;
 }

 buildLog.push({'type': type, 'smallMessage': smallMessage, 'largeMessage': largeMsg});
 message = largeMsg.toString();
 obj = {'type': type, 'smallMessage': smallMessage, 'largeMessage': message};
 emit = false;

 if (socket.constructor.name === 'Socket') {
   emit = true;
 }
 if (emit) {
   socket.emit('buildLog', obj);
 }
};

Updating logs in real time

The helper created above emits the event ‘buildLog’, the socket on listening this event appends the data inside logs division with the data received from the helper while emitting the event.

socket.on('buildLog', function(data) {
   const spanElem = $('<span></span>'); // will contain the info about type of statement
   const spanMess = $('<span></span>'); // will contain the actual message
   const aElem = $('<button></button>'); // Button to view the detailed error log
   const divElem = $('

); // Contain the detailed error log
   const paragraph = $('<p></p>'); // Contain the whole statement

  //Code for styling the logs division
  ....
  ....


     divElem.text(data.largeMessage);
     paragraph.append(aElem);
     paragraph.append(divElem);
     updateStatusAnimate(data.smallMessage, 200, 'red');
     $('#btnGenerate').prop('disabled', false);
     $('input[ type = "radio" ]').attr('disabled', false);
     $('#email').prop('disabled', false);
   }
   $('#buildLog').append(paragraph);
   $('#buildLog').scrollTop($('#buildLog')[0].scrollHeight);
 });
});

Add request waiting number

Whenever a new request is received from the client the server emits the event ‘waiting’ if any other job is currently being processed. The helper above is used to add request waiting number to the logs.

const jobs = await queue.getJobs('waiting', {start: 0, end: 25});
const activeJob = await queue.getJobs('active', {start: 0, end: 25});
const jobIds = jobs.map((currJob) => currJob.id);

if (jobIds.indexOf(currJobId) !== -1) {
 socket.emit('waiting');
 logger.addLog('Info', 'Request waiting number: ' + (currJobId - activeJob[0].id), socket);
}

Add status in logs

On listening the event named ‘waiting’ the status is updated to ‘waiting’ in the view and is shown to the client.

socket.on('waiting', function() {
 updateStatusAnimate('Request status: Waiting');
});

Update request waiting number

Whenever a job is started being processed from the queue, the waiting number of all the requests in the ready queue is updated. The socket connection for corresponding request is obtained from the main socket object(socketObj) which updates whenever a new request comes from the client.

const jobs = new Promise(function(resolve) {
 resolve(queue.getJobs('waiting', {start: 0, end: 25}));
});

generator.createDistDir(job.data, socketObj[processId], done);
jobs.then(function(waitingJobs) {
 waitingJobs.forEach(function(waitingJob) {
   logger.addLog('Info', 'Request waiting number: ' + (waitingJob.id - job.id), socketObj[waitingJob.id]);
 });
});

 

Resources

Continue ReadingAdding Logs for Request Status in Open Event Web app

Play Youtube Videos in SUSI.AI Android app

SUSI.AI Android app has many response functionalities ranging from giving simple ANSWER type responses to complex TABLE and MAP type responses. Although, even after all these useful response types there were some missing action types all related to media. SUSI.AI app was not capable of playing any kind of music or video.So, to do this  the largest source of videos in the world was thought and the functionality to play youtube videos in the app was added.

Since, the app now has two build flavors corresponding to the FDroid version and PlayStore version respectively it had to be considered that while adding the feature to play youtube videos any proprietary software was not included with the FDroid version. Google provides a youtube API that can be used to play videos inside the app only instead of passing an intent and playing the videos in the youtube app.

Steps to integrate youtube api in SUSI.AI

The first step was to create an environment variable that stores the API key, so that each developer that wants to test this functionality can just create an API key from the google API console and put it in the build.gradle file in the line below

def YOUTUBE_API_KEY = System.getenv(‘YOUTUBE_API_KEY’) ?: ‘”YOUR_API_KEY”‘    

In the build.gradle file the buildConfigfield parameter names API_KEY was created so that it can used whenever we need the API_KEY in the code. The buildConfigfield was declared for both the release and debug build types as :

release {
  minifyEnabled false
 proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’
 buildConfigField “String”, ‘YOUTUBE_API_KEY’, YOUTUBE_API_KEY
}

debug {
  buildConfigField “String”, ‘YOUTUBE_API_KEY’, YOUTUBE_API_KEY
}

The second step is to catch the audio and video action type in the ParseSusiResponseHelper.kt file which was done by adding two constants “video_play” and “audio_play” in the Constant class. These actions were easily caught in the app as :

Constant.VIDEOPLAY -> try {
  identifier = susiResponse.answers[0].actions[i].identifier
} catch (e: Exception) {
  Timber.e(e)
}

Constant.AUDIOPLAY -> try {
  identifier = susiResponse.answers[0].actions[i].identifier
} catch (e: Exception) {
  Timber.e(e)
}

The third step involves making a ViewHolder class for the audio and video play actions. A simple layout was made for this viewholder which can display the thumbnail of the video and has a play button on the thumbnail, on clicking which plays the youtube video. Also, in the ChatFeedRecyclerAdapter we need to specify the action as Audio play and video play and then load the specific viewholder for the youtube videos whenever the response from the server for this particular action is fetched. The YoutubeViewHolder.java file describes the class that displays the thumbnail of the youtube video whenever a response arrives.

public void setPlayerView(ChatMessage model) {
  this.model = model;

  if (model != null) {
      try {
          videoId = model.getIdentifier();
          String img_url = “http://img.youtube.com/vi/” + videoId + “/0.jpg”;

          Picasso.with(itemView.getContext()).load(img_url).
                  placeholder(R.drawable.ic_play_circle_filled_white_24dp)
                  .into(playerView);
      } catch (Exception e) {
          Timber.e(e);
      }

  }

The above method shows how the thumbnail is set for a particular youtube video.

The fourth step is to pass the response from the server in the ChatPresenter to play the video asked by the user. This is  achieved by calling a function declared in the IChatView so that the youtube video will be played after fetching the response from the server :

if (psh.actionType == Constant.VIDEOPLAY || psh.actionType == Constant.AUDIOPLAY) {
  // Play youtube video
  identifier = psh.identifier
  chatView?.playVideo(identifier)
}

The fifth step is a bit complicated, as we know the app contains two flavors, one for the FDroid version that contains all the Open Source libraries and software and the second is the PlayStore version which may keep the proprietary software. Since, the youtube api is nothing but a piece of proprietary software we cannot include its implementation in the FDroid version and so different methods were devised to play the youtube videos in both versions. Since,  we use flavors and only want that Youtube API is compiled and included in the playstore version the youtube dependency was updated as :

playStoreImplementation files(‘libs/YouTubeAndroidPlayerApi.jar’)

This ensures that the library is only included in the playStore version. But on this step another problem is encountered, as now the code in both the versions will be different an encapsulation method was devised which enabled the app to keep separate code in both flavors.

To keep different code for both variants, in the src folder directories named fdroid and playStore were created and package name was added and then finally a java folder was created in each directory in which the separate code was kept for each flavor. An interface in the main directory was created which was used as a means to provide encapsulation so that it was implemented differently in each of the flavors to provide different functionality. The interface was created as :

interface IYoutubeVid {
  fun playYoutubeVid(videoId: String)
}

An object of the interface was made initialised in the ChatActivity and separate classes

named YoutubeVid.kt was made in each flavor which implemented the interface IYoutubeVid. So, now what happened was that depending on the build variant the YoutubeVid class of the particular flavor was called and the video would play according the sources that are available in that flavor.

In ChatActivity the following implementation was followed :

Declaration :

lateinit var youtubeVid: IYoutubeVid

Initialisation :

youtubeVid = YoutubeVid(this)

Call to the overridden function :

override fun playVideo(videoId: String) {
  Timber.d(videoId)
  youtubeVid.playYoutubeVid(videoId)
}

Final Output

References

  1. Youtube Android player API : https://developers.google.com/youtube/android/player/
  2. Building apps with product flavors – Tomoaki Imai https://medium.com/@tomoima525/building-multiple-regions-apps-with-product-flavors-good-and-bad-fe33e3c31060
  3. Kotlin style guide : https://android.github.io/kotlin-guides/style.html

 

Continue ReadingPlay Youtube Videos in SUSI.AI Android app