Show skills image in Circular Image View in SUSI.AI Android app

Each SUSI.AI skill has some data like skill name, skill image, skill rating and so on. Some of the skills image have a square appearance while others have a circular appearance and so on. This blog shows how to transform all images to circular image view while setting the skill image in the appropriate view holder in the skills card using Picasso.

Step – 1 : Create a new helper class called CircleTransform.java that implements the Transformation interface from Picasso.

Step -2 : Override the transform and key methods.

Step – 3 : Create a Bitmap and perform the following steps inside the transform() method, as mentioned in the code below :

@Override
public Bitmap transform(Bitmap source) {
   int size = Math.min(source.getWidth(), source.getHeight());
   int x = (source.getWidth() - size) / 2;
   int y = (source.getHeight() - size) / 2;

   Bitmap squaredBitmap = Bitmap.createBitmap(source, x, y, size, size);
   if (!squaredBitmap.equals(source)) {
       source.recycle();
   }

   Bitmap bitmap = Bitmap.createBitmap(size, size, source.getConfig());

   Canvas canvas = new Canvas(bitmap);
   Paint paint = new Paint();
   BitmapShader shader = new BitmapShader(squaredBitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP);
   paint.setShader(shader);
   paint.setAntiAlias(true);

   float radius = size / 2f;
   canvas.drawCircle(radius, radius, radius, paint);

   squaredBitmap.recycle();
   return bitmap;
}

 

This method returns a bitmap that we shall use to add to the appropriate view holder.

Step – 4 : Also return a string called “circle” from the key() method.

@Override
public String key() {
   return "circle";
}

 

Step – 5 : Now, add this transformation to the code, where the skill image is set into the appropriate view holder using Picasso.

fun setSkillsImage(skillData: SkillData, imageView: ImageView) {
   Picasso.with(imageView.context)
           .load(getImageLink(skillData))
           .error(R.drawable.ic_susi)
           .transform(CircleTransform())
           .fit()
           .centerCrop()
           .into(imageView)
}

 

Now, all skill images will be circular, as can be seen in the following screenshot :

 .     

The first image shows the skills image before applying CircleTransform while the second image shows the same after applying it.

Resources

Continue Reading

Using RealmRecyclerView Adapter to show list of recorded sensor data from Realm Database

In previous blog Storing Recorded Sensor Data in Realm Database we have stored the data fetched from sensors into the Realm Database by defining model classes.

In this blog, we will use the data stored in the Realm to display a list of recorded experiments in the form of well defining card view items so that it is easier for the user to understand.

For showing the list we will make use of RecyclerView  widget provided by Android which is a more advanced version of the List view and is used to display large data sets in a vertical list, horizontal list, grid, staggered grid etc.

RecyclerView  works in accordance with RecyclerView Adapter which is core engine that is responsible of inflating the layout of list items, populating the items with data, recycling of list item views when they go out of viewing screen and much more.

For this blog, we are going to use a special RecyclerView Adapter provided by Realm itself because it integrates properly with the Realm Database and handles modifying, addition, deletion or updating of Realm data automatically and efficiently.   

Step 1 Adding the dependencies

As always first we need to add the following code in our build.gradle file to add the dependency of Realm database and RealmRecyclerViewAdapter.

dependencies {
   implementation"com.android.support:recyclerview-v7:27.1.1 "
   implementation 'io.realm:android-adapters:2.1.1'
}

Step 2 Adding RecyclerView widget in our Activity layout file

First, we need to create an activity and name it as “DataLoggerActivity”, inside the layout of the Activity add the <RecyclerView> widget. This RecyclerView will act as a container of our list item.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context=".activity.DataLoggerActivity">

    <android.support.v7.widget.RecyclerView
        android:layout_below="@id/top_app_bar_layout"
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</RelativeLayout>

Step 3 Creating the layout and View holder for the list item

We have to create the layout of the list item which will be inflated by the Adapter. So for this create an XML file in res folder and name it “data_list_item.xml”. For the list of the experiments, we want to show Name of the experiment, recording time, recording date for every list item. For this we will make use of <CardView> and <TextView>. This gist shows the code of xml file.

