How webclient Settings are Implemented in SUSI.AI Accounts

The goal of accounts.susi is to implement all settings from different clients at a single place where user can change their android, iOS or webclient settings altogether and changes should be implemented directly on the susi server and hence the respective clients. This post focuses on how webclient settings are implemented on accounts.susi. SUSI.AI follows the accounting model across all clients and every settings of a user are stores at a single place in the accounting model itself. These are stored in a json object format and can be fetched directly from the ListUserSettings.json endpoint of the server. Continue reading How webclient Settings are Implemented in SUSI.AI Accounts

How User preferences data is stored in Chat.susi.ai using Accounting Model

Like any other accounting services SUSI.AI also provides a lot of account preferences. Users can select their language, timezone, themes, speech settings etc. This data helps users to customize their experience when using SUSI.AI.

In the web client these user preferences are fetch from the server by UserPreferencesStore.js and the user identity is fetched by UserIdentityStore.js. These settings are then exported to the settings.react.js file. This file is responsible for the settings page and takes care of all user settings. Whenever a user changes a setting, it identifies the changes and show an option to save these changes. These changes are then updated on the server using the accounting model of SUSI.AI. Let’s take a look at each file discussed above in detail. Continue reading How User preferences data is stored in Chat.susi.ai using Accounting Model

Open Event Frontend – Settings Service

This blog illustrates how the settings of a particular user are obtained in the Open Event Frontend web app. To access the settings of the user a service has been created which fetches the settings from the endpoint provided by Open Event Server.

Let’s see why a special service was created for this.

Problem

In the first step of the event creation wizard, the user has the option to link Paypal or Stripe to accept payments. The option to accept payment through Paypal or Stripe was shown to the user without checking if it was enabled by the admin in his settings. To solve this problem, we needed to access the settings of the admin and check for the required conditions. But since queryRecord() returned a promise we had to re-render the page for the effect to show which resulted in this code:

canAcceptPayPal: computed('data.event.paymentCurrency', function() {     this.get('store').queryRecord('setting', {}) .then(setting => { this.set('canAcceptPayPal', (setting.paypalSandboxUsername || setting.paypalLiveUsername) && find(paymentCurrencies, ['code', this.get('data.event.paymentCurrency')]).paypal); this.rerender(); });

This code was setting a computed property inside it and then re-rendering which is bad programming and can result in weird bugs.

Solution

The above problem was solved by creating a service for settings. This made sense as settings would be required at other places as well. The file was called settings.js and was placed in the services folder. Let me walk you through its code.

  • Extend the default Service provided by Ember.js and initialize store, session, authManager and _lastPromise.
import Service, { inject as service } from '@ember/service';
import { observer } from '@ember/object';

export default Service.extend({

 store       : service(),
 session     : service(),
 authManager : service(),

_lastPromise: Promise.resolve(),
  • The main method which fetches results from the server is called _loadSettings(). It is an async method. It queries setting from the server and then iterates through every attribute of the setting model and stores the corresponding value from the fetched result.
/**
* Load the settings from the API and set the attributes as properties on the service
*
* @return {Promise<void>}
* @private
*/
async _loadSettings() {
 const settingsModel = await this.get('store').queryRecord('setting', {});
 this.get('store').modelFor('setting').eachAttribute(attributeName => {
   this.set(attributeName, settingsModel.get(attributeName));
 });
},
  • The initialization of the settings service is handled by initialize(). This method returns a promise.
/**
* Initialize the settings service
* @return {*|Promise<void>}
*/
initialize() {
 const promise = this._loadSettings();
 this.set('_lastPromise', promise);
 return promise;
}
  • _authenticationObserver observes for changes in authentication changes and reloads the settings as required.
/**
* Reload settings when the authentication state changes.
*/
_authenticationObserver: observer('session.isAuthenticated', function() {
 this.get('_lastPromise')
   .then(() => this.set('_lastPromise', this._loadSettings()))
   .catch(() => this.set('_lastPromise', this._loadSettings()));
}),

The service we created can be directly used in the app to fetch the settings for the user. To solve the Paypal and Stripe payment problem described above, we use it as follows:

canAcceptPayPal: computed('data.event.paymentCurrency', 'settings.paypalSandboxUsername', 'settings.paypalLiveUsername', function() {
 return (this.get('settings.paypalSandboxUsername') || this.get('settings.paypalLiveUsername')) && find(paymentCurrencies, ['code', this.get('data.event.paymentCurrency')]).paypal;
}),

canAcceptStripe: computed('data.event.paymentCurrency', 'settings.stripeClientId', function() {
 return this.get('settings.stripeClientId') && find(paymentCurrencies, ['code', this.get('data.event.paymentCurrency')]).stripe;
}),

Thus, there is no need to re-render the page and dangerously set the property inside its computed method.

References

Implementing Settings for Lux Meter Instrument in PSLab Android App

In PSLab android app, we have included sensor instruments which use either inbuilt sensor of smartphone or external sensor to record sensor data. For example, Lux Meter uses the light sensor to record lux data but the problem is that these instruments doesn’t contain settings option to configure the sensor record-setting like which sensor to use, change the update period etc.

Therefore, we need to create a settings page for the Lux Meter instrument which allows the user to modify the properties of the sensor instrument. For that I will use Android Preference APIs to build a settings interface that is similar to setting activity in any other android app.

The main building block of the settings activity is the Preference object. Each preference appears as a single setting item in an activity and it corresponds to key-value pair which stores the settings in default Shared Preferences file. Every time any setting is changed by the user the Android will store the updated value of the setting in the default shared preferences which we can read in any other activity across the app.

In the following steps I will describe instruction on how to create the setting interface in Android app: 

Step1 Include the dependency

First, we need to include the dependency for v7 Preference Support Library by including the following code:

dependencies { 
compile fileTree(dir: 'libs', include: ['*.jar']) 
compile 'com.android.support:appcompat-v7:23.1.1' 
compile 'com.android.support:design:23.1.1' 
compile 'com.android.support:preference-v7:23.1.1' 
}

 Step 2 Creating the preferences screen in XML

For this step, I have created a preference screen which will be inflated when the settings fragment is being created.

I created a file named  “lux_meter_settings.xml” and place it in the res/xml/ directory.

The root node for the XML file must be a <PreferenceScreen> element. We have to add each Preference within this element. Each child I added within the <PreferenceScreen> element appears as a single item in the list of settings.

The preference which I have used are:

<EdittextPreference> This preference opens up a dialog box with edit text and stores whatever value is written by the user in the edit text. I have used this preference for inputting update period and high limit of data during recording.

<CheckboxPreference> shows an item with a checkbox for a setting that is either enabled or disabled. The saved value is a boolean (true if it’s checked). I have used this preference for enabling or disabling location data with the recorded data.

<ListPreference> opens a dialog with a list of radio buttons. The saved value can be any one of the supported value types. I have used this preference to allow the user to choose between multiple sensor types.

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
       <EditTextPreference
           android:key="setting_lux_update_period"
           android:title="@string/update_period"
           android:dialogTitle="@string/update_period"
           android:defaultValue="1000"
           android:dialogMessage="Please provide time interval(in ms) at which data will be updated"
           android:summary="Update period is 900ms"/>

       <EditTextPreference
           android:key="setting_lux_high_limit"
           android:title="High Limit"
           android:dialogTitle="High Limit"
           android:defaultValue="2000"
           android:dialogMessage="Please provide maximum limit of LUX value to be recorded"
           android:summary="High Limit is 2000 Lux"/>

       <CheckBoxPreference
           android:defaultValue="false"
           android:key="include_location_sensor_data"
           android:summary="Include the location data in the logged file"
           android:title="Include Location Data" />
</PreferenceScreen>

The above XML file will produce a layout as shown in Figure 1 below:

Figure 1 shows a preview of settings layout in Android Studio

 

Step 3 Implementing the backend of setting interface

As android Documentation clearly states:

As your app’s settings UI is built using Preference objects instead of View objects, you need to use a specialized Activity or Fragment subclass to display the list settings:

  • If your app supports versions of Android older than 3.0 (API level 10 and lower), you must build the activity as an extension of the PreferenceActivity class.
  • On Android 3.0 and later, you should instead use a traditional Activity that hosts a PreferenceFragment that displays your app settings.

Therefore, I first created an Activity named “SettingsActivity” which will act as a host for a Fragment. This gist contains code for SettingsActivity which I defined in the PSLab android app which will show the Setting Fragment.

Now, for setting fragment I have created a new fragment and name it “LuxMeterSettingsFragment” and make that fragment class extends the PreferenceFragmentCompat class and for which I needed to add this import statement.

import android.support.v7.preference.PreferenceFragmentCompat;

And then inside the SettingsFragment, I overridden the following method like this:

@Override
public void onCreatePreferences(Bundle savedInstanceState,
                                String rootKey) {
    setPreferencesFromResource(R.xml.lux_meter_settings, rootKey);
}

This method is called when the Android is creating the Preferences that is when we need to call the method ‘setPreferencesFromResource()’ and pass the resource file in which we have defined our preferences along with the root key which comes as a parameter. Here, the Android will create preferences referred by the key which we have provided and initialize it to the default value in the default SharedPreferences file.

Step 4 Providing setting option to navigate to setting activity

First I included a setting option in the menu file which is getting inflated in the Lux Meter Activity toolbar as shown in below code.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto">
  
   ...

   <item
       android:id="@+id/settings"
       android:title="@string/lux_meter_settings"
       app:showAsAction="never" />
</menu>

Then, heading over to  Lux Meter Activity in the onOptionItemSelected() method I added below code in which I created intent to open Setting Activity when the setting option is selected.

@Override
public boolean onOptionsItemSelected(MenuItem item) {
case R.id.settings:
   Intent settingIntent = new Intent(this, SettingsActivity.class);
   settingIntent.putExtra("title", "Lux Meter Settings");
   startActivity(settingIntent);
   break;
}

After this step, we can see the settings option in Lux Meter Activity as shown in Figure 2

Figure 2 shows the setting option in the overflow menu

 

To see if its working opens the app -> open Lux Meter Instrument-> Open Overflow Menu -> click on Lux Meter Setting Option to open settings.

Here we can the Lux Meter Setting as shown by the Figure 3.

Figure 3 shows the screenshot of the Lux Meter Setting activity on the actual device

 

Step 5 Implementing listener for change in Preferences item

Now we can also implement a listener by making SettingsFragment class implement SharedPreference.OnSharedPreferenceChangeListener interface and then overriding the onSharedPreferenceChanged() method.

Now, to register this listener with the Preference Screen which we will do it in the onResume() method of the fragment and deregister the listener in the onPause() method to correspond with the lifecycle of the fragment.

@Override
public void onResume() {
   super.onResume();
      
getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onPause() {
   super.onPause();
        getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
}

Thus we have successfully implemented settings for the Lux Meter Instrument.

Resources

  1. Adding Settings to an App – Google Developer Fundamental Course Article on how to add settings.
  2. Gist – Setting Activity implementation – The gist used for setting activity in the app

Implementing Map View in Devices Tab in Settings

The Table View implemented in the Devices tab in settings on SUSI.AI Web Client has a column “geolocation” which displays the latitudinal and longitudinal coordinates of the device. These coordinates needed to be displayed on a map. Hence, we needed a Map View apart from the Table View, dedicated to displaying the devices pinpointed on a map. This blog post explains how this feature has been implemented on the SUSI.AI Web Client.

Modifying the fetched data of devices to suitable format

We already have the fetched data of devices which is being used for the Table View. We need to extract the geolocation data and store it in a different suitable format to be able to use it for the Map View. The required format is as follows:

[
   {
      "location":{
         "lat": latitude1,
         "lng": longitude2
      }
   },
   {
      "location":{
         "lat": latitude1,
         "lng": longitude2
      }
   }
]

 

To modify the fetched data of devices to this format, we modify the apiCall() function to facilitate extraction of the geolocation info of each device and store them in an object, namely ‘mapObj’. Also, we needed variables to store the latitude and longitude to use as the center for the map. ‘centerLat’ and ‘centerLng’ variables store the average of all the latitudes and longitudes respectively. The following code was added to the apiCall() function to facilitate all the above requirements:

let mapObj = [];
let locationData = {
  lat: parseFloat(response.devices[i].geolocation.latitude),
  lng: parseFloat(response.devices[i].geolocation.longitude),
};
centerLat += parseFloat(response.devices[i].geolocation.latitude);
centerLng += parseFloat(response.devices[i].geolocation.longitude);
let location = {
  location: locationData,
};
mapObj.push(location);
centerLat = centerLat / mapObj.length;
centerLng = centerLng / mapObj.length;
if (mapObj.length) {
  this.setState({
    mapObj: mapObj,
    centerLat: centerLat,
    centerLng: centerLng,
  });
}

 

The following code was added in the return function of Settings.react.js file to use the Map component. All the modified data is passed as props to this component.

<MapContainer
  google={this.props.google}
  mapData={this.state.mapObj}
  centerLat={this.state.centerLat}
  centerLng={this.state.centerLng}
  devicenames={this.state.devicenames}
  rooms={this.state.rooms}
  macids={this.state.macids}
/>

 

The implementation of the MapContainer component is as follows:

 componentDidUpdate() {
    this.loadMap();
  }

  loadMap() {
    if (this.props && this.props.google) {
      const {google} = this.props;
      const maps = google.maps;
      const mapRef = this.refs.map;
      const node = ReactDOM.findDOMNode(mapRef);

      const mapConfig = Object.assign({}, 
        
          center: { lat: this.props.centerLat, lng: this.props.centerLng },
          zoom: 2,
        }
      )
      this.map = new maps.Map(node, mapConfig);
    }
  }

 

Let us go over the code of MapContainer component step by step.

  1. Firstly, the componentDidUpdate() function calls the loadMap function to load the google map.

  componentDidUpdate() {
    this.loadMap();
  }

 

  1. In the loadMap() function, we first check whether props have been passed to the MapContainer component. This is done by enclosing all contents of loadMap function inside an if statement as follows:

  if (this.props && this.props.google) {
    // All code of loadMap() function
  } 

 

  1. Then we set the prop value to google, and maps to google maps props. This is done as follows:

  const {google} = this.props;
  const maps = google.maps;

 

  1. Then we look for HTML div ref ‘map’ in the React DOM and name it ‘node’. This is done as follows:

  const mapRef = this.refs.map;
  const node = ReactDOM.findDOMNode(mapRef);

 

  1. Then we set the center and the default zoom level of the map using the props we provided to the MapContainer component.

  {
    center: { lat: this.props.centerLat, lng: this.props.centerLng },
    zoom: 2,
  }

 

  1. Then we create a new Google map on the specified node (ref=’map’) with the specified configuration set above. This is done as follows:

this.map = new maps.Map(node, mapConfig);

 

  1. In the render function of the MapContainer component, we return a div with a ref ‘map’ as follows:

 render() {
    return (
      <div ref="map" style={style}>
        loading map...
      </div>
    );
  }

 

This is how the Map View has been implemented in the Devices tab in Settings on SUSI.AI Web Client.

Resources

Implementing Table View in Devices Tab in Settings

We can connect to the SUSI.AI Smart Speaker using our mobile apps (Android or iOS). But there needs to be something implemented which can tell you what all devices are linked to your account. This is in consistency with the way how Google Home devices and Amazon Alexa devices have this feature implemented in their respective apps, which allow you to see the list of devices connected to your account. This blog post explains how this feature has been implemented on the SUSI.AI Web Client.

Fetching data of the connected devices from the server

The information of the devices connected to an account is stored in the Accounting object of that user. This is a part of a sample Accounting object of a user who has 2 devices linked to his/her account. This is the data that we wish to fetch. This data is accessible at the /aaa/listUserSettings.json endpoint.

{
  "devices": {
    "37-AE-5F-7B-CA-3F": {
      "name": "Device 1",
      "room": "Room 1",
      "geolocation": {
        "latitude": "50.34567",
        "longitude": "60.34567"
      }
    },
    "9D-39-02-01-EB-95": {
      "name": "Device 2",
      "room": "Room 2",
      "geolocation": {
        "latitude": "52.34567",
        "longitude": "62.34567"
      }
    }
  }
} 

 

In the Settings.react.js file, we make an AJAX call immediately after the component is mounted on the DOM. This AJAX call is made to the /aaa/listUserSettings.json endpoint. The received response of the AJAX call is then used and traversed to store the information of each connected device in a format that would be more suitable to use as a prop for the table.

apiCall = () => {
    $.ajax({
      url: BASE_URL + '/aaa/listUserSettings.json?' + 'access_token=' +
  cookies.get('loggedIn');,
      type: 'GET',
      dataType: 'jsonp',
      crossDomain: true,
      timeout: 3000,
      async: false,
      success: function(response) {
        let obj = [];
         // Extract information from the response and store them in obj object
        obj.push(myObj);
        this.setState({
          dataFetched: true,
          obj: obj,
        });
      }
      }.bind(this),
  };

 

This is how the extraction of keys takes place inside the apiCall() function. We first extract the keys of the ‘devices’ JSONObject inside the response. The keys of this JSONObject are the Mac Addresses of the individual devices. Then we traverse inside the JSONObject corresponding to each Mac Address and store the name of the device, room and also the geolocation information of the device in separate variables, and then finally push all this information inside an object, namely ‘myObj’. This JSONObject is then pushed to a JSONArray, namely ‘obj’. Then a setState() function is called which sets the value of ‘obj’ to the updated ‘obj’ variable.

let keys = Object.keys(response.devices);
keys.forEach(i => {
    let myObj = {
        macid: i,
        devicename: response.devices[i].name,
        room: response.devices[i].room,
        latitude: response.devices[i].geolocation.latitude,
        longitude: response.devices[i].geolocation.longitude,
    };
}

 

This way we fetch the information of devices and store them in a variable named ‘obj’. This variable will now serve as the data for the table which we want to create.

Creating table from this data

The data is then passed on to the Table component as a prop in the Settings.react.js file.

<TableComplex
    // Other props
    tableData={this.state.obj}
/>

 

The other props passed to the Table component are the functions for handling the editing and deleting of rows, and also for handling the changes in the textfield when the edit mode is on. These props are as follows:

<TableComplex
    startEditing={this.startEditing}
    stopEditing={this.stopEditing}
    handleChange={this.handleChange}
    handleRemove={this.handleRemove}
/> 

 

The 4 important functions which are passed as props to the Table component are:

  1. startEditing() function:

When the edit icon is clicked for any row, then the edit mode should be enabled for that row. This also changes the edit icon to a check icon. The columns corresponding to the room and the name of the device should turn into text fields to enable the user to edit them. The implementation of this function is as follows:

startEditing = i => {
    this.setState({ editIdx: i });
}; 

 

‘editIdx’ is a variable which contains the row index for which the edit mode is currently on. This information is passed to the Table component which then handles the editing of the row.

  1. stopEditing() function:

When the check icon is clicked for any row, then the edit mode should be disabled for that row and the updated data should be stored on the server. The columns corresponding to the room and the name of the device should turn back into label texts. The implementation of this function is as follows:

stopEditing = i => {
  let data = this.state.obj;
  this.setState({ editIdx: -1 });
  // AJAX call to server to store the updated data
  $.ajax({
    url:
      BASE_URL + '/aaa/addNewDevice.json?macid=' + macid + '&name=' + devicename + '&room=' + room + '&latitude=' + latitude + '&longitude=' + longitude + '&access_token=' + cookies.get('loggedIn'),
    dataType: 'jsonp',
    crossDomain: true,
    timeout: 3000,
    async: false,
    success: function(response) {
      console.log(response);
    },
  });
};

 

The value of ‘editIdx’ is also set to -1 as the editing mode is now off for all rows.

  1. handleChange() function:

When the edit mode is on for any row, if we change the value of any text field, then that updated value is stored so that we can edit it without the value getting reset to the initial value of the field.

handleChange = (e, name, i) => {
  const value = e.target.value;
  let data = this.state.obj;
  this.setState({
    obj: data.map((row, j) => (j === i ? { ...row, [name]: value } : row)),
  });
};

 

  1. handleRemove() function:

When the delete icon is clicked for any row, then that row should get deleted. This change should be reflected to us on the site, as well as on the server. Hence, The implementation of this function is as follows:

handleRemove = i => {
  let data = this.state.obj;
  let macid = data[i].macid;

  this.setState({
    obj: data.filter((row, j) => j !== i),
  });
  // AJAX call to server to delete the info of device with specified Mac Address
  $.ajax({
    url:
      BASE_URL + '/aaa/removeUserDevices.json?macid=' + macid + '&access_token=' + cookies.get('loggedIn'),
    dataType: 'jsonp',
    crossDomain: true,
    timeout: 3000,
    async: false,
    success: function(response) {
      console.log(response);
    },
  });
};  

 

This is how the Table View has been implemented in the Devices tab in Settings on SUSI.AI Web Client.

Resources

 

How to Store Mobile Settings in the Server from SUSI Web Chat Settings Page

While we are adding new features and capabilities to SUSI Web Chat application, we wanted to provide settings changing capability to SUSI users. SUSI team decided to maintain a settings page to give that capability to users.

This is how it’s interface looks like now.

In this blog post I’m going to add another setting category to our setting page. This one is for  saving mobile phone number and dial code in the server.

UI Development:

First we need to  add new category to settings page and it should be invisible when user is not logged in. Anonymous users should not get mobile phone category in settings page.

     let menuItems = cookies.get('loggedIn') ?
            <div>
                <div className="settings-list">
                    <Menu
                        onItemTouchTap={this.loadSettings}
                        selectedMenuItemStyle={blueThemeColor}
                        style={{ width: '100%' }}
                        value={this.state.selectedSetting}
                    >
                       <MenuItem value='Mobile' className="setting-item" leftIcon={<MobileIcon />}>Mobile<ChevronRight className="right-chevron" /></MenuItem>
                        <hr className="break-line" />
                    </Menu>
                </div>
            </div>

 

Next we have to show settings UI when user clicks on the category name.

 if (this.state.selectedSetting === 'Mobile' && cookies.get('loggedIn')) {}
                currentSetting = (
  <Translate text="Country/region : " />
                            <DropDownMenu maxHeight={300}
               value={this.state.countryCode?this.state.countryCode:'US'}

 

Show US if the state does not deines the country code

                                onChange={this.handleCountryChange}>
                                {countries}
                            </DropDownMenu>
<Translate text="Phone number : " />
                            <TextField name="selectedCountry"
                            disabled={true}
                            value={countryData.countries[this.state.countryCode?this.state.countryCode:'US'].countryCallingCodes[0] }
                         	/>
                            <TextField name="serverUrl"
                                onChange={this.handleTelephoneNoChange}
                                value={this.state.phoneNo }
 />
)}

 

Then we need to get list of country names and country dial codes to show in the above drop down. We used country-data node module for that.

To install country-data module use this  command.

npm install --save country-data

 

We have used it in the settings page as below.

import countryData from 'country-data';
    	countryData.countries.all.sort(function(a, b) {
            if(a.name < b.name){ return -1};
            if(a.name > b.name){ return 1};
            return 0;
        });
        let countries = countryData.countries.all.map((country, i) => {
         	return (<MenuItem value={countryData.countries.all[i].alpha2} key={i} primaryText={ countryData.countries.all[i].name+' '+ countryData.countries.all[i].countryCallingCodes[0] } />);
        });

 

First we sort the country data list from it’s name. After that we made a list of “”s from this list of data.
Then we have to check whether the user changed or added the phone number and region (dial code).
It handles by this function mentioned above. ( onChange={this.handleCountryChange}> and
onChange={this.handleTelephoneNoChange} )

    handleCountryChange = (event, index, value) => {
        this.setState({'countryCode': value });
    }

 

Then we have to get the phone number using below function.

    handleTelephoneNoChange = (event, value) => {
        this.setState({'phoneNo': value});
    }

 

Next we have to update the function that triggers when user clicks the save button.

    handleSubmit = () => {
        let newCountryCode = !this.state.countryCode?
        this.intialSettings.countryCode:this.state.countryCode;
        let newCountryDialCode = !this.state.countryDialCode?
        this.intialSettings.countryDialCode:this.state.countryDialCode;
        let newPhoneNo = this.state.phoneNo;
        let vals = {
            countryCode: newCountryCode,
            countryDialCode: newCountryDialCode,
            phoneNo: newPhoneNo
}
let settings = Object.assign({}, vals);
cookies.set('settings', settings);
 this.implementSettings(vals);
 }

 

This code snippet stores Country Code, Country Dial code and phone no in the server.
Now we have to update the Store. Here we are going to change UserPreferencesStore “UserPreferencesStore” .
First we have to setup default values for things we are going to store.

let _defaults = {
	  CountryCode: 'US',
   	  CountryDialCode: '+1',
   	  PhoneNo: ''
}

 

Finally we have to update the dispatchToken to change and get these new data

UserPreferencesStore.dispatchToken = ChatAppDispatcher.register(action => {
   switch (action.type) {
       case ActionTypes.SETTINGS_CHANGED: {
           let settings = action.settings;
           if(settings.hasOwnProperty('theme')){
                   _defaults.Theme = settings.theme;
           }
           if(settings.hasOwnProperty('countryDialCode')){
               _defaults.countryDialCode = settings.countryDialCode;
           }
           if(settings.hasOwnProperty('phoneNo')){
               _defaults.phoneNo = settings.phoneNo;
           }
           if(settings.hasOwnProperty('countryCode')){
               _defaults.countryCode = settings.countryCode;
           }
           UserPreferencesStore.emitChange();
           break;
}
}

 

Finally application is ready to store and update Mobile phone number and region code in the server.

Resources:

Creating Settings Screen in SUSI Android Using PreferenceActivity and Kotlin

An Android application often includes settings that allow the user to modify features of the app. For example, SUSI Android app allows users to specify whether they want to use in built mic to give speech input or not. Different settings in SUSI Android app and their purpose are given below

Setting                                        Purpose
Enter As Send It allows users to specify whether they want to use enter key to send message or to add new line.
Mic Input It allows users to specify whether they want to use in built mic to give speech input or not.
Speech Always It allows users to specify whether they want voice output in case of speech input or not.
Speech Output It allows users to specify whether they want speech output irrespective of input type or not.
Language It allows users to set different query language.
Reset Password It allows users to change password.
Select Server It allows users to specify whether they want to use custom server or not.

Android provides a powerful framework, Preference framework, that allows us to define the way we want preferences. In this blog post, I will show you how Settings UI is created using Preference framework and Kotlin in SUSI Android.

Advantages of using Preference are:

  • It has own UI so we don‘t have to develop our own UI for it
  • It stores the string into the SharedPreferences so we don’t need to manage the values in SharedPreference.

First, we will add the dependency in build.gradle(project) file as shown below.

compile ‘com.takisoft.fix:preference-v7:25.4.0.3’

To create the custom style for our Settings Activity screen we can set

android:theme=“@style/PreferencesThemeLight”

as the base theme and can apply various other modifications and colour over this. By default, it has the usual Day and Night theme with NoActionBar extension.

Layout Design

I used PreferenceScreen as the main container to create UI of Settings and filled it with the other components. Different components used are following:

  • SwitchPreferenceCompat: This gives us the Switch Preference which we can use to toggle between two different modes in the setting.
<com.takisoft.fix.support.v7.preference.SwitchPreferenceCompat

android:defaultValue=”true”

  • PreferenceCategory: It is used for grouping the preference. For example, Chat Settings, Mic Settings, Speech Settings etc are different groups in settings.

  • ListPreference: This preference display list of values and help in selecting one. For example in setLanguage option ListPreference is used to show a list of query language. List of query language is provided via xml file array.xml (res/values). Attribute android:entries point to arrays languagentries and android:entryValue holds the corresponding value defined for each of the languages.
<ListPreference

android:title=“@string/Language”
android:key=“Lang_Select”

android:entries=“@array/languagentries”

android:entryValues=“@array/languagentry”

</ListPreference>

Implementation in SUSI Android

All the logic related to Preferences and their action is written in ChatSettingsFragment class. ChatSettingsFragment extends PreferenceFragmentCompat class.

class ChatSettingsFragment : PreferenceFragmentCompat()

Fragment populate the preferences when created. addPreferencesFromResource method is used to inflate view from xml.

addPreferencesFromResource(R.xml.pref_settings)

Reference

Settings Controller UI using Static Table View

Dynamic Table Views are used at places where there may be any kind of reusability of cells. This means that there would exist cells that would have the same UI elements but would differ in the content being displayed. Initially the Settings Controller was built using UICollectionViewController which is completely dynamic but later I realized that the cells will remain static every time so there is no use of dynamic cells to display the UI hence, I switched to static table view cells. Using Static Table View is very easy. In this blog, I will explain how the implementation of the same was achieved in SUSI iOS app.

Let’s start by dragging and dropping a UITableViewController into the storyboard file.

The initial configuration of the UITableView has content as Dynamic Prototypes but we need Static cells so we choose them and make the sections count to 5 to suit our need. Also, to make the UI better, we choose the style as Grouped.

Now for each section, we have the control of statically adding UI elements so, we add all the settings with their corresponding section headers and obtain the following UI.

       

After creating this UI, we can refer any UI element independently be it in any of the cells. So here we create references to each of the UISlider and UISwitch so that we can trigger an action whenever the value of anyone of them changes to get their present state.

To create an action, simply create a function and add `@IBAction` in front so that they can be linked with the UI elements in the storyboard and then click and drag the circle next to the function to UI element it needs to be added. After successful linking, hovering over the same circle would reveal all the UI elements which trigger that function. Below is a method with the @IBAction identifier indicating it can be linked with the UI elements in the storyboard. This method is executed whenever any slider or switch value changes, which then updates the UserDefaults value as well sends an API request to update the setting for the user on the server.

@IBAction func settingChanged(sender: AnyObject?) {
        var params = [String: AnyObject]()
        var key: String = ""

        if let senderTag = sender?.tag {
            if senderTag == 0 {
                key = ControllerConstants.UserDefaultsKeys.enterToSend
            } else if senderTag == 1 {
                key = ControllerConstants.UserDefaultsKeys.micInput
            } else if senderTag == 2 {
                key = ControllerConstants.UserDefaultsKeys.hotwordEnabled
            } else if senderTag == 3 {
                key = ControllerConstants.UserDefaultsKeys.speechOutput
            } else if senderTag == 4 {
                key = ControllerConstants.UserDefaultsKeys.speechOutputAlwaysOn
            } else if senderTag == 5 {
                key = ControllerConstants.UserDefaultsKeys.speechRate
            } else if senderTag == 6 {
                key = ControllerConstants.UserDefaultsKeys.speechPitch
            }

            if let slider = sender as? UISlider {
                UserDefaults.standard.set(slider.value, forKey: key)
            } else {
                UserDefaults.standard.set(!UserDefaults.standard.bool(forKey: key), forKey: key)
            }

            params[ControllerConstants.key] = key as AnyObject
            params[ControllerConstants.value] = UserDefaults.standard.bool(forKey: key) as AnyObject

            if let delegate = UIApplication.shared.delegate as? AppDelegate, let user = delegate.currentUser {
                params[Client.UserKeys.AccessToken] = user.accessToken as AnyObject
                params[ControllerConstants.count] = 1 as AnyObject

                Client.sharedInstance.changeUserSettings(params) { (_, message) in
                    DispatchQueue.main.async {
                        self.view.makeToast(message)
                    }
                }
            }
        }
    }

References

How to Store and Retrieve User Settings from SUSI Server in SUSI iOS

Any user using the SUSI iOS client can set preferences like enabling or disabling the hot word recognition or enabling input from the microphone. These settings need to be stored, in order to be used across all platforms such as web, Android or iOS. Now, in order to store these settings and maintain a synchronization between all the clients, we make use of the SUSI server. The server provides an endpoint to retrieve these settings when the user logs in.

First, we will focus on storing settings on the server followed by retrieving settings from the server. The endpoint to store settings is as follows:

http://api.susi.ai/aaa/changeUserSettings.json?key=key&value=value&access_token=ACCESS_TOKEN

This takes the key value pair for storing a settings and an access token to identify the user as parameters in the GET request. Let’s start by creating the method that takes input the params, calls the API to store settings and returns a status specifying if the executed successfully or not.

 let url = getApiUrl(UserDefaults.standard.object(forKey: ControllerConstants.UserDefaultsKeys.ipAddress) as! String, Methods.UserSettings)

        _ = makeRequest(url, .get, [:], parameters: params, completion: { (results, message) in
            if let _ = message {
                completion(false, ResponseMessages.ServerError)
            } else if let results = results {
                guard let response = results as? [String : AnyObject] else {
                    completion(false, ResponseMessages.ServerError)
                    return
                }
                if let accepted = response[ControllerConstants.accepted] as? Bool, let message = response[Client.UserKeys.Message] as? String {
                    if accepted {
                        completion(true, message)
                        return
                    }
                    completion(false, message)
                    return
                }
            }
        })

Let’s understand this function line by line. First we generate the URL by supplying the server address and the method. Then, we pass the URL and the params in the `makeRequest` method which has a completion handler returning a results object and an error object. Inside the completion handler, check for any error, if it exists mark the request completed with an error else check for the results object to be a dictionary and a key `accepted`, if this key is `true` our request executed successfully and we mark the request to be executed successfully and finally return the method. After making this method, it needs to be called in the view controller, we do so by the following code.

Client.sharedInstance.changeUserSettings(params) { (_, message) in
  DispatchQueue.global().async {
    self.view.makeToast(message)
  }
}

The code above takes input params containing the user token and key-value pair for the setting that needs to be stored. This request runs on a background thread and displays a toast message with the result of the request.

Now that the settings have been stored on the server, we need to retrieve these settings every time the user logs in the app. Below is the endpoint for the same:

http://api.susi.ai/aaa/listUserSettings.json?access_token=ACCESS_TOKEN

This endpoint accepts the user token which is generated when the user logs in which is used to uniquely identify the user and his/her settings are returned. Let’s create the method that would call this endpoint and parse and save the settings data in the iOS app’s User Defaults.

if let _ = message {
  completion(false, ResponseMessages.ServerError)
} else if let results = results {
  guard let response = results as? [String : AnyObject] else {
    completion(false, ResponseMessages.ServerError)
    return
  }
  guard let settings = 
response[ControllerConstants.Settings.settings.lowercased()] as? [String:String] else {
    completion(false, ResponseMessages.ServerError)
    return
  }
  for (key, value) in settings {
    if value.toBool() != nil {
      UserDefaults.standard.set(value.toBool()!, forKey: key)
    } else {
      UserDefaults.standard.set(value, forKey: key)
    }
  }
  completion(true, response[Client.UserKeys.Message] as? String ?? "error")
}

Here, the creation of the URL is same as we created above the only difference being the method passed. We parse the settings key value into a dictionary followed by a loop which loop’s through all the keys and stores the value in the User Defaults for that key. We simply call this method just after user log in as follows:

Client.sharedInstance.fetchUserSettings(params as [String : AnyObject]) { (success, message) in
  DispatchQueue.global().async {
    print("User settings fetch status: \(success) : \(message)")
  }
}

That’s all for this tutorial where we learned how to store and retrieve settings on the SUSI Server.

References