Displaying SUSI Smart speaker under Devices while logging in

The user should be given an ability to access all his devices on one page(Smart Speaker, IOS Device, WebClient and the Android Device). The user was previously allowed to access his/her web app account, the IOS app, Android App. But not the Smart Speaker. Now, this feature will allow the user to easily manage the Smart Speaker devices without many hassles.

In this post, we will be talking about the API’s that we have used to send the details of the Smart-Speaker to the server.

About the API’s

  1. Below is the API endpoint which will return the list of all devices present under the user’s account

We use the following endpoint

/aaa/ListUserSettings.json?/access_token=access_token

 

Below is sample response :

“devices”: {
“8C-39-45-cc-eb-95”: {
“name”: “Device 1”,
“room”: “Room 1”,
“geolocation”: {
“latitude”: “52.34567”,
“longitude”: “62.34567”
}
 }
}

 

  1. The second endpoint that we will be using is to add a new Device under the devices section

API Endpoint

/aaa/addNewDevice.json?

 

This endpoint has the following parameters

  • macid (Mac address of the device)
  • name (Name of the device)
  • room (Room info of the device)
  • latitude (Latitude info of the device)
  • longitude (Longitude info of the device)

 

After successfully hitting the endpoint , you’ll get the following response

 

{
“accepted”: true,
“message”: “You have successfully added the device!”,
“session”: {“identity”: {
“type”: “email”,
“name”: [email protected],
“anonymous”: false
}}
}

 

Implementing the API’s

  1. First, we check the server for existing devices. This step is implemented primarily to check weather our current Smart Speaker is already configured or not.
get_device_info = api_endpoint + ‘/aaa/listUserSettings.json?’

param1 = {
       ‘access_token’:access_token
   }

   # print(access_token)

   if access_token is not None:
       device_info_response = requests.get(get_device_info,param1)
       device_info = device_info_response.json()

   # print(device_info)

If the current device is not already configured on Server, we proceed to next step.

  1. Now we will configure the device with the server and then post the device settings there.
    We will implement the API in the following way:

 

if device_info is not None:
   device = device_info[‘devices’] # list of existing mac ids
   print(device)
   session = device_info[‘session’] # session info
   identity = session[‘identity’]
   name = identity[‘name’]
   
   params2 = {
   ‘macid’: macid,
   ‘name’: name,
   ‘device’: ‘Smart Speaker’,
   ‘access_token’: access_token
   }

   for dev in device:
       if dev == macid:
           print(‘Device already configured’)
           return
       else :
           adding_device = requests.post(add_device_url, params2)
           print(adding_device.url)

 

To extract the mac address from the speaker and pass it as the params , we use a python library called UUID and this is how SUSI Smart Speaker is displayed on the web client(chat.susi.ai).

Resources

Tags

 

Continue Reading

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

Adding Offline support To SUSI Linux

Till now, SUSI smart speaker was working only as an online model like the other speakers in the market. For the first time, we have introduced a feature which allows the speaker to work offline. We deployed the server on the hardware itself and also provide the option of an online server as a fallback.

 

The Offline Support was implemented in the following steps

 

Step 1: Deploying SUSI Server Locally

 

Firstly , configure a bash script to allow automatic deployment of the server along with the initialization of the susi_linux script.

 

echo “Deploying local server”
if  [ ! -e “susi-server” ]
then
   git clone https://github.com/fossasia/susi_server.git
fi

if [ -e “susi_server” ]
then    
   cd susi_server
   git submodule update –recursive –remote
   git submodule update –init –recursive
   ./gradlew build
   bin/start.sh
fi 

 

The above builds the server and deploys it on ‘localhost:4000’.

 

Then, add the following test on SUSI Linux wrapper to check if the local server is up and running. Using the local server not adds an offline support but also increases the efficiency by around 30%.

def check_local_server():
   test_params = {
       ‘q’: ‘Hello’,
       ‘timezoneOffset’: int(time.timezone / 60)
   }
   try:
       chat_url = ‘http://localhost:4000/susi/chat.json’
       if (requests.get(chat_url, test_params)):
           print(‘connected to local server’)
           global api_endpoint
           api_endpoint = ‘http://localhost:4000’
   except requests.exceptions.ConnectionError:
       print(‘local server is down’)


check_local_server()

 

As shown above, this is a test checking for the local server. If the local server is down, the online server is chosen as a fallback

 

Step 2: Adding an Offline STT Service

Now, that we are able to process a query offline. We must have a way in which, we can recognize the user’s voice commands without using the internet. For that, we use the service of PocketSphinx. But first, we check if the internet is available or not

 

def internet_on():
       try:
           urllib2.urlopen(‘http://216.58.192.142’, timeout=1)  # nosec #pylint-disable type: ignore
           return True  # pylint-enable
       except urllib2.URLError as err:
           print(err)
           return False

 

If the internet connection is available, we use the online STT service which is Google STT ( default) and switch over to PocketSphinx in case the internet connection is not available.

 

Step 3: Adding the Offline TTS service

Finally, we’ll need an offline TTS service which will help us turn SUSI’s response to voice commands. We’ll be using a service called flite TTS as our offline TTS.

 

elif payload == ‘ConnectionError’:
            self.notify_renderer(‘error’, ‘connection’)                                  self.notify_renderer(‘error’, ‘connection’)
            config[‘default_tts’] = ‘flite’
            os.system(‘play extras/connect-error.wav’)              

 

We check if there is a ConnectionError, and then we switch to flite TTS after play an error query

 

Final Output:

We now get a Smart Speaker which is functional without any internet connection.

 

References

Tags

 

Fossasia, susi, gsoc, gsoc’18, offline_tts , offline_stt ,flite , pocketsphinx

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

Displaying Skills Feedback on SUSI.AI Android App

SUSI.AI has a feedback system where the user can post feedback for a skill using Android, iOS, and web clients. In skill details screen, the feedback posted by different users is displayed. This blog shows how the feedback from different users can be displayed in the skill details screen under feedback section.

Three of the items from the feedback list are displayed in the skill details screen. To see the entire list of feedback, the user can tap the ‘See All Reviews’ option at the bottom of the list.

The API endpoint that has been used to get skill feedback from the server is https://api.susi.ai/cms/getSkillFeedback.json

The following query params are attached to the above URL to get the specific feedback list :

  • Model
  • Group
  • Language
  • Skill Name

The list received is an array of `Feedback` objects, which hold three values :

  • Feedback String (feedback) – Feedback string posted by a user
  • Email (email) – Email address of the user who posted the feedback
  • Time Stamp – Time of posting feedback

To display feedback, use the RecyclerView. There can be three possible cases:

  • Case – 1: Size of the feedback list is greater than three
    In this case, set the size of the list to three explicitly in the FeedbackAdapter so that only three view holders are inflated. Inflate the fourth view holder with “See All Reviews” text view and make it clickable if the size of the received feedback list is greater than three.
    Also, when the user taps “See All Reviews”, launch an explicit intent to open the Feedback Activity. Set the AllReviewsAdapter for this activity. The size of the list will not be altered here because this activity must show all feedback.
  • Case – 2: Size of the feedback list is less than or equal to three
    In this case simply display the feedback list in the SkillDetailsFragment and there is no need to launch any intent here. Also, “See All Reviews” will not be displayed here.

    Case – 3: Size of the feedback list is zero
    In this case simply display a message that says no feedback has been submitted yet.Here is an example of how a “See All Reviews” screen looks like :

Implementation

First of all, define an XML layout for a feedback item and then create a data class for storing the query params.

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


Now, make the GET request using Retrofit from the model (M in MVP).

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

   fetchFeedbackResponseCall = ClientBuilder.fetchFeedbackCall(query)

   fetchFeedbackResponseCall.enqueue(object : Callback<GetSkillFeedbackResponse> {
       override fun onResponse(call: Call<GetSkillFeedbackResponse>, response: Response<GetSkillFeedbackResponse>) {
           listener.onFetchFeedbackModelSuccess(response)
       }

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

override fun cancelFetchFeedback() {
   fetchFeedbackResponseCall.cancel()
}


The feedback list received in the JSON response can now be used to display the user reviews with the help of custom adapters, keeping in mind the three cases already discussed above.

Resources

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
Close Menu
%d bloggers like this: