Creating an Update Daemon for SUSI Smart Speaker

A daemon in reference of operating systems is a computer program that runs as a background process rather than under direct control of the user. Various daemons are being used in SUSI smart speaker.

The following daemons have been created

  • Update Daemon
  • Media Discovery Daemon
  • Factory Reset Daemon 

In this blog, we’ll be discussing the implementation of the Update Daemon in SUSI.AI

Update Daemon

Due to the ever-growing coding community, it is needed to provide regular updates to the smart speaker and keep it in sync with the latest technology. Hence an Update Daemon was required that could fetch updates at a regular interval.

The Updated Daemon was implemented in the following steps

1.Deciding the Update Interval

How frequently should we check for updates was the first question that was tackled while implementing this daemon.
We decided that we should check for Update, every time the Raspberry Pi starts and an internet connection was available.

2. Implementing The Decision

To start the Update script every time the Raspberry Pi starts, we decided to create Systemd rules.

[Unit]
Description=Update Check- SUSI Linux
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/home/pi/SUSI.AI/susi_linux/update_daemon/update_check.sh

[Install]
WantedBy=multi-user.target

The above rule waits for a network connection to be established with the Raspberry Pi and then triggers a bash script that fetches updates

3. Fetching The Updates


Now, a bash script was prepared that would fetch the latest changes from the online repo and merge the latest changes in the local repo

 

#!/bin/sh

UPSTREAM=${1:-‘@{u}’}
LOCAL=$(git rev-parse @)
REMOTE=$(git rev-parse “$UPSTREAM”)
BASE=$(git merge-base @ “$UPSTREAM”)
CHECK=”
if [ $LOCAL = $REMOTE ]
then
   echo “Up-to-date”
   CHECK=’up-to-date
elif [ $LOCAL = $BASE ]
then
   echo “Need to pull”
   CHECK=”Need-to-pull”
else
   echo “Diverged”
fi

if [$CHECK = “Need-to-pull”]
then
   git fetch UPSTREAM
   git merge UPSTREAM/master
fi

 

Resources

Tags

 

susi.ai, gsoc, gsoc’18, fossasia, update, daemon, update_daemon, smart speaker, systemd, hardware

Continue Reading

Create a Wireless Access Point Using a Raspberry Pi to Connect with SUSI Smart Speaker

To use the pi as a wifi bridge, a local network or just as a wifi range extender.We at FOSSASIA are using it as a network to connect between our SUSI.AI smart speaker and the Android and IOS devices. Or maybe because you can !! :’)

Requirements:

  1. Raspberry Pi Model 3(since we will be using an internal wifi)
  2. Power supply for the Pi.
  3. Monitor (optional)
  4. Keyboard (optional)
  5. Mouse (optional)

Steps:

1.Install and upgrade raspbian

 

Sudo apt-get update && sudo apt-get install

 

2. Install hostapd and dnsmasq .
This will allow us to use our raspberry pi as a wireless access point

 

apt-get remove –purge hostapd -yqq
apt-get update -yqq
apt-get upgrade -yqq
apt-get install hostapd dnsmasq -yqq

 

3. Now we will add broadcasting IP and DNS address in the dnsmasq configuration file

To access the configuration file use:

sudo nano /etc/dnsmasq.co

 

And to the bottom of the file, add the following commands

 

interface=wlan0
dhcp-range=10.0.0.2,10.0.0.5,255.255.255.0,12h

 

  1. Now to select the SSID and the PASSWORD for the access point, we’ll need to change the configurations of hostapd package
sudo nano /etc/hostapd/hostapd.conf

 

Then, use the following commands :

 

interface=wlan0
hw_mode=g
channel=10
auth_algs=1
wpa=2
wpa_key_mgmt=WPA-PSK
wpa_pairwise=CCMP
rsn_pairwise=CCMP
wpa_passphrase=“your_broadcasting_password”
ssid=“your_broadcasting_ssid”
ieee80211n=1
wmm_enabled=1
ht_capab=[HT40][SHORT-GI-20][DSSS_CCK-40]

 

  1. To finally sum up the configuration, we’ll have to create a  custom network interface that combines all the settings that we have made.
sudo nano /etc/network/interfaces

 

And add the following lines it the EOF

allow-hotplug wlan0
iface wlan0 inet static
address 10.0.0.1
netmask 255.255.255.0
network 10.0.0.0
broadcast 10.0.0.255

 