The layout of the list item created is shown in Figure 2

Figure 1 Layout of list item showing mock information

Now we need to create a view holder for this layout which we need to pass to the Adapter, the following code shows the implementation of View Holder for above list item layout.

public class ViewHolder extends RecyclerView.ViewHolder {
   private TextView sensor, dateTime;
   ImageView deleteIcon;
   private CardView cardView;

   public ViewHolder(View itemView) {
       super(itemView);
       dateTime = itemView.findViewById(R.id.date_time);
       sensor = itemView.findViewById(R.id.sensor_name);
       deleteIcon = itemView.findViewById(R.id.delete_item);
       cardView = itemView.findViewById(R.id.data_item_card);
   }
}

Step 4 Creating the adapter for RecyclerView  

In this step, we will start by creating a class called “SensorLoggedListAdpater” and for using use the RecyclerView adapter provided by Realm we need to make this class extend the RealmRecyclerViewAdpater class.

But for that we need to pass two generic parameter:

  1. Model Class : This is class which define a Realm model, for this, we will pass a reference of “SensorLogged.class” which is defined in the previous blog as we want to show the list experiments which are stored using “SensorLogged” model class.
  2. ViewHolder : For this, we will pass the ViewHolder that we have created in Step 3.

As every RecyclerView Adapter needs a arraylist which contains the list of object containing information which we have to populate on the list item, the RealmRecyclerViewAdpater needs data in form of RealmResult to operate on, so we will create a constructor and pass in the RealmResult list in the super() method which we need to provide when we initialize this adapter in our “DataLoggerActivity” class.

public SensorLoggerListAdapter(RealmResults<SensorLogged> list, Activity context) {
   super(list, true, true);
   this.context = context;
   realm = Realm.getDefaultInstance();
}

Now we need to override two methods provided by RealmRecyclerViewAdapter class that are:

  1. public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType): In which we will inflate the layout of list item “dta_list_tem.xml” which we have created in Step 3.
  2. public void onBindViewHolder(@NonNull final ViewHolder holder, int position): In which we will populate the list item view using references stored in the ViewHolder with the data which we have provided while initializing the adapter.
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
   View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.logger_data_item, parent, false);
   return new ViewHolder(itemView);
}

@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
   SensorLogged temp = getItem(position);
   holder.sensor.setText(temp.getSensor());
   Date date = new Date(temp.getDateTimeStart());
   holder.dateTime.setText(String.valueOf(sdf.format(date)));
}

Step 5 Initializing the Adapter in Data Logger Activity and connecting with RecyclerView

Now we head to our Data Logger Activity, here in OnCreate() method first we will create a object of RecyclerView, then we will initialize our adapter by passing the RealmResult<SensorLogged> list which we have queried from the Realm Database.

Then we will set the LinearLayoutManager and finally, we will connect the the Adapter with the RecyclerView.

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_data_logger);
   ButterKnife.bind(this);

   Realm realm = Realm.getDefaultInstance();

   RealmResults<SensorLogged> results;
   String title;
  
   results = realm.where(SensorLogged.class)
           .findAll()
           .sort("dateTimeStart", Sort.DESCENDING);

   SensorLoggerListAdapter adapter = new SensorLoggerListAdapter(results, this);
   LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);

   recyclerView.setLayoutManager(linearLayoutManager);

   recyclerView.setAdapter(adapter);
}

After following all the above steps we have finally a activity as shown in Figure 4.

Figure 2 showing a list of recorded experiments with the instrument
name and date time of an experiment

Thus we have successfully displayed a list of the experiments from the data stored in the Realm Database using RealmRecyclerViewAdapter.

Resources

  1. https://academy.realm.io/posts/android-realm-listview/ – Blog on creating a To-do list on Realm official website
  2. https://gist.github.com/Avjeet/2f350feeafff17ec855a39891d8c2d66  Gist of layout of list item used
Continue Reading

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

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

Implementation

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

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

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

 

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

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

 

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

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

 

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

Resources

 

Continue Reading

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

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

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

 

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

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

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

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

 

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

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

 

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

...

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

 

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

...

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

 

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

...

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

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

 

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

Resources

Continue Reading

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

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

It accepts one compulsory url parameter –

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

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

API Development

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

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

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

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

 

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

Here are the important code snippets –

  • Continuation of the first code snippet –

.
.
.
// check if id exists already

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

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

		return new ServiceResponse(result);
	}

 

  • Sample response of checkRegistration.json API endpoint –

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

 

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

Resources

Continue Reading

Fetch Five Star Skill Rating from getSkillList API in SUSI.AI Android

SUSI.AI had a thumbs up/down rating system till now, which has now been replaced by a five star skill rating system. Now, the user is allowed to rate the skill based on a five star rating system. The UI components include a rating bar and below the rating bar is a section that displays the skill rating statistics – total number of ratings, average rating and a graph showing the percentage of users who rated the skill with five stars, four stars and so on.

SUSI.AI Skills are rules that are defined in SUSI Skill Data repo which are basically the processed responses that SUSI returns to the user queries. When a user queries something from the SUSI Android app, a query to SUSI Server is made which in turn fetches data from SUSI Skill Data and returns a JSON response to the app. Similarly, to get skill ratings, a call to the ‘/cms/getSkillList.json’ API is made. In this API, the server checks the SUSI Skill Data repo for the skills and returns a JSON response consisting of all the required information like skill name, author name, description, ratings, etc. to the app. Then, this JSON response is parsed to extract individual fields to display the appropriate information in the skill details screen of the app.

API Information

The endpoint to fetch skills is ‘/cms/getSkillList.json’
The endpoints takes three parameters as input –

  • model – It tells the model to which the skill belongs. The default value is set to general.
  • group – It tells the group(category) to which the skill belongs. The default value is set to All.
  • language – It tells the language to which the skill belongs. The default value is set to en.

Since all skills have to be fetched, this API is called for every group individually. For instance, call “https://api.susi.ai/cms/getSkillList.json?group=Knowledge” to get all skills in group “Knowledge”. Similarly, call for other groups.

Here is a sample response of a skill named ‘Capital’ from the group Knowledge :

"capital": {
      "model": "general",
      "group": "Knowledge",
      "language": "en",
      "developer_privacy_policy": null,
      "descriptions": "A skill to tell user about capital of any country.",
      "image": "images/capital.png",
      "author": "chashmeet singh",
      "author_url": "https://github.com/chashmeetsingh",
      "skill_name": "Capital",
      "terms_of_use": null,
      "dynamic_content": true,
      "examples": ["What is the capital of India?"],
      "skill_rating": {
        "negative": "0",
        "positive": "4",
        "feedback_count" : 0,
        "stars": {
          "one_star": 0,
          "four_star": 1,
          "five_star": 0,
          "total_star": 1,
          "three_star": 0,
          "avg_star": 4,
          "two_star": 0
        }
      },
      "creationTime": "2018-03-17T17:11:59Z",
      "lastAccessTime": "2018-06-06T00:46:22Z",
      "lastModifiedTime": "2018-03-17T17:11:59Z"
    },


It consists of all details about the skill called ‘Capital’:

  1. Model (model)
  2. Group (group)
  3. Language (language)
  4. Developer Privacy Policy (developer_privacy_policy)
  5. Description (descriptions)
  6. Image (image)
  7. Author (author)
  8. Author URL (author_url)
  9. Skill name (skill_name)
  10. Terms of Use (terms_of_use)
  11. Content Type (dynamic_content)
  12. Examples (examples)
  13. Skill Rating (skill_rating)
  14. Creation Time (creationTime)
  15. Last Access Time (lastAccessTime)
  16. Last Modified Time (lastModifiedTime)

From among all this information, the information of interest for this blog is Skill Rating. This blog mainly deals with showing how to parse the JSON response to get the skill rating star values, so as to display the actual data in the skill rating graph.

A request to the getSkillList API is made for each group using the GET method.

@GET("/cms/getSkillList.json")
Call<ListSkillsResponse> fetchListSkills(@Query("group") String groups);

It returns a JSON response consisting of all the aforementioned information. Now, to parse the JSON response, do the following :

  1. Add a response for the response received as a result of API call. ListSkillsResponse contains two objects – group and skills.
    This blog is about getting the skill rating, so let us proceed with parsing the required response. The skills object contains the skill data that we need. Hence, next a SkillData class is created.

    class ListSkillsResponse {
       val group: String = "Knowledge"
       val skillMap: Map<String, SkillData> = HashMap()
    }
  2. Now, add the SkillData class. This class defines the response that we saw for ‘Capital’ skill above. It contains skill name, author, skill rating and so on.

    class SkillData : Serializable {
       var image: String = ""
       @SerializedName("author_url")
       @Expose
       var authorUrl: String = ""
       var examples: List<String> = ArrayList()
       @SerializedName("developer_privacy_policy")
       @Expose
       var developerPrivacyPolicy: String = ""
       var author: String = ""
       @SerializedName("skill_name")
       @Expose
       var skillName: String = ""
       @SerializedName("dynamic_content")
       @Expose
       var dynamicContent: Boolean? = null
       @SerializedName("terms_of_use")
       @Expose
       var termsOfUse: String = ""
       var descriptions: String = ""
       @SerializedName("skill_rating")
       @Expose
       var skillRating: SkillRating? = null
    }
    
  3. Now, add the SkillRating class. As what is required is the skill rating, narrowing down to the skill_rating object. The skill_rating object contains the actual rating for each skill i.e. the stars values. So, this files defines the response for the skill_rating object.

    class SkillRating : Serializable {
       var stars: Stars? = null
    }
    
  4. Further, add a Stars class. Ultimately, the values that are needed are the number of users who rated a skill at five stars, four stars and so on and also the total number of users and the average rating. Thus, this file contains the values inside the ‘stars’ object.

    class Stars : Serializable {
       @SerializedName("one_star")
       @Expose
       var oneStar: String? = null
       @SerializedName("two_star")
       @Expose
       var twoStar: String? = null
       @SerializedName("three_star")
       @Expose
       var threeStar: String? = null
       @SerializedName("four_star")
       @Expose
       var fourStar: String? = null
       @SerializedName("five_star")
       @Expose
       var fiveStar: String? = null
       @SerializedName("total_star")
       @Expose
       var totalStar: String? = null
       @SerializedName("avg_star")
       @Expose
       var averageStar: String? = null
    }
    

Now, the parsing is all done. It is time to use these values to plot the skill rating graph and complete the section displaying the five star skill rating.

To plot these values on the skill rating graph refer to the blog on plotting horizontal bar graph using MPAndroid Chart library. In step 5 of the linked blog, replace the second parameter to the BarEntry constructor by the actual values obtained by parsing.

Here is how we do it.

  • To get the total number of ratings
val  totalNumberofRatings: Int = skillData.skillRating?.stars?.totalStars

 

  • To get the average rating
val averageRating: Float = skillData.skillRating?.stars?.averageStars

 

  • To get number of users who rated the skill at five stars
val fiveStarUsers: Int = skillData.skillRating?.stars?.fiveStar

Similarly, get the number of users for fourStar, threeStar, twoStar and oneStar.

Note : If the totalNumberOfRatings equals to zero, then the skill is unrated. In this case, display a message informing the user that the skill is unrated instead of plotting the graph.

Now, as the graph shows the percentage of users who rated the skill at a particular number of stars, calculate the percentage of users corresponding to each rating, parse the result to Float and place it as the second parameter to the BarEntry constructor  as follows :

 entries.add(BarEntry(4f, (fiveStarUsers!!.toFloat() / totalUsers) * 100f)))

Similarly, replace the values for all five entries. Finally, add the total ratings and average rating section and display the detailed skill rating statistics for each skill, as in the following figure.

Resources

 

 

Continue Reading

Handling Android Runtime permissions in UI Tests in SUSI.AI Android

With the introduction of Marshmallow (API Level 23), in SUSI.AI it was needed to ensure that:

  • It was verified if we had the permission that was needed, when required
  • The user was requested to grant permission, when it deemed appropriate
  • The request (empty states or data feedback) was correctly handled within the UI to represent the outcome of being granted or denied the required permission

You might have written UI tests. What about instances where the app needs the user’s permissions, like allow the app to access contacts on the device, for the tests to run? Would the tests pass when the test is run on Android 6.0+ devices?
And, can Espresso be used to achieve this? Unfortunately, Espresso doesn’t have the ability to access components from outside of the application package. So, how to handle this?
There are two approaches to handle this :
1) Using the UI Automator
2) Using the GrantPermissionRule

Let us have a look at both of these approaches in detail :

Using UI Automator to Handle Runtime Permissions on Android for UI Tests :

UI Automator is a UI testing framework suitable for cross-app functional UI testing across system and installed apps. This framework requires Android 4.3 (API level 18) or higher.
The UI Automator testing framework provides a set of APIs to build UI tests that perform interactions on user apps and system apps. The UI Automator APIs allows you to perform operations such as opening the Settings menu or the app launcher in a test device. This testing framework is well-suited for writing black box-style automated tests, where the test code does not rely on internal implementation details of the target app.

The key features of this testing framework include the following :

  • A viewer to inspect layout hierarchy. For more information, see UI Automator Viewer.
  • An API to retrieve state information and perform operations on the target device. For more information, see Accessing device stateAPIs that support cross-app UI testing. For more information, see UI Automator APIs.
  • Unlike Espresso, UIAutomator can interact with system applications which means that you’ll be able to interact with the permissions dialog, if needed.

    So, how to do this? Well, if you want to grant a permission in a UI test then you need to find the corresponding UiObject that you wish to click on. In our case, the permissions dialog box is the UiObject. This object is a representation of a view – it is not bound to the view but contains information to locate the matching view at runtime, based on the properties of the UiSelector instance within it’s constructor. A UiSelector instance is an object that declares elements to be targeted by the UI test within the layout. You can set various properties such as a text value, class name or content-description, for this UiSelector instance.
    So, once you have your UiObject (the permissions dialog), you can determine which option you want to select and then use click( ) method to grant/deny permission access.

fun allowPermissionsIfNeeded() {
         iIf (BUILD.VERSION.SDK_INT >= 23){
                   var allowPermissions : UiObject = mDevice
                                       .findObject(UiSelector( ).text("ALLOW"))
                   if(allowPermissions.exists( )) {
                             try {
                                       allowPermissions.click( )
                             } catch (e : UiObjectNotFoundException) {
                                       Log.e(TAG, "There is no permission dialog to interact with", e)
                             }
                   } 
         }
}

Similarly, you can also handle “DENY” approach.

So, this is how you can use UI Automator to handle Android runtime permissions for UI testing. Now, let us have a look at the other and a newer approach :

Using GrantPermissionRule to Handle Runtime Permissions on Android for UI Tests :

GrantPermissionRule is used to grant runtime permissions to avoid the permission dialog from showing up and blocking the UI of the app. In this approach, permissions can only be requested for API level 23 (Android M) or higher. All you need to do is to add the following rule to your UI test :

@Rule 
public GrantPermissionRule mRuntimePermissionRule =     
           GrantPermissionRule.grant(android.Manifest
.permission.ACCESS_FINE_LOCATION);

ACCESS_FINE_LOCATION (in the above code) can be replaced by any other permission that your app requires.

This would be also be implemented in the SUSI.AI Android app for UI tests. Unit tests and UI tests form an integral part of a good software. Hence, you need to write quality tests for your projects to detect and fix bugs and flaws easily and conveniently.

Resources

 

Continue Reading

Upload Avatar for a User in SUSI.AI Server

In this blog post, we are going to discuss on how the feature to upload the avatar for a user was implemented on the SUSI.AI Server. The API endpoint by which a user can upload his/her avatar image is https://api.susi.ai/aaa/uploadAvatar.json.

  • The endpoint is of POST type.
  • It accepts two request parameters –
  • image – It contains the entire image file sent from the client
  • access_token – It is the access_token for the user

The minimalUserRole is set to USER for this API, as only logged-in users can use this API.

Going through the API development

  • The image and access_token parameters are first extracted via the req object, that is passed to the main function. The  parameters are then stored in variables.
  • There is a check if the access_token and image exists. It it doesn’t, an error is thrown.
  • This code snippet discusses the above two points –

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    Part imagePart = req.getPart("image");
    if (req.getParameter("access_token") != null) {
        if (imagePart == null) {
            result.put("accepted", false);
            result.put("message", "Image file not received");
        } else {.
        }
    else{
        result.put("message","Access token are not given");
        result.put("accepted",false);
        resp.setContentType("application/json");
        resp.setCharacterEncoding("UTF-8");
        resp.getWriter().write(result.toString());
    }
}

 

  • Then the input stream is extracted from the imagePart and stored. And post that the identity is checked if it is valid.
  • The input stream is converted into the Image type using the ImageIO.read method.
  • The image is eventually converted into a BufferedImage using a function, described below.

public static BufferedImage toBufferedImage(Image img)
{
    if (img instanceof BufferedImage)
        return (BufferedImage) img;

    // Create a buffered image with transparency
    BufferedImage bimage = new BufferedImage(img.getWidth(null),     
    img.getHeight(null), BufferedImage.TYPE_INT_ARGB);
    // Draw the image on to the buffered image
    Graphics2D bGr = bimage.createGraphics();
    bGr.drawImage(img, 0, 0, null);
    bGr.dispose();
    // Return the buffered image
    return bimage;
}

 

  • After that, the file path and name is set. The avatar for each user is stored in the /data/avatar_uploads/<uuid of the user>.jpg.
  • The avatar is written to the path using the ImageIO.write function. Once, the file is stored on the server, the success response is sent and the client side receives it.

Resources

Continue Reading

Displaying Avatar Image of Users using Gravatar on SUSI.AI

This blog discusses how the avatar of the user has been shown at different places in the UI like the app bar, feedback comments, etc using the Gravatar service on SUSI.AI. A Gravatar is a Globally Recognized Avatar. Your Gravatar is an image that follows you from site to site appearing beside your name when you do things like comment or post on a blog. So, the Gravatar service has been integrated in SUSI.AI, so that it helps identify the user via the avatar too.

Going through the implementation

  • The aim is to get an avatar of the user from the email id. For that purpose, Gravatar exposes a publicly available avatar of the user, which can be accessed via the following steps :
    • Creating the Hash of the email
    • Sending the image request
  • For creating the MD5 hash of the email, use the npm library md5. The function takes a string as input and returns the hash of the string.
  • Now, a URL is generated using this hash.
  • The URL format is https://www.gravatar.com/avatar/HASH, where ‘HASH’ is the hash of the email of the user. In case, the hash is invalid, Gravatar returns a default avatar image.
  • Also, append ‘.jpg’ to the URL to maintain image format consistency on the website. When, the generated URL is used in an <img> tag, it behaves like an image and an avatar is returned when the URL is requested by the browser.
  • It has been displayed on various instances in the UI like app bar , feedback comments section, etc. The implementation in the feedback section has been discussed below.
  • The CircleImage component has been used for displaying the avatar, which takes name as a required property and src as the link of the image, if present. Following function returns props to the CircleImage component.

import md5 from 'md5';
import { urls } from './';

// urls.GRAVATAR_URL = ‘https://www.gravatar.com/avatar’;

let getAvatarProps = emailId => {
  const emailHash = md5(emailId);
  const GRAVATAR_IMAGE_URL = `${urls.GRAVATAR_URL}/${emailHash}.jpg`;
  const avatarProps = {
    name: emailId.toUpperCase(),
    src: GRAVATAR_IMAGE_URL,
  };
  return avatarProps;
};

export default getAvatarProps;

 

  • Then pass the returned props on the CircleImage component and set it as the leftAvatar property of the feedback comments ListItem. Following is the snippet –

….
<ListItem
  key={index}
  leftAvatar={<CircleImage {...avatarProps} size="40" />}
  primaryText={
    <div>
      <div>{`${data.email.slice(
        0,
        data.email.indexOf('@') + 1,
      )}...`}</div>
      <div className="feedback-timestamp">
        {this.formatDate(parseDate(data.timestamp))}
      </div>
    </div>
  }
  secondaryText={<p>{data.feedback}</p>}
/>
….
.
.

 

  • This displays the avatar of the user on the UI. The UI changes have been shown below :

References

Continue Reading

Overriding the Basic File Attributes while Skill Creation/Editing on Server

In this blog post, we are going to understand the method for overriding basic file attributes while Skill creation/editing on SUSI Server. The need for this arose, when the creationTime for the Skill file that is stored on the server gets changed when the skill was edited.

Need for the implementation

As briefly explained above, the creationTime for the Skill file that is stored on the server gets changed when the skill is edited. Also, the need to override the lastModifiedTime was done, so that the Skills based on metrics gives correct results. Currently, we have two metrics for the SUSI Skills – Recently updated skills and Newest Skills. The former is determined by the lastModifiedTime and the later is determined by the creationTime. Due, to inconsistencies of these attributes, the skills that were shown were out of order. The lastModifiedTime was overridden to save the epoch date during Skill creation, so that the newly created skills don’t show up on the Recently Updated Skills section, whereas the creationTime was overridden to maintain the correct the time.

Going through the implementation

Let us first have a look on how the creationTime was overridden in the ModifySkillService.java file.

.
BasicFileAttributes attr = null;
Path p = Paths.get(skill.getPath());
try {
    attr = Files.readAttributes(p, BasicFileAttributes.class);
} catch (IOException e) {
    e.printStackTrace();
}
FileTime skillCreationTime = null;
if( attr != null ) {
    skillCreationTime = attr.creationTime();
}

if (model_name.equals(modified_model_name) &&
    group_name.equals(modified_group_name) &&
    language_name.equals(modified_language_name) &&
    skill_name.equals(modified_skill_name)) {
    // Writing to File
    try (FileWriter file = new FileWriter(skill)) {
        file.write(content);
        json.put("message", "Skill updated");
        json.put("accepted", true);

    } catch (IOException e) {
        e.printStackTrace();
        json.put("message", "error: " + e.getMessage());
    }
    // Keep the creation time same as previous
    if(attr!=null) {
        try {
            Files.setAttribute(p, "creationTime", skillCreationTime);
        } catch (IOException e) {
            System.err.println("Cannot persist the creation time. " + e);
        }
    }.
}
.
.
.

 

  • Firstly, we get the BasicFileAttributes of the Skill file and store it in the attr variable.
  • Next, we initialise the variable skillCreationTime of type FileTime to null and set the value to the existing creationTime.
  • The new Skill file is saved on the path using the FileWriter instance, which changes the creationTime, lastModifiedTime to the time of editing of the skill.
  • The above behaviour is not desired and hence, we want to override the creationTIme with the FileTime saved in skillCreationTIme. This ensures that the creation time of the skill is persisted, even after editing the skill.
  • Now we are going to see how the lastModifiedTime was overridden in the CreateSkillService.java file.

.
Path newPath = Paths.get(path);
// Override modified date to an older date so that the recently updated metrics works fine
// Set is to the epoch time
try {
  Files.setAttribute(newPath, "lastModifiedTime", FileTime.fromMillis(0));
} catch (IOException e) {
  System.err.println("Cannot override the modified time. " + e);
}
.
.
.

 

  • For this, we get the newPath of the Skill file and then the lastModifiedTime for the Skill File is explicitly set to a particular time.
  • We set it to FileTime.fromMillis(0) i.e, the epoch time.

I hope that I was able to convey my learnings and implementation of the code properly and it proved to be helpful for your understanding.

Resources

Documentation for BasicFileAttributes Interface – https://docs.oracle.com/javase/8/docs/api/java/nio/file/attribute/BasicFileAttributes.html

Continue Reading
Close Menu
%d bloggers like this: