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.
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.
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:
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';exportdefault 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 settingfrom 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.
_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:
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 AndroidPreferenceAPIsto 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:
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 eachPreference 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"?><PreferenceScreenxmlns:android="http://schemas.android.com/apk/res/android"><EditTextPreferenceandroid: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"/><EditTextPreferenceandroid: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"/><CheckBoxPreferenceandroid: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:
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.
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.
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.
@OverridepublicbooleanonOptionsItemSelected(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
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.
Step 5 Implementing listener for change in Preferences item
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.
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:
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:
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.
Let us go over the code of MapContainer component step by step.
Firstly, the componentDidUpdate() function calls the loadMap function to load the google map.
componentDidUpdate(){this.loadMap();}
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}
Then we set the prop value to google, and maps to google maps props. This is done as follows:
const{google}=this.props;constmaps=google.maps;
Then we look for HTML div ref ‘map’ in the React DOM and name it ‘node’. This is done as follows:
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.
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){letobj=[];// Extract information from the response and store them in obj objectobj.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.
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 propstableData={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:
The 4 important functions which are passed as props to the Table component are:
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.
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=>{letdata=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.
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.
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=>{letdata=this.state.obj;letmacid=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.
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.
Show US if the state does not deines the country code
onChange={this.handleCountryChange}>
{countries}
</DropDownMenu><Translatetext="Phone number : "/><TextFieldname="selectedCountry"disabled={true}value={countryData.countries[this.state.countryCode?this.state.countryCode:'US'].countryCallingCodes[0]}/><TextFieldname="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){ return1};
return0;
});
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} )
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
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.
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.
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.
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
} elseif senderTag ==1 {
key = ControllerConstants.UserDefaultsKeys.micInput
} elseif senderTag ==2 {
key = ControllerConstants.UserDefaultsKeys.hotwordEnabled
} elseif senderTag ==3 {
key = ControllerConstants.UserDefaultsKeys.speechOutput
} elseif senderTag ==4 {
key = ControllerConstants.UserDefaultsKeys.speechOutputAlwaysOn
} elseif senderTag ==5 {
key = ControllerConstants.UserDefaultsKeys.speechRate
} elseif 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)
}
}
}
}
}
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:
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’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:
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.
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
User Defaults: Apple’s docs listing various methods for using User defaults
You must be logged in to post a comment.