Adding chrome custom tabs support for native browsing in Eventyay Organizer App

In Eventyay Organizer App when a user taps a URL, we face a choice: either launch a browser, or build our own in-app browser using WebViews.

Both options present challenges — launching the browser is a heavy context switch that isn’t customizable, while WebViews don’t share state with the browser and add maintenance overhead.

Chrome Custom Tabs give apps more control over their web experience, and make transitions between native and web content more seamless without having to resort to a WebView.

The first step for a Custom Tabs integration is adding the Custom Tabs Support Library to your project. Open your build.gradle file and add the support library to the dependency section. One must remember that Chrome Custom Tabs is not an Open Source library, so we are here including the  playStoreImplementation  as opposed to implementation, so that our FDroid build doesn’t fail.

Adding the dependency in build.gradle(app-level) in the project:

dependencies {
   //Other dependencies

   // Chrome Custom Tabs
   playStoreImplementation ‘com.android.support:customtabs:${versions.chromeCustomTabs}
}

 

And add this to  versions.gradle  file:

versions.chromeCustomTabs=‘27.1.0’

The UI Customizations are done by using the  CustomTabsIntent  and the CustomTabsIntent.Builder  classes; the performance improvements are achieved by using the  CustomTabsClient  to connect to the Custom Tabs service, warm-up Chrome and let it know which urls will be opened.

Since we need this to feature to be available for each place in the app that redirects to an external link, we must create it in the utility class of the project. Let’s name this class as BrowserUtils and do it as follows:

public final class BrowserUtils {
    private BrowserUtils() {
   }
    public static void launchUrl(Context context, String url) {
       CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
       CustomTabsIntent customTabsIntent = builder.build();
       customTabsIntent.launchUrl(context, Uri.parse(url));
   }
}

Now all we need to do is replace the old method to create an intent:

findPreference(getString(R.string.privacy_policy_key)).setOnPreferenceClickListener(preference -> {
           Intent intent = new Intent(Intent.ACTION_VIEW);
           intent.setData(Uri.parse(PRIVACY_POLICY_URL));
           startActivity(intent);
           return true;
       });

with this call to the utility method:

findPreference(getString(R.string.privacy_policy_key)).setOnPreferenceClickListener(preference -> {
           BrowserUtils.launchUrl(getContext(), PRIVACY_POLICY_URL);
           return true;
       });

We can also add animation or customize the color of the toolbar, add action buttons or add animations like this:

builder.setToolbarColor(colorInt);
builder.setStartAnimations(this, R.anim.slide_in_right, R.anim.slide_out_left);

builder.setExitAnimations(this, R.anim.slide_in_left, R.anim.slide_out_right);

Here’s the result:

Resources

Continue ReadingAdding chrome custom tabs support for native browsing in Eventyay Organizer App

Server Side Implement of Usage Analytics of a Skill in SUSI.AI

Which skills are being used most frequently in SUSI.AI? And which are rarely used? Such statistics are needed to give proper responses from the most used skills.

So when the user sends a query to SUSI, it searches for the best-suited skill to respond to that query. The usage count of that skill is incremented accordingly with the date.

Storage of Skill Usage Data

  1. Create a skillUsage.json file to store the stats and make a JSONTray object for that in DAO.java file.
  2. Modify the SusiArguement.java file to check which skill is being currently used for the response and write the skill usage stats to skillUsage.json.The function updateUsageData takes the skillPath in parameter and splits it from slash character. Thus the model name, group name, language name and skill name can be obtained. Then it searches for the date in the JSON file. If it exists then its count value is increased by 1, else a new object is inserted in the JSON file that contains the date and the corresponding usage count.

public void updateUsageData(String skillPath) {
    Boolean dateExists = false;
    for (int i = 0; i<usageData.length(); i++) {
        JSONObject dayUsage = usageData.getJSONObject(i);
        if (dayUsage.get("date").equals(today)){
            dayUsage.put("count", dayUsage.getInt("count")+1+"");
            usageData.put(i,dayUsage);
            dateExists = true;
            break;
        }
    }
}   

API to access the Skill Usage Data

  1. Create GetSkillUsageService.java file to return the usage stats stored in skillUsage.json. It runs at the endponit – /cms/getSkillUage.json and mining role required to access this API is anonymous. It simply reads the data stored in skillUsage.json file and returns it.
JsonTray skillRating = DAO.skillUsage;
JSONObject  languageName = groupName.getJSONObject(language_name);
if (languageName.has(skill_name)) {
    JSONArray skillUsage = languageName.getJSONArray(skill_name);
    result.put("skill_name", skill_name);
    result.put("skill_usage", skillUsage);
    result.put("accepted", true);
    result.put("message", "Skill usage fetched");
    return new ServiceResponse(result);
}
  1. Add the API file to skillUsage.json
services = new Class[]{
	...
	//Skill usage data
	GetSkillUsageService.class
	...
}

So now we know which skills are being used at what rate and make the required changes like server scaling, adding more accuracy to the feature etc. We can expand these stats to country wise or device wise usage distribution.

Resources

 

Continue ReadingServer Side Implement of Usage Analytics of a Skill in SUSI.AI

Limit the Chatbots to Run on Specified Domains

SUSI botbuilder enables users to make their own private skill and deploy a chatbot widget in their websites. Users can copy paste a javascript code into their website’s source code to activate the bot. But what if someone copies that code from your website and put it in their own website? You won’t want the chat bot to work for such users in some cases. Thus we have a feature through which the bot creator can restrict the usage of the chatbot to only certain domains. The chat bot will not work from other domains.

Understanding the APIs Used

In working of the chatbot, there are mainly two APIs used from the server which play a mainstream role. The first API is the cms/getSkillMetaData.json API. It is used to get the design and configurations of the chatbot. The second API is the susi/chat.json API. It is used to get responses from the server applying the private skill. By restricting the chatbot usage we try to restrict the usage of these two APIs. Also, on the client side we display the chatbot only if the server sends a valid response indicating that the chatbot is legitimate. However, this can be circumvented if the person modifies the javascript of the chatbot. Hence, we need to secure the above two APIs. We check the domain from where the request is coming by checking the referer field in the request’s header.

Securing the APIs

In each of the above two APIs, we check if the bot owner has checked “allow bot only on own site”. If no, then the APIs can be accessed from any domain we need not check. If selected yes, then we need to check if the current site’s domain is allowed in the allowed sites list. For this, we extract the current domain from the request’s referer field. The allowed sites list is fetched from the configure object of that skill.

public static boolean allowDomainForChatbot(JSONObject configureObject, String referer) {
    Boolean allowed_site = true;
    if (configureObject.getBoolean("allow_bot_only_on_own_sites") && configureObject.has("allowed_sites") && configureObject.getString("allowed_sites").length() > 0) {
        allowed_site = false;
        if (referer != null && referer.length() > 0) {
            String[] sites = configureObject.getString("allowed_sites").split(",");
            for (int i = 0; i < sites.length; i++) {
                String site = sites[i].trim();
                int referer_index = referer.indexOf("://");
                String host = referer;
                if (referer.indexOf('/',referer_index+3) > -1) {
                    host = referer.substring(0,referer.indexOf('/',referer_index+3));
                }
                if (host.equalsIgnoreCase(site)) {
                    allowed_site = true;
                    break;
                }
            }
        }
    }
    return allowed_site; 
}

 

Result

Not allowed from other domains:
(For getSkillMetaData.json API)

Allowed on approved domains:
(For getSkillMetaData.json API)

Resources

 

Continue ReadingLimit the Chatbots to Run on Specified Domains

Upgrading Open Event to Use Sendgrid API v3

Sendgrid recently upgraded their web API to send emails, and support for previous versions was deprecated. As a result, Open Event Server’s mail sending tasks were rendered unsuccessful, because the requests they were sending to Sendgrid were not being processed. On top of that, it was also found out later that the existing Sendgrid API key on the development server was expired. This had to be fixed at the earliest because emails are a core part of Open Event functionality.

The existing way for emails to be sent via Sendgrid used to hit the endpoint “https://api.sendgrid.com/api/mail.send.json” to send emails. Also, the payload structure was as follows:

payload = {
    'to': to,
    'from': email_from,
    'subject': subject,
    'html': html
}

Also, a header  “Authorization”: “Bearer ” accompanied the above payload. However, Sendgrid changed the payload structure to be of the following format:

{

“personalizations”: [

{“to”: [

{“email”: “example@example.com“}

]

}

],

“from”: {

“email”: “example@example.com

},

“subject”: “Hello, World!”,

“content”: [

{

“type”: “text/plain”,

“value”: “Heya!”

}

]

}

Furthermore, the endpoint was changed to be “https://api.sendgrid.com/v3/mail/send”. To incorporate all these changes with the minimum number of modified lines in the codebase, it was required for that the structure change itself happens at a fairly low level. This was because there are lots of features in the server that perform a wide variety of email actions. Thus, it was clear that changing all of them will not be the most efficient thing to do. So the perfect place to implement the API changes was the function send_email() in mail.py, because all other higher-level email functions are built on top of this function. But this was not the only change, because this function itself used another function, called send_email_task() in tasks.py, specifically for sending email via Sendgrid. So, in conclusion, the header modifications were made in send_email() and payload structure as well as endpoint modifications were made within send_email_task(). This brought the server codebase back on track to send emails successfully. Finally, the key for development server was also renewed and added to its settings in the Heroku Postgres database.

Screenshots:

Screen Shot 2018-08-21 at 3.40.12 PM.png

Screen Shot 2018-08-21 at 3.40.32 PM.png

Resources

Continue ReadingUpgrading Open Event to Use Sendgrid API v3

Deploying loklak search on Heroku & Using config to store and load external URLs

It is really important to have a separate configuration for all the hardcoded URLs having multiple instances across the project. It would help in testing and comparing projects like loklak with a separate configuration for hardcoded URLs. We would also be discussing deployment of Angular based loklak on heroku through this blog post.

Creating shared URL config

The idea here is to export a const Object containing all the collective hardcoded URLs used inside the project. We would try to store similar/collective URLs inside a same key e.g. all the github URLs must go inside key: ‘github’.

export const defaultUrlConfig = {
	fossasia: {
		root: 'https://fossasia.org',
		blog: 'https://blog.fossasia.org'
	},
	loklak: {
		apiServer: 'https://api.loklak.org',
		apiBase: 'loklak.org',
		blog: 'http://blog.loklak.net',
		dev: 'https://dev.loklak.org',
		apps: 'https://apps.loklak.org'
	},
	github: {
		loklak: 'https://github.com/loklak',
		fossasia: 'https://github.com/fossasia'
	},
	phimpme: {
		root: 'https://phimp.me'
	},
	susper: {
		root: 'https://susper.com'
	},
	susiChat: {
		root: 'https://chat.susi.ai'
	},
	pslab: {
		root: 'https://pslab.fossasia.org'
	},
	eventyay: {
		root: 'https://eventyay.com'
	}
};

 

Storing URLs with a key instead of storing it inside a simple Array, makes it easier to add a new URL at required place and easy to modify the existing ones. Now, this configuration can easily be called inside any Component file.

Using config inside the component

Now, the work is really simple. We just need to import the configuration file inside the required Component and store/use directly the respective URLs.

First step would be to import the configuration file as follows:

import { defaultUrlConfig } from ‘../shared/url-config’;

 

Note: Respective path for url-config file for different components might differ.

Now, we would need to store the defaultUrlConfig inside a variable.

public configUrl = defaultUrlConfig;

 

At this point, we have all the configuration URLs which could be extracted from configUrl.

Displaying URLs in HTML

We would use Angular’s interpolation binding syntax to display the URLs from configUrl in HTML. Let’s say we want to display FOSSASIA’s github url in HTML, then we would simply need to do:

{{ configUrl.github.fossasia }}

 

This could be used as an example to to replace all the hardcoded URLs inside the project.

Deploying loklak search on Heroku

Note: I am skipping the initial steps of creating a project on heroku. It is very easy to setup a project on heroku, for initial steps please follow up here.

First step in this direction would be to add a server.js file in root directory and add the following express server code in it:

const express = require(‘express’);
const path = require(‘path’);
const app = express();
app.use(express.static(__dirname + ‘/dist/<name-of-app>’));
app.get(‘/*’, function(req,res) {
res.sendFile(path.join(__dirname+‘/dist/<name-of-app>/index.html’));
});
app.listen(process.env.PORT || 8080);

 

Second step would be to add the following commands inside package.json at respective attributes.

“postinstall”: “ng build –aot -prod”
“start”: “node server.js”

 

Testing

Click on the respective URL link inside the project UI to test the configuration for hardcoded URLs. To check the deployment on heroku, please open the following URLs:

Development branch: loklak-search-dev.herokuapp.com

Master branch: loklak-search.herokuapp.com

Resources

Continue ReadingDeploying loklak search on Heroku & Using config to store and load external URLs

Applying Private Skill as Web Bots in SUSI Chat

Along with public skills, we now have private skills as web bots! Users can create their own private skills which can be used only by them and in the chatbots deployed by them. The SUSI Server accepts parameters to identify a valid private skill and applies that private skill for a particular chat. It then executes the query and sends the response to the client. This blog explains how private skill is applied in the SUSI Chat.

Understanding the API

The API to receive response from SUSI is /susi/chat.json. For applying only the public skills, we can send a request like /susi/chat.json?q=hello. Here only one parameter “q” is involved in which we send the query. However, for a private skill, the following parameters are involved:

  • privateskill – when the client sends this parameter, it indicates to use a private skill.
  • userid – the userid of the user who has created the private skill
  • group – the group name of the private skill
  • language – the language of the private skill
  • skill – the skill name of the private skill

Thus, the four parameters userid, group, language, skill serves to uniquely identify a private skill.

Fetching the private skill

After the client sends the appropriate parameters to apply a private skill, the server must actually apply the private skill. This is done in a similar manner how persona and dream skills are applied. The First step is the fetch the private skill from the susi_private_skill_data folder.

// read the private skill
File private_skill_dir = new File(DAO.private_skill_watch_dir,userId);
File group_file = new File(private_skill_dir, group_name);
File language_file = new File(group_file, language);
skillfile = SusiSkill.getSkillFileInLanguage(language_file, skill_name, false);
String text = new String(Files.readAllBytes(skillfile.toPath()), StandardCharsets.UTF_8);

Applying the private skill

After we have fetched the private skill, the next step is to apply it. To do this, we create a SusiMind object and add it to the general minds variable. Thus the skill will be applied to the particular chat with the highest priority, since it will be the first skill to be added to the minds variable. Later, the public skills can be added to the minds variable, thus their priority will be lower than the private skill.

String text = new String(Files.readAllBytes(skillfile.toPath()), StandardCharsets.UTF_8);
// fill an empty mind with the private skill
SusiMind awakeMind = new SusiMind(DAO.susi_chatlog_dir, DAO.susi_skilllog_dir); // we need the memory directory here to get a share on the memory of previous dialoges, otherwise we cannot test call-back questions
JSONObject rules = SusiSkill.readLoTSkill(new BufferedReader(new InputStreamReader(new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8)), SusiLanguage.unknown, dream);
awakeMind.learn(rules, skillfile);
SusiSkill.ID skillid = new SusiSkill.ID(skillfile);
SusiSkill activeskill = awakeMind.getSkillMetadata().get(skillid);
awakeMind.setActiveSkill(activeskill);
// we are awake!
minds.add(awakeMind);

 

Result

Example API: http://localhost:4000/susi/chat.json?q=hi&privateskill=1&userid=17a70987d09c33e6f56fe05dca6e3d27&group=Knowledge&language=en&skill=knowprides

The skill exists in the correct location:

The skill file content is:

Thus, on sending the query “tell me” with the other parameters, we get the correct reply i.e “yes sure” from the server:

Resources

 

 

Continue ReadingApplying Private Skill as Web Bots in SUSI Chat

Using Transitions API with Email Validation in Open Event Organizer Android App

Transitions in Material Design apps provide visual continuity. As the user navigates the app, views in the app change state. Motion and transformation reinforce the idea that interfaces are tangible, connecting common elements from one view to the next.

In the Open Event Organizer Android App, we need a transition from the Get Started screen such that, if the user email is registered, we transition to the Login Screen otherwise, we Transition to the Sign Up Screen. And the transition should be such that the email field is continuously visible. One more condition is that, if the email field is even varied by one character, we need to transition back to the Get Started Screen.

To implement this, we need to use Shared Elements from the Android Transitions API.

What are shared elements?

A shared element transition determines how views that are present in two fragments transition between them. For example, an image that is displayed on an ImageView on both Fragment A and Fragment B transitions from A to B when B becomes visible.

Fade transition is used to fade a view and ChangeBounds transition is used to move a view without changing its size.

To make a transition, we use the setupTransitionAnimations() function like this:

(Note that we have created our own Fade and ChangeBounds transitions and not using XML)

   public void setupTransitionAnimation(Fragment fragment) {
       Fade exitFade = new Fade();
       exitFade.setDuration(100);
       this.setExitTransition(exitFade);
       fragment.setReturnTransition(exitFade);

       ChangeBounds changeBoundsTransition = new ChangeBounds();
       fragment.setSharedElementEnterTransition(changeBoundsTransition);

       Fade enterFade = new Fade();
       enterFade.setStartDelay(300);
       enterFade.setDuration(300);
       fragment.setEnterTransition(enterFade);
       this.setReenterTransition(enterFade);
   }

Now, in order to detect, if the email field is touched and even changed by one character, we use the TextWatcher like this:

       binding.emailLayout.getEditText().addTextChangedListener(new TextWatcher() {

           @Override
           public void beforeTextChanged(CharSequence s, int start, int count, int after) {
               //do nothing
           }

           @Override
           public void onTextChanged(CharSequence s, int start, int before, int count) {
               if (start != 0) {
                   sharedViewModel.setEmail(s.toString());
                   getFragmentManager().popBackStack();
               }
           }

           @Override
           public void afterTextChanged(Editable s) {
               //do nothing

           }

       }     

So basically, the moment the text is changed, we add the changed email to the SharedViewModel so that it can be used in the other fragments, and then to start the transition, we pop the back stack using getFragmentManager().popBackStack();

This is what the result looks like:

Resources

  • Google – Android developer blog post:

https://android-developers.googleblog.com/2018/02/continuous-shared-element-transitions.html

Continue ReadingUsing Transitions API with Email Validation in Open Event Organizer Android App

Option to Rename an Image in Phimpme Android Application

In the Phimpme Android application, users can perform various operations on images such as editing an image, sharing an image, moving the image to another folder, printing a pdf version of the image and many more. However, another important functionality that has been implemented is the option to rename an image. So in this blog post, I will be discussing how we achieved the functionality to rename an image.

Step 1

First, we need to add an option in the overflow menu to rename the image being viewed. The option to rename an image has been added by implementing the following lines of code in the  menu_view_pager.xml file.

<item
  android:id=“@+id/rename_photo”
  app:showAsAction=“never”
  android:title=“@string/Rename”/>

Step 2

Now after the user chooses the option to rename any image from the overflow menu, an alert dialog with an edittext would be displayed to the user with the old name and provide the user to add a new name for the photo. The dialog box with edittext has been implemented with the following lines of XML code.

<android.support.v7.widget.CardView
  android:layout_width=“match_parent”
  android:layout_height=“wrap_content”
  app:cardCornerRadius=“2dp”
  android:id=“@+id/description_dialog_card”>
  <ScrollView
      android:layout_width=“match_parent”
      android:layout_height=“match_parent”>
      <LinearLayout
          android:layout_width=“match_parent”
          android:layout_height=“wrap_content”
          android:orientation=“vertical”>
          <TextView
              android:id=“@+id/description_dialog_title”
              android:layout_width=“match_parent”
              android:textColor=“@color/md_dark_primary_text”
              android:layout_height=“wrap_content”
              android:background=“@color/md_dark_appbar”
              android:padding=“24dp”
              android:text=“@string/type_description”
              android:textSize=“18sp”
              android:textStyle=“bold” />
          <LinearLayout
              android:id=“@+id/rl_description_dialog”
              android:layout_width=“match_parent”
              android:layout_height=“wrap_content”
              android:orientation=“horizontal”
              android:padding=“15dp”>
              <EditText
                  android:id=“@+id/description_edittxt”
                  android:layout_width=“fill_parent”
                  android:layout_height=“wrap_content”
                  android:padding=“15dp”
                  android:hint=“@string/description_hint”
                  android:textColorHint=“@color/grey”
                  android:layout_margin=“20dp”
                  android:gravity=“top|left”
                  android:inputType=“textCapSentences|textMultiLine”
                  android:maxLength=“2000”
                  android:maxLines=“4”
                  android:selectAllOnFocus=“true”/>
              <ImageButton
                  android:layout_width=“@dimen/mic_image”
                  android:layout_height=“@dimen/mic_image”
                  android:layout_alignRight=“@+id/description_edittxt”
                  app2:srcCompat=“@drawable/ic_mic_black”
                  android:layout_gravity=“center”
                  android:background=“@color/transparent”
                  android:paddingEnd=“10dp”
                  android:paddingTop=“12dp”
                  android:id=“@+id/voice_input”/>
          </LinearLayout>
      </LinearLayout>
  </ScrollView>
</android.support.v7.widget.CardView>

The screenshot displaying the dialog with edittext is provided below.

Step 3

Now after retrieving the new name entered by the user for the photo we would extract the extension of the old name which can be .jpg, .png etc. Thereafter we’d need to create a new File object passing in the new path of the image(the path folder remains the same only the image name gets changed to a new one) as the constructor parameter. Now using the renameTo method of the File class the old image file can be renamed. However, with this rename operation the image reference would not be automatically updated in the MediaStore database. So we’d need to delete the old file path from the Android system MediaStore database which keeps a URI reference to all the media files present on the device. At last, we’d need to invoke the MediaScanner class to scan all the media files so that the new file path of the renamed image is scanned and is picked up by the MediaStore database. This can be done with the help of an action intent to initiate the media scan action. The code changes implemented to perform the above-mentioned operation is given below.

public void onClick(View dialog) {
      if (editTextNewName.length() != 0) {
          int index = file.getPath().lastIndexOf(“/”);
          String path = file.getPath().substring(0, index);
          File newname = new File(path + “/” + editTextNewName.getText().toString() + “.” +
                  imageextension);
          if(file.renameTo(newname)){
              ContentResolver resolver = getApplicationContext().getContentResolver();
              resolver.delete(
                      MediaStore.Images.Media.EXTERNAL_CONTENT_URI, MediaStore.Images.Media.DATA +
                              “=?”, new String[] { file.getAbsolutePath() });
              Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
              intent.setData(Uri.fromFile(newname));
              getApplicationContext().sendBroadcast(intent);
          }
          if(!allPhotoMode){
              int a = getAlbum().getCurrentMediaIndex();
              getAlbum().getMedia(a).setPath(newname.getPath());
          }else {
              listAll.get(current_image_pos).setPath(newname.getPath());
          }
          renameDialog.dismiss();
          SnackBarHandler.showWithBottomMargin(parentView, getString(R.string.rename_succes), navigationView
                  .getHeight());
      } else {
          SnackBarHandler.showWithBottomMargin(parentView, getString(R.string.insert_something),
                  navigationView.getHeight());
          editTextNewName.requestFocus();
      }
  }
});

This is how we have implemented the functionality to rename an image in the Phimpme Android application. To get the full source code, please refer to the Phimpme Android Github repository listed in the resource section below.

Resources

1.Android Developer documentation-https://developer.android.com/reference/java/io/File

2.Github-Phimpme Android Repository – https://github.com/fossasia/phimpme-android/

3.Renaming a file in java – http://stacktips.com/tutorials/java/how-to-delete-and-rename-a-file-in-java

 

Continue ReadingOption to Rename an Image in Phimpme Android Application

Making API for Fetching Private Skill Bot Settings from Server

SUSI Server needs to provide an API which would return the private skill’s metadata. This metadata will include the chatbot’s design and configuration settings. The skill’s meta data is being stored in the chatbot.json file in the server. This API will be called from the websites where the chatbot has been deployed. The skill’s metadata will be used for customizing the design of the chatbot. This blog explains how the API for fetching a private skill bot’s metadata is made.

Understanding the API

The API used to fetch public skill’s metadata is /cms/getSkillMetadata.json, with the following parameters:

  • Model
  • Group
  • Language
  • Skill

These parameters help to uniquely identify the skill.

The same API is used to fetch private skill’s metadata too. But the parameters changes to:

  • userid
  • Group
  • Language
  • Skill

The userid also helps to secure the identity of the user. Unlike email id, the user can be easily identified using the userid. Also it prevents from accessing other user’s chatbots without knowing their userid.

Understanding the data stored

In the chatbot.json file, the skill’s metadata are stored in the following format:

{"17a70987d09c33e6f56fe05dca6e3d27": {"Knowledge": {"en": {"test2new1": {
  "design": {
    "botIconColor": "#000000",
    "userMessageTextColor": "#ffffff",
    "botMessageBoxBackground": "#f8f8f8",
    "userMessageBoxBackground": "#0077e5",
    "botMessageTextColor": "#455a64",
    "bodyBackground": "#ffffff"
  },
  "configure": {
    "sites_enabled": "website1.com, website2.com",
    "sites_disabled": "website3.com"
  },
  "timestamp": "2018-07-20 01:04:39.571"
}}}}}

 

Thus, each entry is stored as a key-value pair. This makes the retrieval of record very efficient.

Making API to fetch private skill bot’s settings

In GetSkillMetadataService.java file, we detect if the client is sending the “userid” parameter or not. For fetching public skill’s metadata, the client will send the “model” parameter and for fetching private skill bot’ settings, the client will send the “userid” parameter. To fetch the private skill bot’s settings, we need to extract it from the chatbot.json file. We fetch the entire object for the particular skill and return it:

// fetch private skill (chatbot) meta data
JsonTray chatbot = DAO.chatbot;
JSONObject json = new JSONObject(true);
JSONObject userObject = chatbot.getJSONObject(userid);
JSONObject groupObject = userObject.getJSONObject(group);
JSONObject languageObject = groupObject.getJSONObject(language);
JSONObject skillObject = languageObject.getJSONObject(skillname);
json.put("skill_metadata", skillObject);
json.put("accepted", true);
json.put("message", "Success: Fetched Skill's Metadata");
return new ServiceResponse(json);

 

Result

Example API request: http://127.0.0.1:4000/cms/getSkillMetadata.json?userid=17a70987d09c33e6f56fe05dca6e3d27&group=Knowledge&language=en&skill=testnew

This gives the following output:

{
  "skill_metadata": {
    "design": {
      "botIconColor": "#000000",
      "userMessageTextColor": "#ffffff",
      "botMessageBoxBackground": "#f8f8f8",
      "userMessageBoxBackground": "#0077e5",
      "botMessageTextColor": "#455a64",
      "bodyBackground": "#ffffff"
    },
    "configure": {
      "sites_enabled": "website1.com, website2.com",
      "sites_disabled": "website3.com"
    },
    "timestamp": "2018-07-20 01:39:55.205"
  },
  "accepted": true,
  "message": "Success: Fetched Skill's Metadata",
  "session": {"identity": {
    "type": "host",
    "name": "127.0.0.1_af2c1fe3",
    "anonymous": true
  }}
}

 

Resources

Continue ReadingMaking API for Fetching Private Skill Bot Settings from Server

Add check-in restrictions to Open Event Organizer App

The Open Event Organizer Android App has the ability to scan and check-in attendees holding different ticket types for an event. But often there are cases when the attendees holding a particular ticket type need to be check-in restricted. It can be because of reasons such as facilitating entry of premium ticket holders before general ticket holders, or not allowing general ticket holders in a VIP queue.

To facilitate this, we have a field called ‘is-checkin-restricted’ for the entity Ticket. So when it is set to true, any check ins for the holder of that particular ticket type will be restricted. Let’s look at how this was implemented in the Orga App.

This is what we want to achieve:

Even though we needed it to be present in the settings screen, we needed it to be dynamic in nature as the types of tickets are themselves dynamic. This meant that we couldn’t achieve this using the plain old preference themes. We must create a whole new fragment for it and try to make it as similar to a preference theme as possible.

We need the following to create a dynamic tickets fragment:

  1. The fragment itself, which should implement the interfaces:  Progressive, Erroneous  to show progress and error.
  2. An Adapter and a ViewHolder
  3. A ViewModel

The fragment CheckinRestriction is similar to the TicketsFragment for the most part except for the part where we need to restrict check in. In the fragment we are providing a checkbox at the top to restrict check-in for all ticket types. So we need to setup click listeners not just for the checkbox, but for the whole view as well, like this:

binding.restrictAll.setOnClickListener(v -> {
       restrictAll(!binding.restrictAllCheckbox.isChecked());
   });
binding.restrictAllCheckbox.setOnClickListener(v -> {
       //checkbox already checked
       restrictAll(binding.restrictAllCheckbox.isChecked());
   });

The restrictAll() method restricts check-in for all ticket types by updating the view and updating the tickets using the ViewModel:

private void restrictAll(boolean toRestrict) {
   binding.restrictAllCheckbox.setChecked(toRestrict);
   ticketSettingsViewModel.updateAllTickets(toRestrict);
   ticketsAdapter.notifyDataSetChanged();
}

It’s also important to note here how we are handling the clicks in the ViewHolder for each ticket item:

public void bind(Ticket ticket) {
   binding.setTicket(ticket);
   View.OnClickListener listener = v -> {
       ticket.isCheckinRestricted = ticket.isCheckinRestricted == null || !ticket.isCheckinRestricted;
       binding.ticketCheckbox.setChecked(ticket.isCheckinRestricted);
       updateTicketAction.push(ticket);
       binding.executePendingBindings();
   };
   itemView.setOnClickListener(listener);
   binding.ticketCheckbox.setOnClickListener(listener);
}

A method that is run each time in order to check if all the tickets are restricted and then accordingly tick the ‘restrict-all’ box.

private void checkRestrictAll() {
   if (ticketSettingsViewModel.getTickets() == null) {
       return;
   }
    boolean restrictAll = true;
    for (Ticket ticket : ticketSettingsViewModel.getTickets().getValue()) {
       if (ticket.isCheckinRestricted == null || !ticket.isCheckinRestricted) {
           restrictAll = false;
           break;
       }
   }
   binding.restrictAllCheckbox.setChecked(restrictAll);
}

This is all of the code we need apart from the boilerplate code in order to successfully build a check-in-restrictions fragment.

Read more of the code here

Resources:

Continue ReadingAdd check-in restrictions to Open Event Organizer App