Auto Updating SUSI Android APK and App Preview on appetize.io

This blog will cover the way in which the SUSI Android APK is build automatically after each commit and pushed to “apk” branch in the github repo. Other thing which will be covered is that how the app preview on appetize.io can be updated after each commit. This is basically for the testers who wish to test the SUSI Android App. There are four ways to test the SUSI Android App. One is to simply download the alpha version of the app from the Google PlayStore. Here is the link to the app. Join the alpha testing and report bugs on the github issue tracker of the repo. Other way is to build the app from Android Studio but you may need to set the complete project. If you are looking to contribute in the project, this is the advised way to test the app. The other two ways are explained below.

Auto Building of APK and pushing to “apk” branch

We have written a script which does following steps whenever a PR is merged:

  1. Checks if the commit is of a PR or a commit to repo
  2. If not of PR, configures a user whose github account will be used to push the APKs.
  3. Clones the repo, generates the debug and release APK.
  4. Deletes everything in the apk branch.
  5. Commits and Pushes new changes to apk branch.

This script is written for people or testers who do not have android studio installed in their computer and want to test the app. So, they can directly download the apk from the apk branch and install it in their phone. The APK is always updated after each commit. So, whenever a tester downloads the APK from apk branch, he will always get the latest app.

if [[ $CIRCLE_BRANCH != pull* ]]
then
    git config --global user.name "USERNAME"
    git config --global user.email "EMAIL"

    git clone --quiet --branch=apk https://USERNAME:[email protected]/fossasia/susi_android apk > /dev/null
    ls
    cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/app/build/outputs/apk/app-debug.apk apk/susi-debug.apk
    cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/app/build/outputs/apk/app-release-unsigned.apk apk/susi-release.apk
    cd apk

    git checkout --orphan workaround
    git add -A

    git commit -am "[Circle CI] Update Susi Apk"

    git branch -D apk
    git branch -m apk

    git push origin apk --force --quiet > /dev/null
fi

Auto Updating of App Preview on appetize.io

The APKs generated in the above step can now be used to set up the preview of the app on the appetize.io. Appetize.io is an online simulator to run mobile apps ( IOS and Android). Appetize.io provides a nice virtual mobile frame to run native apps with various options like screen size, mobile, OS version, etc. Appetize.io provides some API to update/publish the app. In SUSI, we once uploaded the app on appetize.io and now we are using the API provided by them to update the APK everytime a commit is pushed in the repository.

API information (Derived from official docs of appetize.io):

You may upload a new version of an existing app, or update app settings.

Send an HTTP POST request to

https://[email protected]/v1/apps/PUBLICKEY

Replace APITOKEN with your API token and PUBLICKEY with the public key of the app you’re updating. Your API token must be permissioned to the same account as was used to upload the app. The POST body must be a JSON object. To delete a previously set field, use a value of null.

Optional Fields

  1. url: (string) a publicly accessible link to your .zip, .tar.gz, or .apk file, used to upload a new version of your app.
  2. note: (string) a note for your own purposes, will appear on your management dashboard.

For the url parameter, we have used https://github.com/fossasia/susi_android/raw/apk/susi-debug.apk and note can be anything. We have used Update SUSI Preview.

curl https://[email protected]/v1/apps/mbpprq4xj92c119j7nxdhttjm0 -H 'Content-Type: application/json' -d '{"url":"https://github.com/fossasia/susi_android/raw/apk/susi-debug.apk", "note": "Update SUSI Preview"}'

Summary

This blog covered about how to implement an automatic structure to generate APKs for testing and using that APK to build a preview on websites like appetize.io and then using the APIs provided by them to update the APK after each PR merge in the repo. Check out the resources below to learn more about the topic. So, if you are thinking of contributing to SUSI Android App, this may help you a little in testing the app. But if not, then you can also use the similar technique for your android app as well and ease the life of testers.

Resources

  1. Docs of appetize.io to learn more about the API https://appetize.io/docs
  2. Tutorial on using curl to make API requests https://curl.haxx.se/docs/httpscripting.html
  3. Tutorial on writing basic shell scripts https://ryanstutorials.net/bash-scripting-tutorial/

Implementing Skill Detail Section in SUSI Android App

SUSI Skills are rules that are defined in SUSI Skill Data repo which are basically the responses SUSI gives to the user queries. When a user queries something from the SUSI Android app, a query to SUSI Server is made which further fetches response from SUSI Skill Data and gives the response to the app. Similarly, when we need to list all skills, an API call is made to server to list all skills. The server then checks the SUSI Skill Data repo for the skills and then return all the required information to the app. Then the app displays all the information about the skill to user. User then can view details of each skill and then interact on the chat interface to use that skill. This process is similar to what SUSI Skill CMS does. The CMS is a skill wiki like interface to view all skills and then edit them. Though the app can not be currently used to edit the skills but it can be used to view them and try them on the chat interface.

API Information

For listing SUSI Skill groups, we have to call on /cms/getGroups.json

This will give you all groups in SUSI model in which skills are present. Current response:

{
  "session": {"identity": {
    "type": "host",
    "name": "14.139.194.24",
    "anonymous": true
  }},
  "accepted": true,
  "groups": [
    "Small Talk",
    "Entertainment",
    "Problem Solving",
    "Knowledge",
    "Assistants",
    "Shopping"
  ],
  "message": "Success: Fetched group list"
}

So, the groups object gives all the groups in which SUSI Skills are located.

Next comes, fetching of skills. For that the endpoint is /cms/getGroups.json?group=GROUP_NAME

Since we want all skills to be fetched, we call this api for every group. So, for example we will be calling http://api.susi.ai/cms/getSkillList.json?group=Entertainment for getting all skills in group “Entertainment”. Similarly for other groups as well.

Sample response of skill:

{
  "accepted": true,
  "model": "general",
  "group": "Shopping",
  "language": "en",
  "skills": {"amazon_shopping": {
    "image": "images/amazon_shopping.png",
    "author_url": "https://github.com/meriki",
    "examples": ["Buy a dress"],
    "developer_privacy_policy": null,
    "author": "Y S Ramya",
    "skill_name": "Shop At Amazon",
    "dynamic_content": true,
    "terms_of_use": null,
    "descriptions": "Searches items on Amazon.com for shopping",
    "skill_rating": null
  }},
  "message": "Success: Fetched skill list",
  "session": {"identity": {
    "type": "host",
    "name": "14.139.194.24",
    "anonymous": true
  }}
}

It gives all details about skills:

  1. image
  2. author_url
  3. examples
  4. developer_privacy_policy
  5. author
  6. skill_name
  7. dynamic_content
  8. terms_of_use
  9. descriptions
  10. skill_rating

Implementation in SUSI Android App

Skill Detail Section UI of Google Assistant

Skill Detail Section UI of SUSI SKill CMS

Skill Detail Section UI of SUSI Android App

The UI of skill detail section in SUSI Android App is the mixture of UI of Skill detail section in Google Assistant ap and SUSI Skill CMS. It displays details of skills in a beautiful manner with horizontal recyclerview used to display the examples.

So, we have to display following details about the skill in Skill Detail Section:

  1. Skill Name
  2. Author Name
  3. Skill Image
  4. Try it Button
  5. Description
  6. Examples
  7. Rating
  8. Content type (Dynamic/Static)
  9. Terms of Use
  10. Developer’s Privacy policy

Let’s see the implementation.

1. Whenever a skill Card View is clicked, showSkillDetailFragment() is called and it opens a new instance of a fragment named SkillDetailsFragment which shows details of the skill. We have to provide necessary information while starting the fragment. This information is passed as a Serializable.

fun showSkillDetailFragment(skillData: SkillData, skillGroup: String) {
   val skillDetailsFragment = SkillDetailsFragment.newInstance(skillData,skillGroup)
   (context as SkillsActivity).fragmentManager.beginTransaction()
           .replace(R.id.fragment_container, skillDetailsFragment)
           .commit()
}

2.  The data which was passed as a Serializeable object is now casted back to the required form and a method to set up the UI is called.

companion object {
   val SKILL_KEY = "skill_key"
   val SKILL_GROUP = "skill_group"
   fun newInstance(skillData: SkillData, skillGroup: String): SkillDetailsFragment {
       val fragment = SkillDetailsFragment()
       val bundle = Bundle()
       bundle.putSerializable(SKILL_KEY, skillData as Serializable)
       bundle.putString(SKILL_GROUP, skillGroup)
       fragment.arguments = bundle

       return fragment
   }
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
   skillData = arguments.getSerializable(
           SKILL_KEY) as SkillData
   skillGroup = arguments.getString(SKILL_GROUP)
   return inflater.inflate(R.layout.fragment_skill_details, container, false)
}

override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
   setupUI()
   super.onViewCreated(view, savedInstanceState)
}

3. The setupUI() method then calls separate method for setting every part of the UI like image, name etc.

fun setupUI() {
   setImage()
   setName()
   setAuthor()
   setTryButton()
   setDescription()
   setExamples()
   setRating()
   setDynamicContent()
   setPolicy()
   setTerms()
}

4. One example of setting a part of the UI is setting Author name. It checks if AuthorName is null or not. After that it anchors author’s github account link with his/her name.

fun setAuthor() {
   skill_detail_author.text = "Author : ${activity.getString(R.string.no_skill_author)}"
   if(skillData.author != null && !skillData.author.isEmpty()){
       if(skillData.authorUrl == null || skillData.authorUrl.isEmpty())
           skill_detail_author.text = "Author : ${skillData.skillName}"
       else {
           skill_detail_author.linksClickable = true
           skill_detail_author.movementMethod = LinkMovementMethod.getInstance()
           if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
               skill_detail_author.text = Html.fromHtml("Author : <a href=\"${skillData.authorUrl}\">${skillData.author}</a>", Html.FROM_HTML_MODE_COMPACT)
           } else {
               skill_detail_author.text = Html.fromHtml("Author : <a href=\"${skillData.authorUrl}\">${skillData.author}</a>")
           }
       }
   }
}

Summary

So, this blog talked about how the Skill detail section in SUSI Android App is implemented. This included how a network call is made, logic for making different network calls, making a horizontal recyclerview for displaying examples. So, If you are looking forward to contribute to SUSI Android App, this can help you a little. But if not so, this may also help you in understanding and how you can implement horizontal recyclerview similar to Google Play Store.

References

  1. To know about servlets https://en.wikipedia.org/wiki/Java_servlet
  2. To see how to implement one https://www.javatpoint.com/servlet-tutorial
  3. To see how to make network calls in android using Retrofit https://guides.codepath.com/android/Consuming-APIs-with-Retrofit
  4. To see how to implement custom RecyclerView Adapter https://www.survivingwithandroid.com/2016/09/android-recyclerview-tutorial.html

Implementing Change Password Feature in SUSI Android App using Custom Dialogs

Recently a new servlet was implemented on the SUSI Server about changing the password of the logged in user. This feature comes in handy to avoid unauthorized usage of the SUSI Account. Almost all the online platforms have this feature to change the password to avoid notorious user to unethical use someone else’s account. In SUSI Android app this new API was used with a nice UI to change the password of the user. The process is very simple and easy to grasp. This blog will try to cover the API information and implementation of the Change Password feature in the android client.

API Information

For changing the password of SUSI Account of the user, we have to call on  /aaa/changepassword.json

We have to provide three parameters along with this api call:

  1. changepassword:  Email of user (type string) using which user is logged in.
  2. password:  Old password (type string with min length of 6) of the user.
  3. newpassword: New password (type string with min length of 6) of the user.
  4. access_token: An encrypted access_token indicating user is logged in.

Sample Response (Success)

{
  "session": {"identity": {
    "type": "email",
    "name": "YOUR_EMAIL_ADDRESS",
    "anonymous": false
  }},
  "accepted": true,
  "message": "Your password has been changed!"
}

Error Response (Failure). This happens when user is not logged in:

HTTP ERROR 401
Problem accessing /aaa/changepassword.json. Reason:
   Base user role not sufficient. Your base user role is 'ANONYMOUS', your user role is 'anonymous'

Implementation in SUSI Android App

The change password option is located in Settings Activity and displayed only when user is logged in. So, if a logged in user wants to change the password of his/her SUSI AI account, he/she can simply go to the Settings and click on the option. Clicking on the options open up a dialog box with 3 input layouts for:

  1. Current Password
  2. New Password
  3. Confirm New Password

So, user can simply add these three inputs and click “Ok”. This will change the password of their account. Let’s see some code explanation.

  1. When user clicks on the “reset password” option from the settings, the showResetPasswordAlert() method is called which displays the dialog. And when user clicks on the “OK” button the resetPassword method() in the presenter is called passing input from the three input layout as parameters.

settingsPresenter.resetPassword(password.editText?.text.toString(), newPassword.editText?.text.toString(), conPassword.editText?.text.toString())