Now, we just have to have to disable default interfaces so that they do not interfere with the custom interfaces that we have made.

To do so

 

sudo nano /etc/dhcpcd.conf

 

Add the following line at the end of the file

denyinterfaces wlan0

 

  1. Now just restart the services

 

systemctl enable hostapd && systemctl enable dnsmasq

sudo service hostapd start && sudo service dnsmasq start

sudo reboot

 

Now, you will be able to enjoy a self-made access point which is used as a basic mode of connection in SUSI Smart Speaker and can also be used in various other access point methods.

 

References

 

Tags

GSOC’18 , FOSSASIA, ACCESS_POINT, SUSI.AI, GSOC, SUSI , SMART_SPEAKER

Continue Reading

Adding Audio Streaming from Youtube in SUSI Linux

In this blog post we will describe how the youtube streaming works in the

SUSI smart speaker and how audio is streamed directly from youtube videos.

To achieve this process, we have used an amazing Open-Source project called MPV music Player along with python libraries like Subprocess.

1.Processing a Query to the server

Firstly , the user asks the smart speaker to play the youtube audio by simply adding a ‘play’ word before his/her favorite song. eg. I’ll say ‘play despacito’ and then the command is recognized and a query is sent to the server which sends the following response as a JSON object.

“actions”: [
     {
       “type”: “answer”,
       “expression”: “Playing Luis Fonsi – Despacito ft. Daddy Yankee”
     },
     {
       “identifier”: “kJQP7kiw5Fk”,
       “identifier_type”: “youtube”,
       “type”: “video_play”
     }]

2.Parsing the response

Then the speaker parses the response in the following way.

The Speaker traverses through all the actions returned in the response and checks for all the “identifier” by assigning a custom class to it.

class VideoAction(BaseAction):
   def __init__(self, identifier , identifier_type):
       super().__init__()
       self.identifier = identifier
       self.identifier_type = identifier_type

Now we check whether the query is the type of a custom class VideoAction and then the client processes the query as the response.

      elif isinstance(action, VideoAction):
          result[‘identifier’] = action.identifier
           audio_url = result[‘identifier’]  

3.Implementing the Actions

Now that we have identified that the response contains a Video Action, we can finally implement a way to play the audio from the URL.
We use a music player called MPV Music Player and the library Subprocess to make it run asynchronously.

if ‘identifier’ in reply.keys():
   classifier = reply[‘identifier’]
   if classifier[:3] == ‘ytd’:
       video_url = reply[‘identifier’]
       video_pid = subprocess.Popen(‘mpv –no-video https://www.youtube.com/watch?v={} –really-quiet &’.format(video_url[4:]), shell=True)  # nosec #pylint-disable type: ignore
       self.video_pid = video_pid.pid


This is how audio is streamed from youtube videos in SUSI Smart Speaker.

Resources

  1. https://github.com/mpv-player/mpv
  2. https://docs.python.org/2/library/subprocess.html
  3. https://github.com/fossasia/susi_linux
  4. https://github.com/fossasia/susi_api_wrapper

Tags

fossasia, gsoc’18, susi, susi.ai, youtube, music, mp3 , mpv, audio stream

 

Continue Reading

Creating a Media Daemon for SUSI Smart Speaker

A daemon in reference of operating systems is a computer program that runs as a background process rather than under direct control of the user. Various daemons are being used in SUSI smart speaker.

The following features have been created

  • Update Daemon
  • Media Discovery Daemon
  • Factory Reset Daemon

In this blog, we’ll be discussing the implementation of the Media Discovery Daemon

Media Discovery Daemon:

The SUSI Smart speaker will have an essential feature which will allow the Users to play music from their USB devices. Hence , a media daemon will be running which will detect a USB connection and then scan it’s contents checking for all the mp3 files and then create custom SUSI skills to allow SUSI Smart Speaker to play music from your USB device.

 

The Media Daemon was implemented in the following steps

1.UDEV Rules

We had to figure out a way to run our daemon as soon as the user inserted the USB storage and stop the daemon as soon as the USB storage was removed

 

So, we used UDEV rules to trigger the Media Daemon.

 

ACTION==“add”, KERNEL==“sd?”, SUBSYSTEM==“block”, ENV{ID_BUS}==“usb”, RUN=“/home/pi/SUSI.AI/susi_linux/media_daemon/autostart.sh”ACTION==“remove, KERNEL==“sd?”, SUBSYSTEM==“block”, ENV{ID_BUS}==“usb”, RUN=“/home/pi/SUSI.AI/susi_linux/media_daemon/autostop.sh”

The Udev rules trigger a script called ‘autostart.sh’  on USB detection and a script called ‘autostop.sh’ on USB removal.

2. Custom Skill Creation

As the USB connection is now detected ,a script is triggered which checks the presence of a  local SUSI server in the repo. If a local server instance is detected,a python script is triggered which parses through the USB mount point and checks for the list of mp3 files present in the storage device and then create a custom skill file in the local server instance.

 

media_daemon_folder = os.path.dirname(os.path.abspath(__file__))
base_folder = os.path.dirname(media_daemon_folder)
server_skill_folder = os.path.join(base_folder, ‘susi_server/susi_server/data/generic_skills/media_discovery’)
server_settings_folder = os.path.join(base_folder, ‘susi_server/susi_server/data/settings’)

def make_skill(): # pylint-enable
   name_of_usb = get_mount_points()
   print(type(name_of_usb))
   print(name_of_usb[0])
   x = name_of_usb[0]
   os.chdir(‘{}’.format(x[1]))
   USB = name_of_usb[0]
   mp3_files = glob(“*.mp3”)
   f = open( media_daemon_folder +‘/custom_skill.txt’,‘w’)
   music_path = list()
   for mp in mp3_files:
       music_path.append(“{}”.format(USB[1]) + “/{}”.format(mp))

   song_list = ” “.join(music_path)
   skills = [‘play audio’,‘!console:Playing audio from your usb device’,‘{“actions”:[‘,‘{“type”:”audio_play”, “identifier_type”:”url”, “identifier”:”file://’+str(song_list) +‘”}’,‘]}’,‘eol’]
   for skill in skills:
       f.write(skill + ‘\n’)
   f.close()
   shutil.move( media_daemon_folder + ‘custom_skill.txt’, server_skill_folder)
   f2 = open(server_settings_folder + ‘customized_config.properties’,‘a’)
   f2.write(‘local.mode = true’)
   f2.close()

def get_usb_devices():
   sdb_devices = map(os.path.realpath, glob(‘/sys/block/sd*’))
   usb_devices = (dev for dev in sdb_devices
       if ‘usb’ in dev.split(‘/’)[5])
   return dict((os.path.basename(dev), dev) for dev in usb_devices)

def get_mount_points(devices=None):
   devices = devices or get_usb_devices() # if devices are None: get_usb_devices
   output = check_output([‘mount’]).splitlines() #nosec #pylint-disable type: ignore
   output = [tmp.decode(‘UTF-8’) for tmp in output ] # pytlint-enable
   def is_usb(path):
       return any(dev in path for dev in devices)
   usb_info = (line for line in output if is_usb(line.split()[0]))
   return [(info.split()[0], info.split()[2]) for info in usb_info] 

 

Now a custom skill file will be created in the local server instance by the name of `custom_skill.txt` and the user can play audio from USB by speaking the command ‘play audio’

 

3. Preparing for the Next USB insertion

Now if the User wants to update his/her music library or wants to use another USB storage device. The USB will be removed and hence the custom skill file is also deleted from the script ‘autstop.sh’ which is triggered via the UDEV rules

#! /bin/bash

SCRIPT_PATH=$(realpath $0)
DIR_PATH=$(dirname $SCRIPT_PATH)

cd $DIR_PATH/../susi_server/susi_server/data/generic_skills/media_discovery/

sudo rm custom_skill.txt  

 

This is how the Media Discovery Daemon works in SUSI Smart Speaker

 

References

Tags

gsoc, gsoc’18 , fossasia, susi.ai, smart speaker, media daemon, susi skills

Continue Reading

Add Unit Test in SUSI.AI Android App

Unit testing is an integral part of software development. Hence, this blog focuses on adding unit tests to SUSI.AI Android app. To keep things simple, take a very basic example of anonymize feedback section. In this section the email of the user is truncated after ‘@’ symbol in order to maintain the anonymity of the user. Here is the function that takes ‘email’ as a parameter and returns the truncated email that had to be displayed in the feedback section :

fun truncateEmailAtEnd(email: String?): String? {
   if (!email.isNullOrEmpty()) {
       val truncateAt = email?.indexOf('@')
       if (truncateAt is Int && truncateAt != -1) {
           return email.substring(0, truncateAt.plus(1)) + " ..."
       }
   }
   return null
}

 

The unit test has to be written for the above function.

Step – 1 : Add the following dependencies to your build.gradle file.

//unit test
testImplementation "junit:junit:4.12"
testImplementation "org.mockito:mockito-core:1.10.19"

 

Step – 2 : Add a file in the correct package (same as the file to be tested) in the test package. The function above is present in the Utils.kt file. Thus create a file, called UtilsTest.kt, in the test folder in the package org.fossasia.susi.ai.helper’.

Step – 3 : Add a method, called testTruncateEmailAtEnd(), to the UtilsTest.kt and add ‘@Test’ annotation to before this method.

Step – 4 : Now add tests for various cases, including all possible corner cases that might occur. This can be using assertEquals() which takes in two paramters – expected value and actual value.

For example, consider an email ‘[email protected]’. This email is passed as a parameter to the truncateAtEnd() method. The expected returned string would be ‘[email protected] …’. So, add a test for this case using assertEquals() as :

assertEquals("[email protected] ...", Utils.truncateEmailAtEnd("[email protected]"))

 

Similary, add other cases, like empty email string, null string, email with numbers and symbols and so on.

Here is how the UtilsTest.kt class looks like.

package org.fossasia.susi.ai.helper

import junit.framework.Assert.assertEquals
import org.junit.Test

class UtilsTest {
   @Test
   fun testTruncateEmailAtEnd() {
       assertEquals("[email protected] ...", Utils.truncateEmailAtEnd("[email protected]"))
       assertEquals(null, Utils.truncateEmailAtEnd("testuser"))
       assertEquals(null, Utils.truncateEmailAtEnd(""))
       assertEquals(null, Utils.truncateEmailAtEnd(" "))
       assertEquals(null, Utils.truncateEmailAtEnd(null))
       assertEquals("[email protected] ...", Utils.truncateEmailAtEnd("[email protected]"))
       assertEquals("[email protected] ...", Utils.truncateEmailAtEnd("[email protected]"))
       assertEquals("[email protected] ...", Utils.truncateEmailAtEnd("[email protected]"))
       assertEquals(null, Utils.truncateEmailAtEnd("test [email protected]"))
   }
}

 

Note: You can add more tests to check for other general and corner cases.

Step – 5 : Run the tests in UtilsTest.kt.

If all the test cases pass, then the tests pass. But, if the tests fail, try to figure out the cause of failure of the tests and add/modify the code in the Utils.kt accordingly. This approach helps recognize flaws in the existing code thereby reducing the risk of bugs and failures.

Resources

Continue Reading

Display skills sorted in different orders in SUSI.AI Android App

Skills in SUSI.AI were displayed in a random order earlier as per the response received from the server. To provide more flexibility to the users, the skills can be sorted by various orders like top-rated, lexicographical, recently updated and so on. This blog shows how to get sorted skills from the server using the getSkillList.json API.

API Information

For requesting a list of SUSI skills, the endpoint used is /cms/getSkillList.json.
This will give you the sorted skills as per the applied filter. Some of the filters include top rated skills, recently updated skills, newly created skills, etc.

Base URL : https://api.susi.ai/cms/getSkillList.json

Parameters to be passed :

  • group – This is the group to which a skill belongs to.
  • language – The language in which the skill is needed.
  • applyFilter – This parameter tells if the filtering needs to be enabled.
  • filter_type – This is the order in which the skills need to be sorted and is applicable if applyFilter is true.
  • filter_name – This tells whether the order of sorting needs to be ascending or descending and is applicable if applyFilter is true.

Currently, there are following filters available :

  • Top Rated : The skills will be sorted based on the skills ratings by users.

filter_type :  rating
filter_name : ascending or descending (based on the requirement).

 

  • Lexicographical : The skills will be sorted in alphabetical order.

filter_type :  lexicographical
filter_name : ascending (to show skills in the order A-Z) or (descending to show skills in the order Z-A).

 

  • Newly Created : The skills will be displayed based on the date of creation.

filter_type :  creation_date
filter_name : ascending to show newly created skills first and descending to show the oldest created skills first.

 

  • Recently Updated : The skills will be sorted based on the date when they were last updated.

filter_type :  modified_date
filter_name : ascending or descending as per requirement.

 

  • Feedback Count : The skills will be sorted as per the feedback count.

filter_type :  feedback
filter_name : ascending to show skills with the most number of feedbacks first and descending to show skills with the least number of feedbacks first.

 

  • This Week Usage : The skills will be sorted as per the usage analytics of the week.

filter_type :  usage
duration : 7
filter_name : descending to show the most used skill first and vice-versa.

 

  • This Week Usage : The skills will be sorted as per the usage analytics for the last 30 days.

filter_type :  usage
duration : 30
filter_name : descending to show the most used skill first and vice-versa.

 

Note: In all the above cases, the ‘applyFilter’ param will be passed with the value ‘true’ otherwise the skills will not be sorted.

Here is an example of a URL for displaying the top rated skills:

https://api.susi.ai/cms/getSkillList.json?group=All&language=en&applyFilter=true&filter_name=descending&filter_type=rating

 

To make a request to the getSkillList.json API, make a GET request as follows :

@GET("/cms/getSkillList.json")
Call<ListSkillsResponse> fetchListSkills(@QueryMap Map<String, String> query);

 

Here the query map contains all the aforementioned params.

Now, make the GET request using Retrofit from the model :

private lateinit var authResponseCallSkills: Call<ListSkillsResponse>

override fun fetchSkills(group: String, language: String, listener: IGroupWiseSkillsModel.OnFetchSkillsFinishedListener) {
   val queryObject = SkillsListQuery(group, language, "true", "descending", "rating")
   authResponseCallSkills = ClientBuilder.fetchListSkillsCall(queryObject)

   authResponseCallSkills.enqueue(object : Callback<ListSkillsResponse> {
       override fun onResponse(call: Call<ListSkillsResponse>, response: Response<ListSkillsResponse>) {
           listener.onSkillFetchSuccess(response, group)
       }

       override fun onFailure(call: Call<ListSkillsResponse>, t: Throwable) {
           Timber.e(t)
           listener.onSkillFetchFailure(t)
       }
   })
}

override fun cancelFetch() {
   try {
       authResponseCallSkills.cancel()
   } catch (e: Exception) {
       Timber.e(e)
   }
}

 

The skills in the filteredData array, received in the JSON response, shall be sorted in the order based on the filter_type and filter_name params that you passed. Now, this array can be used to display skills on the skills listing page.

Resources

Continue Reading

Showing skills based on different metrics in SUSI Android App using Nested RecyclerViews

SUSI.AI Android app had an existing skills listing page, which displayed skills under different categories. As a result, there were a number of API calls at almost the same time, which led to slowing down of the app. Thus, the UI of the Skill Listing page has been changed so as to reduce the number of API calls and also to make this page more useful to the user.

API Information

For getting a list of SUSI skills based on various metrics, the endpoint used is /cms/getSkillMetricsData.json

This will give you top ten skills for each metric. Some of the metrics include skill ratings, feedback count, etc. Sample response for top skills based on rating :

"rating": [
  {
    "model": "general",
    "group": "Knowledge",
    "language": "en",
    "developer_privacy_policy": null,
    "descriptions": "A skill to tell atomic mass and elements of periodic table",
    "image": "images/atomic.png",
    "author": "Chetan Kaushik",
    "author_url": "https://github.com/dynamitechetan",
    "author_email": null,
    "skill_name": "Atomic",
    "protected": false,
    "reviewed": false,
    "editable": true,
    "staffPick": false,
    "terms_of_use": null,
    "dynamic_content": true,
    "examples": ["search for atomic mass of radium"],
    "skill_rating": {
      "bookmark_count": 0,
      "stars": {
        "one_star": 0,
        "four_star": 3,
        "five_star": 8,
        "total_star": 11,
        "three_star": 0,
        "avg_star": 4.73,
        "two_star": 0
      },
      "feedback_count": 3
    },
    "usage_count": 0,
    "skill_tag": "atomic",
    "supported_languages": [{
      "name": "atomic",
      "language": "en"
    }],
    "creationTime": "2018-07-25T15:12:25Z",
    "lastAccessTime": "2018-07-30T18:50:41Z",
    "lastModifiedTime": "2018-07-25T15:12:25Z"
  },
  .
  .

]

 

Note : The above response shows only one of the ten objects. There will be ten such skill metadata objects inside the “rating” array. It contains all the details about skills.

Implementation in SUSI.AI Android App

Skill Listing UI of SUSI SKill CMS

Skill Listing UI of SUSI Android App

The UI of skills listing in SUSI Android app displays skills for each metric in a horizontal recyclerview, nested in a vertical recyclerview. Thus, for implementing horizontal recyclerview inside vertical recyclerview, you need two viewholders and two adapters (one each for a recyclerview). Let us go through the implementation.

  • Make a query object consisting of the model and language query parameters that shall be passed in the request to the server.

val queryObject = SkillMetricsDataQuery("general", 
PrefManager.getString(Constant.LANGUAGE,Constant.DEFAULT))

 

  • Fetch the skills based on metrics, by calling fetch in SkillListModel which then makes an API call to fetch groups.

skillListingModel.fetchSkillsMetrics(queryObject, this)

 

  • When the API call is successful, the below mentioned method is called which in turn parses the received response and updates the adapter to display the skills based on different metrics.

override fun onSkillMetricsFetchSuccess(response: Response<ListSkillMetricsResponse>) {
   skillListingView?.visibilityProgressBar(false)
   if (response.isSuccessful && response.body() != null) {
       Timber.d("METRICS FETCHED")
       metricsData = response.body().metrics
       if (metricsData != null) {
           metrics.metricsList.clear()
           metrics.metricsGroupTitles.clear()
           if (metricsData?.rating != null) {
               if (metricsData?.rating?.size as Int > 0) {
                   metrics.metricsGroupTitles.add(utilModel.getString(R.string.metric_rating))
                   metrics.metricsList.add(metricsData?.rating)
                   skillListingView?.updateAdapter(metrics)
               }
           }

           if (metricsData?.usage != null) {
               if (metricsData?.usage?.size as Int > 0) {
                   metrics.metricsGroupTitles.add(utilModel.getString(R.string.metric_usage))
                   metrics.metricsList.add(metricsData?.usage)
                   skillListingView?.updateAdapter(metrics)
               }
           }

           if (metricsData?.newest != null) {
               val size = metricsData?.newest?.size
               if (size is Int) {
                   if (size > 0) {
                       metrics.metricsGroupTitles.add(utilModel.getString(R.string.metric_newest))
                       metrics.metricsList.add(metricsData?.newest)
                       skillListingView?.updateAdapter(metrics)
                   }
               }
           }

           if (metricsData?.latest != null) {
               if (metricsData?.latest?.size as Int > 0) {
                   metrics.metricsGroupTitles.add(utilModel.getString(R.string.metric_latest))
                   metrics.metricsList.add(metricsData?.latest)
                   skillListingView?.updateAdapter(metrics)
               }
           }

           if (metricsData?.feedback != null) {
               if (metricsData?.feedback?.size as Int > 0) {
                   metrics.metricsGroupTitles.add(utilModel.getString(R.string.metric_feedback))
                   metrics.metricsList.add(metricsData?.feedback)
                   skillListingView?.updateAdapter(metrics)
               }
           }

           if (metricsData?.topGames != null) {
               val size = metricsData?.feedback?.size
               if (size is Int) {
                   if (size > 0) {
                       metrics.metricsGroupTitles.add(utilModel.getString(R.string.metrics_top_games))
                       metrics.metricsList.add(metricsData?.topGames)
                       skillListingView?.updateAdapter(metrics)
                   }
               }
           }

           skillListingModel.fetchGroups(this)
       }
   } else {
       Timber.d("METRICS NOT FETCHED")
       skillListingView?.visibilityProgressBar(false)
       skillListingView?.displayError()
   }
}

 

  • When skills are fetched, the data in adapter is updated using skillMetricsAdapter.notifyDataSetChanged()

override fun updateAdapter(metrics: SkillsBasedOnMetrics) {
   swipe_refresh_layout.isRefreshing = false
   if (errorSkillFetch.visibility == View.VISIBLE) {
       errorSkillFetch.visibility = View.GONE
   }
   skillMetrics.visibility = View.VISIBLE
   this.metrics.metricsList.clear()
   this.metrics.metricsGroupTitles.clear()
      this.metrics.metricsList.addAll(metrics.metricsList)
   this.metrics.metricsGroupTitles.addAll(metrics.metricsGroupTitles)
      skillMetricsAdapter.notifyDataSetChanged()
}

 

  • The data is set to the layout in two adapters made earlier. The following is the code to set the title for the metric and adapter to horizontal recyclerview. This is the SkillMetricsAdapter to set data to show item in vertical recyclerview.

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
   if (metrics != null) {
       if (metrics.metricsList[position] != null) {
           holder.groupName?.text = metrics.metricsGroupTitles[position]
       }

       skillAdapterSnapHelper = StartSnapHelper()
       holder.skillList?.setHasFixedSize(true)
       val mLayoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
       holder.skillList?.layoutManager = mLayoutManager
       holder.skillList?.adapter = SkillListAdapter(context, metrics.metricsList[position], skillCallback)
       holder.skillList?.onFlingListener = null
       skillAdapterSnapHelper.attachToRecyclerView(holder.skillList)
   }
}

 

Continue Reading

Displaying Avatar of Users using Gravatar on SUSI.AI Android App

This blog post shows how the avatar image of the user is displayed in the feedback section using the Gravatar service on SUSI.AI Android app. A Gravatar is a Globally Recognized Avatar. Your Gravatar is an image that follows you from site to site appearing beside your name when you do things like comment or post on a blog. So, it was decided to integrate the service to SUSI.AI, so that it helps to  identify the user via the avatar image too.

Implementation

  • The aim is to use the user’s email address to get his/her avatar. For this purpose, Gravatar exposes a publicly available avatar of the user, which can be accessed via following steps :
    • Creating the Hash of the email
    • Sending the image request to Gravatar
  • For the purpose of creating the MD5 hash of the email, use the MessageDigest class. The function takes an algorithm such as SHA-1, MD5, etc. as input and returns a MessageDigest object that implements the specified digest algorithm.

Perform a final update on the digest using the specified array of bytes, which then completes the digest computation. That is, this method first calls update(input), passing the input array to the update method, then calls digest().

fun toMd5Hash(email: String?): String? {
   try {
       val md5 = MessageDigest.getInstance("MD5")
       val md5HashBytes: Array<Byte> = md5.digest(email?.toByteArray()).toTypedArray()
       return byteArrayToString(md5HashBytes)
   } catch (e: Exception) {
       return null
   }
}

 

  • Convert the byte array to String, which is the requires MD5 hash of the email string.

fun byteArrayToString(array: Array<Byte>): String {
   val result = StringBuilder(array.size * 2)
   for (byte: Byte in array) {
       val toAppend: String = String.format("%x", byte).replace(" ", "0")
       result.append(toAppend)
   }
   return result.toString()
}

 

  • Now, a URL is generated using the hash. This is the URL that will be used to fetch the avatar.
  • The URL format is https://www.gravatar.com/avatar/HASH, where HASH is the hash of the email of the user. In case, the hash is invalid, Gravatar returns a placeholder avatar.
  • Also, append ‘.jpg’ to the URL to maintain image format consistency in the app.
  • Finally, load this URL using Picasso and set it in the appropriate view holder.

fun setAvatar(context: Context, email: String?, imageView: ImageView) {
   val imageUrl: String = GRAVATAR_URL + toMd5Hash(email) + ".jpg"
   Picasso.with(context)
           .load(imageUrl)
           .fit().centerCrop()
           .error(R.drawable.ic_susi)
           .transform(CircleTransform())
           .into(imageView)
}

 

You can now see the avatar image of the users in the feedback section. 😀

Resources

 

Continue Reading

Use objects to pass multiple query parameters when making a request using Retrofit

There are multiple instances where there is a need to make an API call to the SUSI.AI server to send or fetch data. The Android client uses Retrofit to make this process easier and more convenient.
While making a GET or POST request, there are often multiple query parameters that need to sent along with the base url. Here is an example how this is done:

@GET("/cms/getSkillFeedback.json")
Call<GetSkillFeedbackResponse> fetchFeedback(
                          @Query("model") String model,
                          @Query("group") String group,
                          @Query("language") String language,
                          @Query("skill") String skill);

 

It can be seen that the list of params can be very long indeed. A long list of params would lead to more risks of incorrect key value pairs and typos.

This blog would talk about replacing such multiple params with objects. The entire process would be explained with the help of an example of the API call being made to the getSkillFeedback.json API.

Step – 1 : Replace multiple params with a query map.

@GET("/cms/getSkillFeedback.json")
Call<GetSkillFeedbackResponse> fetchFeedback(@QueryMap Map<String, String> query);

 

Step – 2 : Make a data class to hold query param values.

data class FetchFeedbackQuery(
       val model: String,
       val group: String,
       val language: String,
       val skill: String
)

 

Step – 3 : Instead of passing all different strings for different query params, pass an object of the data class. Hence, add the following code to the ISkillDetailsModel.kt interface.

...

fun fetchFeedback(query: FetchFeedbackQuery, listener: OnFetchFeedbackFinishedListener)
...

 

Step – 4 : Add a function in the singleton file (ClientBuilder.java) to get SUSI client. This method should return a call.

...

public static Call<GetSkillFeedbackResponse> fetchFeedbackCall(FetchFeedbackQuery queryObject){
   Map<String, String> queryMap = new HashMap<String, String>();
   queryMap.put("model", queryObject.getModel());
   queryMap.put("group", queryObject.getGroup());
   queryMap.put("language", queryObject.getLanguage());
   queryMap.put("skill", queryObject.getSkill());
   //Similarly add other params that might be needed
   return susiService.fetchFeedback(queryMap);
}
...

 

Step – 5 : Send a request to the getSkillFeedback.json API by passing an object of FetchFeedbackQuery data class to the fetchFeedbackCall method of the ClientBuilder.java file which in turn would return a call to the aforementioned API.

...

override fun fetchFeedback(query: FetchFeedbackQuery, listener:  
                                   ISkillDetailsModel.OnFetchFeedbackFinishedListener) {

   fetchFeedbackResponseCall = ClientBuilder.fetchFeedbackCall(query)
   ...
}

 

No other major changes are needed except that instead of passing individual strings for each query param as params to different methods and creating maps at different places like in a view, create an object of FetchFeedbackQuery class and use it to pass data throughout the project. This ensures type safety. Also, data classes reduce the code length significantly and hence are more convenient to use in practice.

Resources

Continue Reading

Make an API to check if an email address has been registered for SUSI.AI

This blog post talks about the implementation of the checkRegistration.json API on the SUSI.AI server, which is a part of the AAA system. The API endpoint to check if an email address has been registered for SUSI is https://api.susi.ai/aaa/checkRegistration.json

It accepts one compulsory url parameter –

  • check_email – It is the parameter that contains the string type email address which the user enters in the email address field of the login screen.

The minimalUserRole is set to ANONYMOUS for this API, as initially the registration status of the email address is unknown.

API Development

  • The parameter is first extracted via the post object that is passed to the serviceImpl function. The  parameter is then stored in a variable. If the parameter is absent, then it is set to the default value null.
  • There is a check if the email is null. If null, an exception is thrown.
  • This code snippet discusses the above two points –

@Override
public ServiceResponse serviceImpl(Query post, HttpServletResponse    
   response, Authorization auth, final JsonObjectWithDefault permissions)
			throws APIException {

        String checkEmail = post.get("check_email", null);

        JSONObject result = new JSONObject();
        
        if (checkEmail == null) {
            throw new APIException(422, "Email not provided.");
        }
.
.
.

 

  • Set the credential variable of type ClientCredential by passing the parameters passwd_login and checkEmail to the ClientCredential constructor.
  • Finally pass this credential variable to the getAuthentication method defined in the DAO to return the authentication object.
  • The authentication object then invokes the authentication.getIdentity() method. If the result is null, it means the email address has not been registered yet and vice-versa.
  • Internally, the entire checking procedure is done from the authentication.json file that is stored in data/settings/ directory of the server.
  • The response object is then sent with three key values mainly, apart from the session object. They are
    • accepted –  true – It tells that the API call has been successful.
    • exists – It tells that the email address has already been registered.
    • check_email –  It is the same email address that was sent as a query parameter.

Here are the important code snippets –

  • Continuation of the first code snippet –

.
.
.
// check if id exists already

ClientCredential credential = new    
   ClientCredential(ClientCredential.Type.passwd_login, checkEmail);
	Authentication authentication =DAO.getAuthentication(credential);

		if (authentication.getIdentity() != null) {
			result.put("exists", true);
		} else {
			result.put("exists", false);
		}
		
		result.put("accepted", true);
		result.put("check_email", checkEmail);

		return new ServiceResponse(result);
	}

 

  • Sample response of checkRegistration.json API endpoint –

{
  "check_email": "[email protected]",
  "session": {"identity": {
    "type": "host",
    "name": "127.0.0.1_356778ca",
    "anonymous": true
  }},
  "exists": true,
  "accepted": true
}

 

The API development was done in the above explained way. This API will be used in improving the authentication flow in the Android client, where, if an email address has already been registered, then the user would be taken to the ‘Enter Password Screen’ otherwise he/she would be directed to the Signup screen.

Resources

Continue Reading
Close Menu
%d bloggers like this: