Add Feature to Post Feedback for a Skill in SUSI.AI Server

In this blog post, we are going to discuss on how the feature to post user feedback was implemented on the SUSI. AI server side. The API endpoint by which a user can post feedback for a Skill is https://api.susi.ai/cms/feedbackSkill.json.

It accepts five url parameters –

  • 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 Knowledge.
  • language – It tells the language to which the skill belongs. The default value is set to en.
  • skill – It is the name of the skill to be given a feedback. It is a compulsory param.
  • feedback – It is the parameter that contains the string type feedback of the skill, that the user wants to update.

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

Going through the API development

  • The parameters are first extracted via the call object that is passed to the main function. The  parameters are then stored in variables. If any parameter is absent, then it is set to the default value.
  • There is a check if the skill exists. It is done by checking, if there exists a file corresponding to that skill. If no, then an exception is thrown.
  • This code snippet discusses the above two points –

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

        String model_name = call.get("model", "general");
        File model = new File(DAO.model_watch_dir, model_name);
        String group_name = call.get("group", "Knowledge");
        File group = new File(model, group_name);
        String language_name = call.get("language", "en");
        File language = new File(group, language_name);
        String skill_name = call.get("skill", null);
        File skill = SusiSkill.getSkillFileInLanguage(language, skill_name, false);
        String skill_feedback = call.get("feedback", null);

        JSONObject result = new JSONObject();
        if (!skill.exists()) {
            throw new APIException(422, "Skill does not exist.");
        }
.
.
.

 

  • Then the  feedbackSkill.json that was initialized in the DAO object, is then opened. It is a JSONTray, which is then parsed.
  • It is checked if the skill is previously rated or not. If yes, has the user rated it before or not. In the case, when the user has given a feedback for it already, we just need to make changes in the feedbackSkill.json file as the feedback_count key present in the skillRatings.json doesn’t change. In case, the user hasn’t given the feedback earlier, the changes also needs to be reflected on the skillRatings.json file. For this use,  the utility function addToSkillRatingJSON is called. The feedback_count of that skill is increased by one, as it is a unique feedback and changes need to be reflected on the file.
  • The response object is then sent with three key values mainly, apart from the session object. They are
    • accepted –  true – It tells that the skill feedback has been updated.
    • message – “Skill feedback updated”
    • feedback –  <feedback> – It is the string that the client has sent as feedback.

Here are code snippets and data objects that will help in understanding the API well –

  • addToSkillRating function –
    public void addToSkillRatingJSON(Query call) {
        String model_name = call.get("model", "general");
        String group_name = call.get("group", "Knowledge");
        String language_name = call.get("language", "en");
        String skill_name = call.get("skill", null);

        JsonTray skillRating = DAO.skillRating;
        JSONObject modelName = new JSONObject();
        JSONObject groupName = new JSONObject();
        JSONObject languageName = new JSONObject();
        JSONObject skillName = new JSONObject();
        if (skillRating.has(model_name)) {
            modelName = skillRating.getJSONObject(model_name);
            if (modelName.has(group_name)) {
                groupName = modelName.getJSONObject(group_name);
                if (groupName.has(language_name)) {
                    languageName = groupName.getJSONObject(language_name);
                    if (languageName.has(skill_name)) {

                        skillName = languageName.getJSONObject(skill_name);
                    }
                }
            }
        }

        if (skillName.has("feedback_count")) {

            int skillFeedback = skillName.getInt("feedback_count");
            skillName.put("feedback_count", skillFeedback + 1 );
        } else {

            skillName.put("feedback_count", 1);
        }
                        
        if (!skillName.has("stars")) {

            JSONObject skillStars = new JSONObject();
            skillStars.put("one_star", "0");
            skillStars.put("two_star", "0");
            skillStars.put("three_star", "0");
            skillStars.put("four_star", "0");
            skillStars.put("five_star", "0");
            skillStars.put("avg_star", "0");
            skillStars.put("total_star", "0");
            skillName.put("stars", skillStars);
        }

        languageName.put(skill_name, skillName);
        groupName.put(language_name, languageName);
        modelName.put(group_name, groupName);
        skillRating.put(model_name, modelName, true);
        return;
    }
}

 

  • Sample  of feedbackSkill.json file –

{
  "general": {
    "Knowledge": {
      "en": {
        "aboutsusi": [
          {
            "feedback": "The skill is awesome!",
            "email": "email1@domain.com",
            "timestamp": "2018-06-06 02:20:20.245"
          },
          {
            "feedback": "Great!",
            "email": "email2@domain.com",
            "timestamp": "2018-06-06 02:20:20.245"
          }
        ],
        "quotes": [
          {
            "feedback": "Wow!",
            "email": "email1@domain.com",
            "timestamp": "2018-06-06 02:19:53.86"
          }
        ]
      }
    }
  }
}
  • Sample  of skillRatings.json file –

{
  "general": {
    "Knowledge": {
      "en": {
        "aboutsusi": {
          "negative": "0",
          "positive": "0",
          "stars": {
            "one_star": 0,
            "four_star": 2,
            "five_star": 0,
            "total_star": 5,
            "three_star": 3,
            "avg_star": 3.4,
            "two_star": 0
          },
          "feedback_count": 2
        },
        "quotes": {
          "negative": "0",
          "positive": "0",
          "stars": {
            "one_star": 0,
            "four_star": 0,
            "five_star": 0,
            "total_star": 0,
            "three_star": 0,
            "avg_star": 0,
            "two_star": 0
          },
          "feedback_count": 1
        }
      }
    }
  }
}

 

I hope the development of creating the aforesaid API is clear and proved to be helpful for your understanding.

Continue ReadingAdd Feature to Post Feedback for a Skill in SUSI.AI Server

Parallelizing travis build in Open Event Web app

 

Open Event Web app uses Travis CI as a platform to perform unit testing. Travis CI is a hosted, distributed continuous integration service used to build and test projects hosted at GitHub. Travis CI automatically detects when a commit has been made and pushed to a GitHub repository that is using Travis CI, and each time this happens, it will try to build the project and run tests. Travis build took around 24 minutes to complete when any commit is made to the project, which is a very large time, to reduce the build time we parallelized the build which uses maximum amount of resources available at that time and run the builds parallely which resulted into better use of resources as well as required lesser amount of time.

Open Event web app uses saucelabs integration to perform selenium tests and travis to perform continuous integration.

Why parallelize the build?

When there are unit tests that are independent of each other and can be executed using a common set of dependencies, those procedures can be performed parallely on different virtual machines bringing out maximum throughput.

Running say a large number of tests on a single machine can increase the build time to a large extent, this build time can be reduced significantly by running the tests parallely on different machines. Open Event Webapp has a build time of around 24 minutes which is reduced to half on parallelising the build.

Parallelizing your builds across virtual machines

To speed up a test suite, you can break it up into several parts using Travis CI’s build matrix feature.

Say you want to split up your unit tests and your integration tests into two different build jobs. They’ll run in parallel and fully utilize the available build capacity and the resources.

The architecture of open event webapp supports test suite for all the pages in the generated application. To parallelize the build, the test suite is divided in different files  with the directory structure as shown below:

   
   ├── test
      ├── serverTest.js
      ├── roomsAndSpeakers.js
      ├── tracks.js
      ├── generatorAndSchedule.js
      ├── sessionAndEvent,js

 

The env key in travis.yml is modified as shown below:

env:
  - TESTFOLDER=test/serverTest.js
  - TESTFOLDER=test/roomsAndSpeakers.js
  - TESTFOLDER=test/tracks.js
  - TESTFOLDER=test/generatorAndSchedule.js
  - TESTFOLDER=test/sessionAndEvent.js

 

The script running tests fetches environment variable and runs the test file accordingly as shown below:

# installing required items for build
install:
 - npm install -g istanbul mocha@3
 - npm install
 - npm install --save-dev

# testing script
script:
 - istanbul cover _mocha -- $TESTFOLDER

# notify codecov and deploy to cloud
after_success:
 if ([ "$TESTFOLDER" == "test/serverTest.js" ]); then
   bash <(curl -s https://codecov.io/bash);
   bash gh_deploy.sh && kubernetes/travis/deploy.sh;
 fi

Results:

The build time which was earlier 23 minutes is reduced to 12 minutes after parallelizing the build.

Resources

 

Continue ReadingParallelizing travis build in Open Event Web app

Enable web app generation for multiple API formats

 

 

 

 

 

Open event server has two types of API (Application Programming Interface) formats, with one being generated by the legacy server and other one by the server side of decoupled development structure. The open event web app supported only the new version of API format, thus an error in read contents of JSON was thrown for the old version API format. To enable the support for both kind of API formats such that web app can be generated for each of them and there is no need to convert JSON files of version v1 to v2 we added an option field to the generator, where the client can choose the API version.

Excerpts and description for difference between data formats of API v1 and v2

The following excerpt is a subprogram getCopyrightData in both versions v1 and v2. The key for getting licence details in v1 is ‘licence_details’ and in v2 is ‘licence-details’. Similarly the key for getting copyright details in v1 is ‘copyright’ and in v2 is ‘event-copyright’.

So the data is extracted from the JSON files depending on the API version, the client has selected.

API V1

function getCopyrightData(event) {
 if(event.licence_details) {
   return convertLicenseToCopyright(event.licence_details, event.copyright);
 } else {
   return event.copyright;
 }
}

 

API V2

function getCopyrightData(event) {
 if(event['licence-details']) {
   return convertLicenseToCopyright(event['licence-details'], event['event-copyright']);
 } else {
   event['event-copyright'].logo = event['event-copyright']['logo-url'];
   return event['event-copyright'];
 }
}

 

Another example showing the difference between the API formats of v1 and v2 is excerpted below.

The following excerpt shows a constant ‘url’ containing the event URLs and the details. The version v1 uses event_url as a key for the main page url whereas v2 uses event-url for getting the same. A similar kind of structural difference is present for rest of the fields where the special character underscore has been replaced by hyphen and a bit of change in the name format for keys such as start_time, end_time.

API v1

const urls= {
 main_page_url: event.event_url,
 logo_url: event.logo,
 background_url: event.background_image,
 background_path: event.background_image,
 description: event.description,
 location: event.location_name,
 orgname: event.organizer_name,
 location_name: event.location_name,
};

 

API v2

const urls= {
 main_page_url: event['event-url'],
 logo_url: event['logo-url'],
 background_url: event['original-image-url'],
 background_path: event['original-image-url'],
 location: event['location-name'],
 orgname: event['organizer-name'],
 location_name: event['location-name'],
};

How we enabled support for both API formats?

To add the support for both API formats we added a options field on generator’s index page where the user chooses the type of API format for web app generation.

<label>Choose your API version</label>
<ul style="list-style-type:none">
 <li id="version1"><input name="apiVersion" type="radio" value="api_v1">   API_v1</li>
 <li id="version2"><input name="apiVersion" type="radio" value="api_v2"> API_v2</li>
</ul>

The generator depending on the version of API format, chooses the correct file where the data extraction from the input JSON files takes place. The file names are fold_v1.js and fold_v2.js for extraction of JSON v1 data and JSON v2 data respectively.

var type = req.body.apiVersion || 'api_v2';

if(type === 'api_v1') {
 fold = require(__dirname + '/fold_v1.js');
}
else {
 fold = require(__dirname + '/fold_v2.js');
}

 

The excerpts of code showing the difference between API formats of v1 and v2 are the contents of fold_v1.js and fold_v2.js files respectively.

Resources

Continue ReadingEnable web app generation for multiple API formats

Submitting a Github Issue through a Google Form

The Pocket Science Lab Android app has various functionalities which have been already implemented but it been on the verge of development, many functionalities are yet to be implemented completely, one such functionality is how the users report the issues of the app, to which comes the idea of using a Google form inside the app for the users to fill it and the issue get directly opened in Github.

Submitting a Github issue through a Google forms requires two things:-

    1. A Github access token which gives access to open a new issue.
      • To generate a Github access token one must follow these steps[2]
        • Go to the personal settings.

        • Select  Developers settings option from it.
        • In Developers settings option Go to personal access tokens and generate an access token.
    1. A fully-structured Google form which has all the details about the issue i.e the title of the issue, the body of the issue, label, etc..
      • Using a Google account create a Google Form which have all the relevant questions about that issue such as title of the issue, body of the issue, label etc..

Once done with all the steps follow these steps to send a Github issue[1]

    1. Click the Responses tab, in it click the More icon.
    2. Select Choose a response destination.
    3. Select New spreadsheet: Creates a new spreadsheet in Google Sheets for responses.
    4. Click Create to create and open the sheet.

Configure the App Script Logic[1]

    1. You should have a newly created blank spreadsheet with headers automatically generated from your form.
    2. Click Tools > Script editor… to launch the App Script editor coding environment. This Script will be bound to your sheet, so you can listen for form submissions and fire off a new issue to your GitHub repo.
    3. In the script editor write the following code
function onFormSubmit(e) {

var title = e.values[1];
var body = e.values[2];
var label = "User opened issue"
var payload = {
"title": title,
"body": a_body,
"label": label,
};

var options = {
"method": "POST",
"contentType": "application/json",
"payload": JSON.stringify(payload)
};
var response = UrlFetchApp.fetch("https://api.github.com/repos/abhinavraj23/AgeGroup/issues?access_token="+ghToken, options)
}

Note:The onFormSubmit function includes an event object e, which includes the form/spreadsheet field values as a simple array with values in the same order as they appear in the spreadsheet. e.values[0] is the first spreadsheet column

The following google-app script uses GitHub Issues API for posting a new issue in Github.

4.Give your app script project a name and save it .

Set up the Trigger[1]

        1. From within the app script editor, click Resources > Current project’s triggers.
        2. Click to add a trigger
          1. Run: onFormSubmit
          2. Events: From spreadsheet, On form submit
        3. Click Save and accept any authorizations to access your forms and access web services on your behalf.
        4. This trigger will listen to form submissions and pass the data to your function, which POSTs the new issue to your GitHub repo.

Thus using these steps one can submit an issue in github through a Google Form and thus the Google Forms can be used in the app as the users can send the issues using a google form, and through this method one can also get the email-id of the user for further contact and thus this is a very  useful method.

Resources

      1. Bmcbride, google-form-to-github-issue,gist.github.com: https://gist.github.com/bmcbride/62600e48274961819084#set-up-the-trigger
      2. Github help, Creating personal access token,help.github.com: https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/
Continue ReadingSubmitting a Github Issue through a Google Form

Open Event Server – Export Attendees as CSV File

FOSSASIA‘s Open Event Server is the REST API backend for the event management platform, Open Event. Here, the event organizers can create their events, add tickets for it and manage all aspects from the schedule to the speakers. Also, once he/she makes his event public, others can view it and buy tickets if interested.

The organizer can see all the attendees in a very detailed view in the event management dashboard. He can see the statuses of all the attendees. The possible statuses are completed, placed, pending, expired and canceled, checked in and not checked in. He/she can take actions such as checking in the attendee.

If the organizer wants to download the list of all the attendees as a CSV file, he or she can do it very easily by simply clicking on the Export As and then on CSV.

Let us see how this is done on the server.

Server side – generating the Attendees CSV file

Here we will be using the csv package provided by python for writing the csv file.

import csv
  • We define a method export_attendees_csv which takes the attendees to be exported as a CSV file as the argument.
  • Next, we define the headers of the CSV file. It is the first row of the CSV file.
def export_attendees_csv(attendees):
   headers = ['Order#', 'Order Date', 'Status', 'First Name', 'Last Name', 'Email',
              'Country', 'Payment Type', 'Ticket Name', 'Ticket Price', 'Ticket Type']
  • A list is defined called rows. This contains the rows of the CSV file. As mentioned earlier, headers is the first row.
rows = [headers]
  • We iterate over each attendee in attendees and form a row for that attendee by separating the values of each of the columns by a comma. Here, every row is one attendee.
  • The newly formed row is added to the rows list.
for attendee in attendees:
   column = [str(attendee.order.get_invoice_number()) if attendee.order else '-',
             str(attendee.order.created_at) if attendee.order and attendee.order.created_at else '-',
             str(attendee.order.status) if attendee.order and attendee.order.status else '-',
             str(attendee.firstname) if attendee.firstname else '',
             str(attendee.lastname) if attendee.lastname else '',
             str(attendee.email) if attendee.email else '',
             str(attendee.country) if attendee.country else '',
             str(attendee.order.payment_mode) if attendee.order and attendee.order.payment_mode else '',
             str(attendee.ticket.name) if attendee.ticket and attendee.ticket.name else '',
             str(attendee.ticket.price) if attendee.ticket and attendee.ticket.price else '0',
             str(attendee.ticket.type) if attendee.ticket and attendee.ticket.type else '']

   rows.append(column)
  • rows contains the contents of the CSV file and hence it is returned.
return rows
  • We iterate over each item of rows and write it to the CSV file using the methods provided by the csv package.
writer = csv.writer(temp_file)
from app.api.helpers.csv_jobs_util import export_attendees_csv
content = export_attendees_csv(attendees)
for row in content:
   writer.writerow(row)

Obtaining the Attendees CSV file:

Firstly, we have an API endpoint which starts the task on the server.

GET - /v1/events/{event_identifier}/export/attendees/csv

Here, event_identifier is the unique ID of the event. This endpoint starts a celery task on the server to export the attendees of the event as a CSV file. It returns the URL of the task to get the status of the export task. A sample response is as follows:

{
  "task_url": "/v1/tasks/b7ca7088-876e-4c29-a0ee-b8029a64849a"
}

The user can go to the above-returned URL and check the status of his/her Celery task. If the task completed successfully he/she will get the download URL. The endpoint to check the status of the task is:

and the corresponding response from the server –

{
  "result": {
    "download_url": "/v1/events/1/exports/http://localhost/static/media/exports/1/zip/OGpMM0w2RH/event1.zip"
  },
  "state": "SUCCESS"
}

The file can be downloaded from the above-mentioned URL.

References

Continue ReadingOpen Event Server – Export Attendees as CSV File

Open Event Server – Export Orders as CSV File

FOSSASIA‘s Open Event Server is the REST API backend for the event management platform, Open Event. Here, the event organizers can create their events, add tickets for it and manage all aspects from the schedule to the speakers. Also, once he/she makes his event public, others can view it and buy tickets if interested.

The organizer can see all the orders in a very detailed view in the event management dashboard. He can see the statuses of all the orders. The possible statuses are completed, placed, pending, expired and canceled.

If the organizer wants to download the list of all the orders as a CSV file, he or she can do it very easily by simply clicking on the Export As and then on CSV.

Let us see how this is done on the server.

Server side – generating the Orders CSV file

Here we will be using the csv package provided by python for writing the csv file.

import csv
  • We define a method export_orders_csv which takes the orders to be exported as a CSV file as the argument.
  • Next, we define the headers of the CSV file. It is the first row of the CSV file.
def export_orders_csv(orders):
   headers = ['Order#', 'Order Date', 'Status', 'Payment Type', 'Total Amount', 'Quantity',
              'Discount Code', 'First Name', 'Last Name', 'Email']
  • A list is defined called rows. This contains the rows of the CSV file. As mentioned earlier, headers is the first row.
rows = [headers]
  • We iterate over each order in orders and form a row for that order by separating the values of each of the columns by a comma. Here, every row is one order.
  • The newly formed row is added to the rows list.
for order in orders:
   if order.status != "deleted":
       column = [str(order.get_invoice_number()), str(order.created_at) if order.created_at else '',
                 str(order.status) if order.status else '', str(order.paid_via) if order.paid_via else '',
                 str(order.amount) if order.amount else '', str(order.get_tickets_count()),
                 str(order.discount_code.code) if order.discount_code else '',
                 str(order.user.first_name)
                 if order.user and order.user.first_name else '',
                 str(order.user.last_name)
                 if order.user and order.user.last_name else '',
                 str(order.user.email) if order.user and order.user.email else '']
       rows.append(column)
  • rows contains the contents of the CSV file and hence it is returned.
return rows
  • We iterate over each item of rows and write it to the CSV file using the methods provided by the csv package.
writer = csv.writer(temp_file)
from app.api.helpers.csv_jobs_util import export_orders_csv
content = export_orders_csv(orders)
for row in content:
   writer.writerow(row)

Obtaining the Orders CSV file:

Firstly, we have an API endpoint which starts the task on the server.

GET - /v1/events/{event_identifier}/export/orders/csv

Here, event_identifier is the unique ID of the event. This endpoint starts a celery task on the server to export the orders of the event as a CSV file. It returns the URL of the task to get the status of the export task. A sample response is as follows:

{
  "task_url": "/v1/tasks/b7ca7088-876e-4c29-a0ee-b8029a64849a"
}</span

The user can go to the above-returned URL and check the status of his/her Celery task. If the task completed successfully he/she will get the download URL. The endpoint to check the status of the task is:

and the corresponding response from the server –

{
  "result": {
    "download_url": "/v1/events/1/exports/http://localhost/static/media/exports/1/zip/OGpMM0w2RH/event1.zip"
  },
  "state": "SUCCESS"
}

The file can be downloaded from the aabove-mentionedURL.

References

Continue ReadingOpen Event Server – Export Orders as CSV File

Creating Instruction Guide using Bottomsheet

The PSLab android app consists of different instruments like oscilloscope, multimeter, wave generator etc and each instrument has different functionality and usage so it is necessary that there should be an instruction guide for every instrument so that the user can easily read the instruction to understand the functionality of the instrument.

In this we will create an instruction guide for the Wave Generator which will contain information about the instrument, it’s functionalities, steps for how to use the instrument.

The main component that I used to create instruction guide is Bottom SheetBottom Sheet is introduced in Android Support v23.2 . It is a special UI widget which slide up from the bottom of the screen and it can be used to reveal some extra information that we cannot show on the main layout like bottom menus,  instructions etc.

They are of two types :

  1. Modal Bottom Sheet:–  This Bottom Sheet has properties very similar to normal dialogs present in Android like elevation only difference is that they pop up from the bottom of screen with proper animation and they are implemented using BottomSheetDialogFragment Class.

  2. Persistent Bottom Sheet:– This Bottom Sheet is included as a part of the layout and they can be slid up and down to reveal extra information. They are implemented using BottomSheetBehaviour Class.

For my project, I used persistent Bottom Sheet as modal Bottom Sheet can’t be slid up and down by the swipe of the finger whereas persistent Bottom Sheet can be slid up and down and can be hidden by swipe features.

Implementing the Bottom Sheet

Step 1: Adding the Dependency

To start using Bottom Sheet we have to add the dependency (We have also include Jake Wharton-Butterknife library for view binding but it is optional.)

dependencies {

   implementation fileTree(include: ['*.jar'], dir: 'libs')

   implementation "com.android.support:appcompat-v7:26.0.1"
   implementation "com.android.support:design:26.0.1"
   implementation "com.jakewharton:butterknife:8.8.1"

   annotationProcessor "com.jakewharton:butterknife-compiler:8.8.1"

Step 2: Creating Bottom Sheet layout file

In this step, we will create the layout of the Bottom Sheet, as our purpose of making Bottom Sheet is to show extra information regarding the instrument so we will include ImageView and TextView inside the layout that will be used to show the content later.

Some attributes in the layout worth noting are:

  • app:layout_behavior: This attribute makes the layout act as Bottom Sheet.
  • app:behavior_peekHeight: This is the height of the Bottom Sheet when it is minimized.
  • app:behavior_hideable: Defines if the Bottom Sheet can be hidden by swiping it down.

Here, we will also create one extra LinearLayout having height equal to the peek_height. This  LinearLayout will be at the top of the BottomSheet as shown in Figure 1 and it will be visible when the BottomSheet is in a minimized state(not hidden). Here we will put text view with like “Show guide” and an arrow pointing upwards so that it is easier for the user to understand that sheet can be viewed by sliding up.

Figure 1 LinearLayout containing textview and imageview

Here is the gist[2] that contains code for the layout of the Bottom Sheet guide

After this step, we can see a layout of Bottom Sheet in our Android Editor as shown in Figure 2

Figure 2 shows the layout of the Bottom Sheet

Step 3: Creating the Container view layout containing content and Bottom Sheet

For container view, we will create new layout under Res Layout and name it “container_view_wavegenerator.xml”

In this layout, we will use ‘Coordinator Layout’ as ViewGroup because persistent Bottom Sheet is implemented using BottomSheetBehavior class which can only be applied to the child of ‘CoordinatorLayout’.

Then add the main layout of our instrument and the layout of the Bottom Sheet inside this layout as its child.

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent">

   <include layout="@layout/activity_wave_generator" />

   <include layout="@layout/bottom_sheet_custom" />

Step 4: Setting Up Bottom Sheet and Handling callbacks

Now we will head over to the “WaveGenerator.java” file(or any instrument java file)Here we will handle set up Bottom Sheet and handle callbacks by using following classes:

BottomSheetBehavior provides callbacks and makes the Bottom Sheet work with CoordinatorLayout.

BottomSheetBehavior.BottomSheetCallback() provides the callback when the Bottom Sheet changes its state. It has two methods that need to be overridden:

  1. public void onSlide(@NonNull View bottomSheet, float slideOffset)

    This method is called when the Bottom Sheet slides up and down on the screen. It has slideOffset as a parameter whose value varies from -1.0 to 0.0 when the Bottom Sheet comes from the hidden state to collapsed and 0.0 to 1.0 when it goes from collapsed state to expanded state.

  2. public void onStateChanged(@NonNull View bottomSheet, int newState)

    This method is called when BottomSheet changed its state. Here, let us also understand the different states which can be attained by the Bottom Sheet:

    • BottomSheetBehavior.STATE_EXPANDED : When the Bottom Sheet is fully expanded showing all the content.
    • BottomSheetBehavior.STATE_HIDDEN : When the Bottom Sheet is hidden at the bottom of the layout.
    • BottomSheetBehavior.STATE_COLLAPSED : When the Bottom Sheet is in a collapsed state that is only the peek_height view part of the layout is visible.
    • BottomSheetBehavior.STATE_DRAGGING : When the Bottom Sheet is dragging.
    • BottomSheetBehavior.STATE_SETTLING : When the Bottom Sheet is settling either at expanded height or at collapsed height.

We will implement these methods in our instrument class, and also put the content that needs to be put inside the Bottom Sheet.

@BindView(R.id.bottom_sheet)
LinearLayout bottomsheet;
@BindView(R.id.guide_title)
TextView bottomSheetGuideTitle;
@BindView(R.id.custom_dialog_schematic)
ImageView bottomSheetSchematic;
@BindView(R.id.custom_dialog_desc)
TextView bottomSheetDesc;

BottomSheetBehavior bottomSheetBehavior;

@Override
protected void onCreate(Bundle savedInstanceState) { 
              
           //main instrument implementation code

           setUpBottomSheet()
}

private void setUpBottomSheet() {

   bottomSheetBehavior = BottomSheetBehavior.from(bottomsheet);

    bottomSheetGuideTitle.setText(R.string.wave_generator);
   bottomSheetSchematic.setImageResource(R.drawable.sin_wave_guide);
   bottomSheetDesc.setText(R.string.wave_gen_guide_desc);

  bottomSheetBehavior.setBottomSheetCallback(new 
  BottomSheetBehavior.BottomSheetCallback() {
       @Override
       public void onStateChanged(@NonNull View bottomSheet, int newState) {
           if (newState == BottomSheetBehavior.STATE_EXPANDED) {
               bottomSheetSlideText.setText(R.string.hide_guide_text);
           } else {
               bottomSheetSlideText.setText(R.string.show_guide_text);
           }
       }

       @Override
       public void onSlide(@NonNull View bottomSheet, float slideOffset) {
           Log.w("SlideOffset", String.valueOf(slideOffset));
        }
      });
}

After following all the above steps the Bottom Sheet will start working properly in the Instrument layout as shown in Figure 3

Figure 3 shows the Bottom Sheet in two different states

To read more about implementing Bottom Sheet in layout refer this: AndroidHive article- working with bottomsheet

Resources

  1. Developer Documentation – BottomSheetBehaviour
  2. Gist containing xml file for layout of Custom Bottomsheet
Continue ReadingCreating Instruction Guide using Bottomsheet

Creating Control Panel For Wave Generator using Constraint Layout

 

In the blog Creating the onScreen Monitor Using CardView I had created the monitors to view the wave properties in this blog we will create the UI of controlling panel that will be used for that monitors with multiple buttons for both analog and digital waveforms.

Which layout to choose?

In today’s world, there are millions of Android devices present with different screen sizes and densities and the major concern of an Android developer is to make the layout that fits all the devices and this task is really difficult to handle with a linear or relative layout with fixed dimensions.

To create a complex layout with lots of views inside the parent using linear layout we have to make use of the attribute layout_weight for their proper stretching and positioning, but such a complex layout require a lot of nesting of weights and  android tries to avoid it by giving a warning :

Nested Weights are bad for performance

This is because layout_weight attribute requires a widget to be measured twice[1]. When a LinearLayout with non-zero weights is nested inside another LinearLayout with non-zero weights, then the number of measurements increases exponentially.

So, to overcome this issue we will make use of special type of layout “Constraint Layout” which was introduced in Google I/O 2016.

Features of Constraint Layout:-

  • It is similar to Relative Layout as all views are laid out according to their relationship with the sibling, but it is more flexible than Relative Layout.
  • It helps to flatten the view hierarchy for complex layouts.
  • This layout is created with the help of powerful tool provided by Android which has a palette on the left-hand side from where we can drag and drop the different widgets like TextView, ImageView, Buttons etc. and on the right-hand side it provides options for positioning, setting margins and other styling option like setting color, change text style etc.
  • It automatically adjusts the layout according to the screen size and hence doesn’t require the use of layout_weight attribute.

In following steps, I will create the controlling panel for Wave generator which is a complex layout with lots of buttons with the help of constraint layout.

Step 1: Add the dependency of the Constraint Layout in the Project

To use Constraint layout add the following to your build.gradle file and sync the project

dependencies {
    implementation "com.android.support.constraint:constraint-layout:1.1.0"
}

Step 2: Applying Guidelines to the layout

Guidelines[3] are anchors that won’t be displayed in your app, they are like one line of a grid above your layout and can be used to attach or constraint your widgets to it. They are only visible on your blueprint or preview editor. These will help to position and constraint the UI components on the screen easily.

For adding guidelines :

As shown in Figure 1 Right-click anywhere on the layout -> Select helpers -> Select horizontal or vertical guideline according to your need.

Figure 1 shows the horizontal guideline being added to the layout.

And for positioning the guideline we have to set the value of attribute layout_constraintGuide_percent  

Let’s say we want the guideline to be at the middle of the screen so we’ll set :

app:layout_constraintGuide_percent=”0.50″

For my layout I have added three guideline :

  • One horizontal guideline at 50%
  • Two vertical guidelines at 30% and 65%

Doing this will bifurcate the screen into six square blocks as shown in below figure :

Figure 2 shows the blueprint of constraint layout containing two vertical and one horizontal guidelines with their percentage offset from respective bases

Step 3: Adding the buttons in the blocks

Until now we have created six squares blocks, now we have to put a button view in each of the boxes.

  • First drag and drop button view from the Palette (shown in Figure 3) on the left side inside the box.

    Figure 3 shows the layout editor palette

     

  • Then we have to set constraints of this button by clicking on the small circle present on the middle of edges and dragging it onto the side of the block facing it.

    Figure 4 shows the button widget getting constrained to sides

     

  • Set the layout_width and layout_height attribute of the button to be “0dp”, doing this the button will expand in all the direction occupying all the space with respect to the border it has been constrained with.

    Figure 6 shows the button widget expanding to all the available space in the box

Similarly, adding buttons in all the square blocks and providing proper theme color we will have a blueprint and layout as shown in Figure 6.

Figure 6 shows the waveform panel blueprint and actual layout for analog waves with six buttons

Following the same steps until now, I have created the other controlling panel layout having buttons for digital waves as shown in Figure 7

Figure 7 shows other constraint layout for digital waves having seven buttons

Detailing and combining the panels to form Complete UI

After adding both the panels we have created in this layout inside the Wave Generator we have the layout as shown in Figure 8

Figure 8 shows the UI of Wave Generator as shown by a actual Android device in the PSLab app.

As we can see on adding the panels the button created inside the layout shrink so as to adapt to the screen and giving out a beautiful button-like appearance.

Resources   

  1. Blog on Nested Weights are bad for performance
  2. Developer Article – Build a Responsive UI with ConstraintLayout
  3. Information about Guidelines
Continue ReadingCreating Control Panel For Wave Generator using Constraint Layout

How to integrate SUSI.AI with Google Sign In API

Google Sign-In manages the OAuth 2.0 flow and token lifecycle, simplifying our integration with Google APIs. A user always has the option to revoke access to an application at any time. Users can see a list of all the apps which they have given permission to access their account details and are using the login API. In this blog post I will discuss how to integrate this API with susi server API to enhance our AAA system. More on Google API here.

Continue ReadingHow to integrate SUSI.AI with Google Sign In API

Handling No internet cases in Open Event Android

It’s pretty common to face connectivity issues and when the user has no Internet connection he should be shown an appropriate response rather than allowing him to send requests to the server. Let’s have a look how we are handling such cases in Open Event Android

Firstly we need to add the required permission in the manifest. We need the permission to access the user’s WiFi state and network state.

<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

 

We use this function to check if the user is connected to the Internet. This function return a Boolean which is true if the user is connected to the Internet otherwise it is false

private fun isNetworkConnected(): Boolean {
val connectivityManager = context?.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager

return connectivityManager?.activeNetworkInfo != null
}

 

This function is used to decide which screen should be shown to the user. If the user has an active Internet connection he will see events fragment but if there is no Internet he will see the no Internet card.

private fun showNoInternetScreen(show: Boolean) {
rootView.homeScreenLL.visibility = if (show) View.VISIBLE else View.GONE
rootView.noInternetCard.visibility = if (!show) View.VISIBLE else View.GONE
}

 

Let’s see how the above two functions are used in the events fragment. When the app starts we check if there is a need to show the no Internet screen. If the user is not connected to the Internet, the no Internet card will be shown. Then when the user clicks on retry, the events fragment is shown again if the user is connected to the Internet.

showNoInternetScreen(isNetworkConnected())

rootView.retry.setOnClickListener {
showNoInternetScreen(isNetworkConnected())
}

 

Let’s have a look a how the XML code looks, here we are only seeing a part of the code as the rest is pretty obvious. We have cardView and inside it all the views ie ImageView,TextView are inside a LinearLayout which has a vertical orientation so that all these views appear below each other.

<android.support.v7.widget.CardView
android:id="@+id/noInternetCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_margin_medium"
app:cardBackgroundColor="@color/white"
app:cardCornerRadius="@dimen/card_corner_radius"
app:cardElevation="@dimen/card_elevation">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_margin_extra_large"
android:orientation="vertical">

<ImageView
android:id="@+id/noInternetImageView"
android:layout_width="@dimen/item_image_view_large"
android:layout_height="@dimen/item_image_view_large"
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/layout_margin_large"
android:scaleType="centerCrop"
app:srcCompat="@drawable/ic_no_internet" />

<TextView
android:id="@+id/noInternetTextview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/layout_margin_large"
android:text="@string/no_internet_message"
android:textSize="@dimen/text_size_medium"
tools:text="No Internet" />

 

References

  1. AndroidHive tutorial – https://www.androidhive.info/2012/07/android-detect-internet-connection-status/
  2. Official Android Documentation – https://developer.android.com/training/monitoring-device-state/connectivity-monitoring
  3. StackOverflow – https://stackoverflow.com/questions/9570237/android-check-internet-connection
Continue ReadingHandling No internet cases in Open Event Android