fun showResetPasswordAlert() {
   val builder = AlertDialog.Builder(activity)
   val resetPasswordView = activity.layoutInflater.inflate(R.layout.alert_reset_password, null)
   password = resetPasswordView.findViewById(R.id.password) as TextInputLayout
   newPassword = resetPasswordView.findViewById(R.id.newpassword) as TextInputLayout
   conPassword = resetPasswordView.findViewById(R.id.confirmpassword) as TextInputLayout
   builder.setView(resetPasswordView)
   builder.setTitle(Constant.CHANGE_PASSWORD)
           .setCancelable(false)
           .setNegativeButton(Constant.CANCEL, null)
           .setPositiveButton(getString(R.string.ok), null)
   resetPasswordAlert = builder.create()
   resetPasswordAlert.show()
   setupPasswordWatcher()
   resetPasswordAlert.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener {
       settingsPresenter.resetPassword(password.editText?.text.toString(), newPassword.editText?.text.toString(), conPassword.editText?.text.toString())
   }
}
  1. In the resetPassword method, all details about the passwords are checked like:
  1. If passwords are not empty.
  2. If passwords’ lengths are greater than 6.
  3. If new password and confirmation new password matches

   

When all the conditions are satisfied and all the inputs are valid, resetPassword() in model is called which makes network call to change password of the user.

settingModel.resetPassword(password,newPassword,this)

override fun resetPassword(password: String, newPassword: String, conPassword: String) {
   if (password.isEmpty()) {
       settingView?.invalidCredentials(true, Constant.PASSWORD)
       return
   }
   if (newPassword.isEmpty()) {
       settingView?.invalidCredentials(true, Constant.NEW_PASSWORD)
       return
   }
   if (conPassword.isEmpty()) {
       settingView?.invalidCredentials(true, Constant.CONFIRM_PASSWORD)
       return
   }

   if (!CredentialHelper.isPasswordValid(newPassword)) {
       settingView?.passwordInvalid(Constant.NEW_PASSWORD)
       return
   }

   if (newPassword != conPassword) {
       settingView?.invalidCredentials(false, Constant.NEW_PASSWORD)
       return
   }
   settingModel.resetPassword(password,newPassword,this)
}

Summary

So, this blog talked about how the Change Password feature is implemented in SUSI Android App. This included how a network call is made, logic for making network, information about API, making dialogs with custom UI, etc. So, If you are looking forward to contribute to SUSI Android App, this can help you a little. But if not so, this may also help you in understanding and how you can implement a dialog box with custom UI.

References

  1. To know about servlets https://en.wikipedia.org/wiki/Java_servlet
  2. To see how to implement one https://www.javatpoint.com/servlet-tutorial
  3. To see how to make network calls in android using Retrofit https://guides.codepath.com/android/Consuming-APIs-with-Retrofit
  4. Official docs for displaying dialog https://developer.android.com/guide/topics/ui/dialogs.html
  5. Implementing dialog boxes with custom UI https://stackoverflow.com/questions/13341560/how-to-create-a-custom-dialog-box-in-android
  6. Pull Request for API reference: https://github.com/fossasia/susi_server/pull/352

Implementing Skill Listing in SUSI Android App using Nested RecyclerViews

SUSI Skills are rules that are defined in SUSI Skill Data repo which are basically the responses SUSI gives to the user queries. When a user queries something from the SUSI Android app, a query to SUSI Server is made which further fetches response from SUSI Skill Data and gives the response to the app. Similarly, when we need to list all skills, an API call is made to server to list all skills. The server then checks the SUSI Skill Data repo for the skills and then return all the required information to the app. Then the app displays all the information about the skill to user. User then can view details of each skill and then interact on the chat interface to use that skill. This process is similar to what SUSI Skill CMS does. The CMS is a skill wiki like interface to view all skills and then edit them. Though the app can not be currently used to edit the skills but it can be used to view them and try them on the chat interface.

API Information

For listing SUSI Skill groups, we have to call on  /cms/getGroups.json

This will give you all groups in SUSI model in which skills are present. Current response:

{
  "session": {"identity": {
    "type": "host",
    "name": "14.139.194.24",
    "anonymous": true
  }},
  "accepted": true,
  "groups": [
    "Small Talk",
    "Entertainment",
    "Problem Solving",
    "Knowledge",
    "Assistants",
    "Shopping"
  ],
  "message": "Success: Fetched group list"
}

So, the groups object gives all the groups in which SUSI Skills are located.

Next comes, fetching of skills. For that the endpoint is /cms/getGroups.json?group=GROUP_NAME

Since we want all skills to be fetched, we call this api for every group. So, for example we will be calling http://api.susi.ai/cms/getSkillList.json?group=Entertainment for getting all skills in group “Entertainment”. Similarly for other groups as well.

Sample response of skill:

{
  "accepted": true,
  "model": "general",
  "group": "Shopping",
  "language": "en",
  "skills": {"amazon_shopping": {
    "image": "images/amazon_shopping.png",
    "author_url": "https://github.com/meriki",
    "examples": ["Buy a dress"],
    "developer_privacy_policy": null,
    "author": "Y S Ramya",
    "skill_name": "Shop At Amazon",
    "dynamic_content": true,
    "terms_of_use": null,
    "descriptions": "Searches items on Amazon.com for shopping",
    "skill_rating": null
  }},
  "message": "Success: Fetched skill list",
  "session": {"identity": {
    "type": "host",
    "name": "14.139.194.24",
    "anonymous": true
  }}
}

It gives all details about skills:

  1. image
  2. author_url
  3. examples
  4. developer_privacy_policy
  5. author
  6. skill_name
  7. dynamic_content
  8. terms_of_use
  9. descriptions
  10. skill_rating

Implementation in SUSI Android App

Skill Listing UI of Google Assistant

Skill Listing UI of SUSI SKill CMS

Skill Listing UI of SUSI Android App

The UI of skill listing in SUSI Android App is the mixture of UI of Skill listing in Google Assistant ap and SUSI Skill CMS. It displays skills in a beautiful manner with horizontal recyclerview nested in vertical recyclerview.

So, for implementing horizontal recyclerview inside vertical recyclerview, you need two viewholders and two adapters (one each for a recyclerview).

Let’s see the implementation.

1. First task is to fetch the information of groups in which skills are located. This line calls method in SkillListModel which then makes an API call to fetch groups.

skillListingModel.fetchGroups(this)

2. When the API call is succeeded, the below mentioned method is called which then calls a  skillListingModel.fetchSkills(groups[0], this) which fetches the skills located in group[0] group.

override fun onGroupFetchSuccess(response: Response<ListGroupsResponse>) {
   if (response.isSuccessful && response.body() != null) {
       groupsCount = response.body().groups.size
       groups = response.body().groups
       skillListingModel.fetchSkills(groups[0], this)
   } else {
       skillListingView?.visibilityProgressBar(false)
       skillListingView?.displayErrorDialog()
   }
}

3. When API call for fetching skills in group[0] succeeds, the count value is increased and then skills in group[1] are fetched and so on.

override fun onSkillFetchSuccess(response: Response<ListSkillsResponse>, group: String) {
   if (response.isSuccessful && response.body() != null) {
       skills.add(Pair(group, response.body().skillMap))
       count++
       if(count == groupsCount) {
           skillListingView?.visibilityProgressBar(false)
           skillListingView?.updateAdapter(skills)
       } else {
           skillListingModel.fetchSkills(groups[count], this)
       }
   } else {
       skillListingView?.visibilityProgressBar(false)
       skillListingView?.displayErrorDialog()
   }
}

4. When skills in all groups are fetched, the data in adapter is updated using skillGroupAdapter.notifyDataSetChanged()

override fun updateAdapter(skills: ArrayList<Pair<String, Map<String, SkillData>>>) {
   this.skills.clear()
   this.skills.addAll(skills)
   skillGroupAdapter.notifyDataSetChanged()
}

5. The data is set to the layout in two adapters made earlier. The following is the code to set the group name and adapter to horizontal recyclerview. This is the GroupAdapter to set data to row item in vertical recyclerview.

override fun onBindViewHolder(holder: GroupViewHolder?, position: Int) {
   if(skills[position].first != null)
       holder?.groupName?.text = skills[position].first
   holder?.skillList?.setHasFixedSize(true)
   val mLayoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
   holder?.skillList?.layoutManager = mLayoutManager
   holder?.skillList?.adapter = SkillListAdapter(context, skills[position])
}

6. Similarly, the data of each individual element in the horizontal recyclerview is set in the skillAdapter. The data set are title, examples, description and image. We have used Picasso library to load images from the URL.

override fun onBindViewHolder(holder: SkillViewHolder?, position: Int) {
   val skillData = skillDetails.second.values.toTypedArray()[position]

   if(skillData.skillName == null || skillData.skillName.isEmpty()){
       holder?.skillPreviewTitle?.text = context.getString(R.string.no_skill_name)
   } else {
       holder?.skillPreviewTitle?.text = skillData.skillName
   }

   if( skillData.descriptions == null || skillData.descriptions.isEmpty()){
       holder?.skillPreviewDescription?.text = context.getString(R.string.no_skill_description)
   } else {
       holder?.skillPreviewDescription?.text = skillData.descriptions
   }

   if(skillData.examples == null || skillData.examples.isEmpty())
       holder?.skillPreviewExample?.text = StringBuilder("\"").append("\"")
   else
       holder?.skillPreviewExample?.text = StringBuilder("\"").append(skillData.examples[0]).append("\"")

   if(skillData.image == null || skillData.image.isEmpty()){
       holder?.previewImageView?.setImageResource(R.drawable.ic_susi)
   } else {
       Picasso.with(context.applicationContext).load(StringBuilder(imageLink)
               .append(skillDetails.first.replace(" ","%20")).append("/en/").append(skillData.image).toString())
               .fit().centerCrop()
               .into(holder?.previewImageView)
   }
}

Summary

So, this blog talked about how the Skill Listing feature in SUSI Android App is implemented. This included how a network call is made, logic for making different network calls, making a nested horizontal recyclerview inside vertical recyclerview, etc. So, If you are looking forward to contribute to SUSI Android App, this can help you a little. But if not so, this may also help you in understanding and how you can implement nested recyclerviews similar to Google Play Store.

References

  1. To know about servlets https://en.wikipedia.org/wiki/Java_servlet
  2. To see how to implement one https://www.javatpoint.com/servlet-tutorial
  3. To see how to make network calls in android using Retrofit https://guides.codepath.com/android/Consuming-APIs-with-Retrofit
  4. To see how to implement Horizontal recyclerView inside Vertical recyclerView http://android-pratap.blogspot.in/2015/12/horizontal-recyclerview-in-vertical.html
  5. To see how to implement custom RecyclerView Adapter https://www.survivingwithandroid.com/2016/09/android-recyclerview-tutorial.html

Implementing Feedback Feature in SUSI Android App

Recently, on SUSI Server, a new servlet was added which is used to rate SUSI Skills either positive or negative. The server stores the rating of a particular skill in a JSON file. These ratings help in improving answers provided by SUSI. So, the server part is done and it was required to implement this in the SUSI Android App. In this blog, I will cover the topic of implementation of the Rating or Feedback feature in SUSI Android App. This will including all the cases when feedback should be sent, when it should not be sent, when to send positive feedback, when to send negative feedback, etc.

API Information

For rating a SUSI Skill, we have to call on  /cms/rateSkill.json providing 5 parameters which are:

  1. model: The model of SUSI Skill. (String)
  2. group: The Group under the model in which that particular skill resides. (String)
  3. language: The language of skill. (String)
  4. skill: This is skill name. (String)
  5. rating: This can be two strings, either “positive” or “negative”. String)

Basically, in the SUSI Skill Data repo (in which all the skills are stored), models, groups language etc are part of folder structure.

So, if a skill is located here

https://github.com/fossasia/susi_skill_data/blob/master/models/general/Knowledge/en/news.txt

This would mean

model = general

group = Knowledge

language = en

skill = news

rating = positive/negative

Implementation in SUSI Android App

    

So, when the like button on a particular skill is clicked, a positive call is made and when the dislike button is clicked, a negative call is made.

Let’s see example when the thumbs up button or like button is clicked.

There can be three cases possible:

  1. None of Like button or dislike button is clicked already: In this case, initially, both like and dislike button will be transparent/hollow. So, when like button is clicked, the like button will be colored blue and a call will be made with positive feedback.
  2. Like button is already clicked: In this case, like button is already clicked. So, it will already be blue. So, when user clicks again on positive button, it should get back to normal/hollow indicating rating which was sent is cancelled and a a call will be made with negative feedback thus cancelling or neutralizing the earlier, positive feedback.
  3. Dislike button is already clicked: In this case, the dislike button is already blue, indicating a negative call is already made. So, now when the like button is clicked, we need to cancel the earlier negative feedback call and sending another negative feedback call. Thus, sending two negative feedback calls. And after that coloring dislike button as blue.

Look at the code below. It is self explanatory. There are three if-else conditions covering all the above mentioned three cases.

thumbsUp.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View view) {
       thumbsUp.setImageResource(R.drawable.thumbs_up_solid);
       if(!model.isPositiveRated() && !model.isNegativeRated()) {
           rateSusiSkill(Constant.POSITIVE, model.getSkillLocation(), context);
           setRating(true, true);
       } else if(!model.isPositiveRated() && model.isNegativeRated()) {
           setRating(false, false);
           thumbsDown.setImageResource(R.drawable.thumbs_down_outline);
           rateSusiSkill(Constant.POSITIVE, model.getSkillLocation(), context);
           sleep(500);
           rateSusiSkill(Constant.POSITIVE, model.getSkillLocation(), context);
           setRating(true, true);
       } else if (model.isPositiveRated() && !model.isNegativeRated()) {
           rateSusiSkill(Constant.NEGATIVE, model.getSkillLocation(), context);
           setRating(false, true);
           thumbsUp.setImageResource(R.drawable.thumbs_up_outline);
       }
   }
});

Similarly for when dislike button is clicked, the above three mentioned cases still hold resulting in this code snippet.

thumbsDown.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View view) {
       thumbsDown.setImageResource(R.drawable.thumbs_down_solid);
       if(!model.isPositiveRated() && !model.isNegativeRated()) {
           rateSusiSkill(Constant.NEGATIVE, model.getSkillLocation(), context);
           setRating(true, false);
       } else if(model.isPositiveRated() && !model.isNegativeRated()) {
           setRating(false, true);
           thumbsUp.setImageResource(R.drawable.thumbs_up_outline);
           rateSusiSkill(Constant.NEGATIVE, model.getSkillLocation(), context);
           sleep(500);
           rateSusiSkill(Constant.NEGATIVE, model.getSkillLocation(), context);
           setRating(true, false);
       } else if (!model.isPositiveRated() && model.isNegativeRated()) {
           rateSusiSkill(Constant.POSITIVE, model.getSkillLocation(), context);
           setRating(false, false);
           thumbsDown.setImageResource(R.drawable.thumbs_down_outline);
       }
   }
});

Summary

So, this blog talked about how the Feedback feature in SUSI Android App is implemented. This included how a network call is made, logic for sending positive/negative feedback, logic to withdraw feedback etc. So, If you are looking forward to contribute to SUSI Android App, this can help you a little. But if not so, this may also help you in understanding and how rating mechanism in social media websites like Facebook, Twitter, Quora, Reddit, etc work.

References

  1. To know about servlets https://en.wikipedia.org/wiki/Java_servlet
  2. To see how to implement one https://www.javatpoint.com/servlet-tutorial
  3. To see how to make network calls in android using Retrofit https://guides.codepath.com/android/Consuming-APIs-with-Retrofit
  4. To see how to implement click listeners on button https://developer.android.com/reference/android/view/View.OnClickListener.html

Introduction To Kotlin in SUSI Android App

Lately, we wrote some of the code of SUSI Android App in Kotlin. Kotlin is a very similar language to Java but with much more advantages than Java. It is easy to adapt and learn. There is no doubt that Kotlin is better than Java but with the announcement of Kotlin Support in Google IO’17 for Android development, Kotlin seems a decent way to write code for an Android App.

Advantages of Kotlin over Java

    1. Reduce Boilerplate Code: It helps making development of app faster as it reduces more than 20 percent of boilerplate code. Writing long statements again and again is a headache for developers. Kotlin comes to rescue in that situation.
    2. Removes Null Pointer Exception: Once a large company faced millions of dollars of loss due to null pointer exception. It causes crashes of apps more often than anything else. Thus Kotlin helps in Null checks and makes app free from Null pointer Exceptions.
    3. Interoperable with Java: Kotlin code and Java code are interoperable. Which means you can write half your code in kotlin and half in Java and it will work like a charm. You can call java methods from Kotlin code and vice versa. So, you can simply move your existing Java based app to Kotlin slowly making your app always running.
    4. Lambda and Inline functions: Yes, Kotlin also has functionalities from functional programming languages. Mainly and most widely used feature of those languages is Lambda functions.
    5. Direct Reference of Views by Id: You do not need to write findViewById(R.id.view_name) or use any other library like Butterknife for view binding. You can simply use the view by its id.
    6. No semicolon:  Last but not the least, you do not need to add a semicolon after each statement. In fact, you do not need to add semicolon at all.

Setting up Android Studio to work with Kotlin

If you have latest Android Studio Canary Version, there is already a build support for Kotlin in it. You need not do anything in that case. But if you don’t have the Canary version, you can add Kotlin Plugin in your Android Studio. Follow the below steps to do that.

  1. Install the Kotlin Plugin:

Android Studio → Preferences… →Plugins → Browse Repository → type “Kotlin” in search box → install

  1. Restart your Android Studio and Rebuild the project. Everything else is already set up in SUSI Android App but if you want to do it for your other apps, follow this link.

Implementation in SUSI Android App

So, I am not going to give unnecessary code but will point out specific things where Kotlin helped a lot to reduce unnecessary code and made the code compact.

1. Listeners:

Earlier with Java

Button signup = (Button) findViewById(R.id.sign_up);

signup.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
               startActivity(new Intent(LoginActivity.this, SignUpActivity.class));
            }
        });

Now, with Kotlin

fun signUp() {
   sign_up.setOnClickListener { startActivity(Intent([email protected]LoginActivity, SignUpActivity::class.java)) }
}

2. Models

With Java

public class MapData {

    private double latitude;
    private double longitude;
    private double zoom;

    public MapData(double latitude, double longitude, double zoom) {
        this.latitude = latitude;
        this.longitude = longitude;
        this.zoom = zoom;
    }

    public double getLatitude() {
        return latitude;
    }

    public void setLatitude(double latitude) {
        this.latitude = latitude;
    }

    public double getLongitude() {
        return longitude;
    }

    public void setLongitude(double longitude) {
        this.longitude = longitude;
    }

    public double getZoom() {
        return zoom;
    }

    public void setZoom(double zoom) {
        this.zoom = zoom;
    }
}

With Kotlin

class MapData (var latitude: Double, var longitude: Double, var zoom: Double) 

3. Constructor

With Java

public class LoginPresenter {
    private LoginActivity loginActivity;
    public LoginPresenter(loginActivity: LoginActivity){
        this.loginActivity = loginActivity;
    }
}

With Kotlin

class LoginPresenter(loginActivity: LoginActivity) {
}

Summary

So, this blog was to give you an idea about Kotlin programming language, it’s advantages over java and information how you can set it up on your Android Studio so as to help you a little in understanding the codebase of SUSI Android App a little more.

Resources

  1. Official Kotlin Guide for Syntax Reference and further learning  https://kotlinlang.org/docs/reference/
  2. Blog by Elye on Setting up Kotlin on Android Studio https://android.jlelse.eu/setup-kotlin-for-android-studio-1bffdf1362e8
  3. Youtube Video tutorial by Derek Banas on Kotlin https://www.youtube.com/watch?v=H_oGi8uuDpA

Auto Deployment of SUSI Server using Kubernetes on Google Cloud Platform

Recently, we auto deployed SUSI Server on Google Cloud Platform using Kubernetes and Docker Images after each commit in the GitHub repo with the help of Travis Continuous Integration. So, basically, whenever a new commit is added to the repo, during the Travis build, we build the docker image of the server and then use it to deploy the server on Google Cloud Platform. We use Kubernetes for deployment since it is very easy to scale up the Project when traffic on the server is increased and Docker because using it we can easily build docker images which then can be used to update the deployment. This schematic will make things more clear what exactly is the procedure.

Prerequisites

  1. You must be signed in to your Google Cloud Console and have enabled billing and must have credits left in your account.
  2. You must have a docker account and a repo in it. If you don’t have one, make it now.
  3. You should have enabled Travis on your repo and have a Travis.yml file in your repo.
  4. You must already have a project in Google Cloud Console. Make a new one if you don’t have.

Pre Deployment Steps

You will be needed to do some work on Google Cloud Platform before actually starting the auto deployment process. Those are:

  1. Creating a new Cluster.
  2. Adding and Formatting Persistence Disk
  3. Adding a Persistent Volume CLaim (PVC)
  4. Labeling a node as primary.

Check out this documentation on how to do that. It may help.

Implementation

Img src: https://cloud.google.com/solutions/continuous-delivery-with-travis-ci

1. The first step is simply to add this line in Travis.yml file and create an empty deploy.sh, file mentioned below.

after_success:
- bash kubernetes/travis/deploy.sh

Now we’ll be moving line by line and adding commands in the empty deploy.sh file that we created in the previous step.

2. Next step is to remove obsolete Google Cloud files and install Google Cloud SDK and kubectl command. Use following lines to do that.

echo ">>> Removing obsolete gcoud files"
sudo rm -f /usr/bin/git-credential-gcloud.sh
sudo rm -f /usr/bin/bq
sudo rm -f /usr/bin/gsutil
sudo rm -f /usr/bin/gcloud

echo ">>> Installing new files"
curl https://sdk.cloud.google.com | bash;
source ~/.bashrc
gcloud components install kubectl

3. In this step you will be needed to download a JSON file which contains your Google Cloud Credentials, then copy that file to your repo and encrypt it using Travis encryption keys. Follow https://youtu.be/7U4jjRw_AJk this video to see how to do that.

4. So, now you have added your encrypted credentials.json files in your repo and now you need to use those credentials to login into your google cloud account. So, use below lines to do that.

echo ">>> Decrypting credentials and authenticating gcloud account"
# Decrypt the credentials we added to the repo using the key we added with the Travis command line tool
openssl aes-256-cbc -K $encrypted_YOUR_key -iv $encrypted_YOUR_iv -in ./kubernetes/travis/Credentials.json.enc -out Credentials.json -d
gcloud auth activate-service-account --key-file Credentials.json
export GOOGLE_APPLICATION_CREDENTIALS=$(pwd)/Credentials.json
#add gcoud project id
gcloud config set project YOUR_PROJECT_ID
gcloud container clusters get-credentials YOUR_CONTAINER

The above lines of code first decrypt your credentials, then login into your account and set the project you already created earlier.

5. Now, we have logged into Google Cloud, we need to build docker image from a dockerfile. Follow official docker docs to see how to write a dockerfile. Here is an example of dockerfile. You will need to add “$DOCKER_USERNAME” and “$DOCKER_PASSWORD” as environment variables in Travis settings of your repo.

echo ">>> Building Docker image"
cd kubernetes/images

docker build --no-cache -t YOUR_DOCKER_USERNAME/YOUR_DOCKER_REPO:$TRAVIS_COMMIT .
docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
docker tag YOUR_DOCKER_USERNAME/YOUR_DOCKER_REPO:$TRAVIS_COMMIT YOUR_DOCKER_USERNAME/YOUR_DOCKER_REPO:latest

6. Now, just push the docker image created in previous step and update the deployment.

echo ">>> Pushing docker image"
docker push YOUR_DOCKER_USERNAME/YOUR_DOCKER_REPO

echo ">>> Updating deployment"
kubectl set image deployment/YOUR_CONTAINER_NAME --namespace=default YOUR_CONTAINER_NAME=YOUR_DOCKER_USERNAME/YOUR_DOCKER_REPO:$TRAVIS_COMMIT

Summary

This blog was about how we have configured travis build and auto deployed SUSI Server on Google Cloud Platform using Kubernetes and Docker. You can do the same with your server too or if you are looking to contribute to SUSI Server, this may help you a little in understanding the code of the repo.

Resources

  1. The documentation for setting up your project on Google CLoud Console before starting auto deployment https://github.com/fossasia/susi_server/blob/afb00cd9c421876f5d640ce87941e502aa52e004/docs/installation/installation_kubernetes_gcloud.md
  2. The documentation for encrypting your google cloud credentials and adding them to your repo https://cloud.google.com/solutions/continuous-delivery-with-travis-ci
  3. Docs for Docker to get you started with Docker https://docs.docker.com/
  4. Travis Documentation on how to secure your credentials https://docs.travis-ci.com/user/encryption-keys/
  5. Travis Documentation on how to add environment variables in your repo settings https://docs.travis-ci.com/user/environment-variables/

Encoding and Saving Images as Strings in Preferences in SUSI Android App

In this blog post, I’ll be telling about how to store images in preferences by encoding them into Strings and also how to retrieve them back. Many a times, you need to store an image in preferences for various purposes and then need to retrieve it back when required. In SUSI Android App, we need to store an image in preference to set the chat background. We just simply select image from gallery, convert image to a byte array, then do a Base 64 encoding to string, store it in preferences and later decode it and set the chat background.

Base64 Encoding-Decoding in Java

You may already know what Base 64 is but still here is a link to Wikipedia article explaining it. So, how to do a Base64 encoding-decoding in java? For that java has a class with all such methods already present. https://docs.oracle.com/javase/8/docs/api/java/util/Base64.html

According to the docs:

This class consists exclusively of static methods for obtaining encoders and decoders for the Base64 encoding scheme. The implementation of this class supports the following types of Base64 as specified in RFC 4648 and RFC 2045.

  • Basic
  • URL and Filename safe
  • MIME

So, you may just use Base64.encode to encode a byte array and Base64.decode to decode a byte array.

Implementation

1. Selecting image from gallery

    

Start Image Picker Intent and pick an image from gallery. After selecting you may also start a Crop Intent to crop image also. After selecting and cropping image, you will get a URI of the image.

override fun openImagePickerActivity() {
   val i = Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
   startActivityForResult(i, SELECT_PICTURE)
}
val thePic = data.extras.getParcelable<Bitmap>("data")
val encodedImage = ImageUtils.Companion.cropImage(thePic)
chatPresenter.cropPicture(encodedImage)

2. Getting image from the URI using inputstream

Next step is to get the image from file using URI from the previous step and InputStream class and store it in a BitMap variable.

val imageStream: InputStream = context.contentResolver.openInputStream(selectedImageUri)
   val selectedImage: Bitmap
   val filePathColumn = arrayOf(MediaStore.Images.Media.DATA)
   val cursor = context.contentResolver.query(getImageUrl(context.applicationContext, selectedImageUri), filePathColumn, null, null, null)
   cursor?.moveToFirst()
   selectedImage = BitmapFactory.decodeStream(imageStream)

3. Converting the bitmap to ByteArray

Now, just convert the Bitmap thus obtained to a ByteArray using below code.

val baos = ByteArrayOutputStream()
   selectedImage.compress(Bitmap.CompressFormat.JPEG, 100, baos)
   val b = baos.toByteArray()

4. Base64 encode the ByteArray and store in preference

Encode the the byte array obtained in last step to a String and store it in preferences.

 val encodedImage = Base64.encodeToString(b, Base64.DEFAULT)
//now you have a string. You can store it in preferences

5. Decoding the String to image

Now whenever you want, you can just decode the stored Base64 encoded string to a byte array and then from byte array to a bitmap and use wherever you want.

fun decodeImage(context: Context, previouslyChatImage: String): Drawable {
   val b = Base64.decode(previouslyChatImage, Base64.DEFAULT)
   val bitmap = BitmapFactory.decodeByteArray(b, 0, b.size)
   return BitmapDrawable(context.resources, bitmap)
}

Summary

So, the main aim of this blog was to give an idea about how can you store images in preferences. There is no way to store them directly. So, you have to convert them to String by encoding them in Base64 format and then decoding it to use it. You also have other ways to store images like storing it in database etc but this one is simpler and fast.

Resources

  1. Stackoverflow answer to “How to save image as String” https://stackoverflow.com/questions/31502566/save-image-as-string-with-sharedpreferences
  2. Other Stackoverflow answer about “Saving Images in preferences” https://stackoverflow.com/questions/18072448/how-to-save-image-in-shared-preference-in-android-shared-preference-issue-in-a
  3. Official docs of Base64 class https://docs.oracle.com/javase/8/docs/api/java/util/Base64.html
  4. Wikipedia link for learning about Base64 https://en.wikipedia.org/wiki/Base64
  5. Stackoverflow answer for “What is Base64 encoding used for?” https://stackoverflow.com/questions/201479/what-is-base-64-encoding-used-for

Implementation of Text to Speech alongside Hotword Detection in SUSI Android App

In this blog post, we’ll be learning about how to implement Text to speech. Now you may be wondering that what is so difficult in implementing text to speech. One can easily find many tutorials on that and can easily look at the official documentation of TTS but there’s a catch here. In this blog post I’ll be telling about how to implement Text to Speech alongside Hotword Detection.

Let me give you a rough idea about how hotword detection works in SUSI Android App. For more details, read my other blog here on Hotword Detection. So, there is a constantly running background recording thread which detects when hotword is detected. Now, you may be thinking why do we need to stop that thread for text to speech. Well there are 2 reasons to do that:

  1. Recording while playing causing problems with mic and may crash the app.
  2. Suppose we even implement that but what will happen if the answer contains word “susi” in it. Now, the hotword will be detected because the speech output contained word “susi” in it (which is our hotword).

So, to avoid these problems we had to come up a way to stop hotword detection only for that particular time when SUSI is giving speech output and resume it back immediately when speech output is finished.

Let’s see how we did that.

Implementation

Check out this video to see how this work in the app

https://youtu.be/V9N6K4SzpXw

Initiating the TTS engine

The first task is to initiate the Text to speech engine. This process takes some time. So, it is done in the starting of app in a new handler.

new Handler().post(new Runnable() {
   @Override
   public void run() {
       textToSpeech = new TextToSpeech(getApplicationContext(), new TextToSpeech.OnInitListener() {
           @Override
           public void onInit(int status) {
               if (status != TextToSpeech.ERROR) {
                   Locale locale = textToSpeech.getLanguage();
                   textToSpeech.setLanguage(locale);
               }
           }
       });
   }
});

Check Audio Focus

The next step is to check whether audio focus is granted. Suppose there is some music playing in the background, in that case we won’t be able to give voice output. So, we check audio focus using below code.

final AudioManager audiofocus = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
 int result = audiofocus.requestAudioFocus(afChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
//DO WORK HERE
}

Using OnAudioFocusChangeListener, we keep a track of when we have access to give speech output and when we don’t.

private AudioManager.OnAudioFocusChangeListener afChangeListener =
       new AudioManager.OnAudioFocusChangeListener() {
           public void onAudioFocusChange(int focusChange) {
               if (focusChange == AUDIOFOCUS_LOSS_TRANSIENT) {
                   textToSpeech.stop();
               } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
                   // Resume playback
               } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
                   textToSpeech.stop();
               }
           }
       };

Converting the given text to speech

Now we have audio focus, we just have to convert given text to speech. Use method textToSpeech.speak().

private void voiceReply(final String reply) {
       Handler handler = new Handler();
       handler.post(new Runnable() {
           @Override
           public void run() {
                   textToSpeech.speak(spokenReply, TextToSpeech.QUEUE_FLUSH, ttsParams);                  
               }
           }
       });
   }
}

Abandon Audio Focus

Now we are done with speech output, it’s time we abandon audio focus.

audiofocus.abandonAudioFocus(afChangeListener);

TTS alongside Hotword Detection

Okay so now the major part. How do we check when to stop hotword detection thread and when to resume it? How do we check if Speech output is finished?

Answer to these questions is textToSpeech.setOnUtteranceProgressListener. The UtteranceProgressListener overrides 3 methods:

  1. onStart: Indicates starting of text to speech conversion. Which means it’s time to stop hotword detection thread.
  2. onDone: Called when every word of the provided text is converted to speech. So, simply resume hotword detection
  3. onError: Called when there is an error and text is not converted to speech. Anyway, we need to resume hotword detection here too.
textToSpeech.setOnUtteranceProgressListener(new UtteranceProgressListener() {
                       @Override
                       public void onStart(String s) {
                           if(recordingThread !=null && isDetectionOn){
                               recordingThread.stopRecording();
                               isDetectionOn = false;
                           }
                       }

                       @Override
                       public void onDone(String s) {
                           if(recordingThread != null && !isDetectionOn && checkHotwordPref()) {
                               recordingThread.startRecording();
                               isDetectionOn = true;
                           }
                       }

                       @Override
                       public void onError(String s) {
                           if(recordingThread != null && !isDetectionOn && checkHotwordPref()) {
                               recordingThread.startRecording();
                               isDetectionOn = true;
                           }
                       }
                   });

                   HashMap<String,String> ttsParams = new HashMap<String, String>();
                   ttsParams.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID,
                           MainActivity.this.getPackageName());

Summary

So, the main thing required for implementation of Text to Speech alongside Hotword detection is a way to control stopping and resuming hotword detection when Text to speech is in process. For that we used UtteranceProgressListener of TextToSpeech class which makes it so easier to do the task we required. You may follow this same approach as well or if you have a better approach, open an issue here.

Resources

  1. Official Documentation of TextToSpeech https://developer.android.com/reference/android/speech/tts/TextToSpeech.html
  2. Documentation of UtteranceProgressListener https://developer.android.com/reference/android/speech/tts/UtteranceProgressListener.html
  3. Blog link to Hotword Detection https://docs.google.com/document/d/1auTyuk32i15Rw94TOkrSruRJ9LZVtjcThoWVJkvnAz8/edit?usp=sharing

Search Functionalities in SUSI Android App Using Android SearchView Widget

Searching is a common feature that is required in most applications. But the problem in implementing searching functionality is that there is no common way to do that. People fight over whose way is best to implement search functionality. In this blog post we’ll be looking at how search functionality works in SUSI Android App and how is it implemented. We have used Android’s SearchView widget to do that. There are many other ways to do so but this one is best suited for our requirements. Let’s see how it works.

UI Components used for Searching

1. Search icon (magnifying glass icon)

In the action bar, you can see a small icon. Clicking on the icon initiates search.

2. Edit text

An Obvious requirement is an edit test to enter search query.

3. Up and Down arrow keys

Required to search through the whole app. Simply use the up and down arrow keys to navigate through the app and find out each occurrence of the word you want to search.

 

 

 

 

 

 

 

4. Cross Button

Last but not the least, a close or cross button to close the search action.

Implementation

We have used Android’s inbuilt Widget SearchView. According to official android documentation

A widget that provides a user interface for the user to enter a search query and submit a request to a search provider. Shows a list of query suggestions or results, if available, and allows the user to pick a suggestion or result to launch into.

This widget makes searching a lot easier. It provides all methods and listeners which are actually required for searching. Let’s cover them one by one.

  1. Starting the search: searchView.setOnSearchClickListener Listener simply activates when a user clicks on search icon in the toolbar. Do all your work which needs to be done at the starting of the search like, hiding some other UI elements of doing an animation inside the listener
searchView.setOnSearchClickListener({
   chatPresenter.startSearch()
})
  1. Stop the Search: searchView.setOnCloseListener Listener gets activated when a user clicks on the cross icon to close the search. Add all the code snippet you want which is needed to be executed when the search is closed inside this like maybe notify the adapter about data set changes or closing the database etc.
searchView.setOnCloseListener({
   chatPresenter.stopSearch()
   false
})
  1.  Searching a query:  searchView.setOnQueryTextListener Listener overrides 2 methods:

3.1 onQueryTextSubmit: As the name suggests, this method is called when the query to be searched is submitted.

3.2 onQueryTextChange: This method is called when query you are writing changes.

We, basically wanted same thing to happen if user has submitted the query or if he is still typing and that is to take the query at that particular moment, find it in database and highlight it. So, chatPresenter.onSearchQuerySearched(query) this method is called in both onQueryTextSubmit and onQueryTextSubmit  to do that.

 searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
 
      override fun onQueryTextSubmit(query: String): Boolean {
           //Handle Search Query
           chatPresenter.onSearchQuerySearched(query)
           recyclerAdapter.query = query
           return false
       }

       override fun onQueryTextChange(newText: String): Boolean {
           if (TextUtils.isEmpty(newText)) {
               modifyMenu(false)
               recyclerAdapter.highlightMessagePosition = -1
               recyclerAdapter.notifyDataSetChanged()
               if (!editText.isFocused) {
                   editText.requestFocus()
               }
           } else {
               chatPresenter.onSearchQuerySearched(newText)
               recyclerAdapter.query = newText
           }
           return false
       }
   })
   return true
}
  1. Finding query in database: Now we have a query to be searched, we can just use a database operation to do that. The below code snippet finds all the messages which has the query present in it and work on it. If the query is not found, it simply displays a toast saying “Not found”
override fun onSearchQuerySearched(query: String) {
   chatView?.displaySearchElements(true)
   results = databaseRepository.getSearchResults(query)
   offset = 1
   if (results.size > 0) {
       chatView?.modifyMenu(true)
       chatView?.searchMovement(results[results.size - offset].id.toInt())
   } else {
       chatView?.showToast(utilModel.getString(R.string.not_found))
   }
}

This is the database operation.

override fun getSearchResults(query: String): RealmResults<ChatMessage> {
   return realm.where(ChatMessage::class.java).contains(Constant.CONTENT,
           query, Case.INSENSITIVE).findAll()
}

  1. Highlighting the part of message: Now, we know which message has the query, we just want to highlight it with a bright color to display the result. For that, we used SpannableString to highlight a part of complete string.
String text = chatTextView.getText().toString();
SpannableString modify = new SpannableString(text);
Pattern pattern = Pattern.compile(query, Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(modify);
while (matcher.find()) {
   int startIndex = matcher.start();
   int endIndex = matcher.end();
   modify.setSpan(new BackgroundColorSpan(Color.parseColor("#ffff00")), startIndex, endIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
chatTextView.setText(modify);

Summary

The whole point of this blog post was to educate about SearchView widget of android and how it makes it easy to search queries. All the methods you need are already implemented. You just need to call them and add database operation.

Resources

  1. The link to official android documentation explaining about different methods in SearchView Class https://developer.android.com/reference/android/widget/SearchView.html
  2. Another tutorial about SearchView http://www.journaldev.com/12478/android-searchview-example-tutorial