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:

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

Store User’s Personal Information with SUSI

In this blog, I discuss how SUSI.AI stores personal information of it’s users. This personal information is mostly about usernames/links to different websites like LinkedIn, GitHub, Facebook, Google/Gmail etc. To store such details, we have a dedicated API. Endpoint is :

https://api.susi.ai/aaa/storePersonalInfo.json

In this API/Servlet, storing the details and getting the details, both the aspects are covered. At the time of making the API call, user has an option either to ask the server for a list of available store names along with their values or request the server to store the value for a particular store name. If a store name already exists and a client makes a call with new/updated value, The servlet will update the value for that particular store name.

The reason you are looking at minimal user role as USER is quite obvious, i.e. these details correspond to a particular user. Hence neither we want someone writing such information anonymously nor we want this information to be visible to anonymous user until allowed by the user.

In the next steps, we start evaluating the API call made by the client. We look at the combination of the parameters present in the request. If the request is to fetch list of available stores, server first checks if Accounting object even has a JSONObject for “stores” or not. If not found, it sends an error message “No personal information is added yet.” and error code 420. Prior to all these steps, server first generates an accounting object for the user. If found, details are encoded as JSONObject’s parameter. Look at the code below to understand things fairly.

Accounting accounting = DAO.getAccounting(authorization.getIdentity());
        if(post.get("fetchDetails", false)) {
            if(accounting.getJSON().has("stores")){
                JSONObject jsonObject = accounting.getJSON().getJSONObject("stores");
                json.put("stores", jsonObject);
                json.put("accepted", true);
                json.put("message", "details fetched successfully.");
                return new ServiceResponse(json);
            } else {
                throw new APIException(420, "No personal information is added yet.");
            }
        }

If the request was not to fetch the list of available stores, It means client wants server to save a new field or update a previous value for that of a store name. A combination of If-else evaluates whether the call even contains required parameters.

if (post.get(“storeName”, null) == null) {
throw new APIException(422, “Bad store name encountered!”);
}

String storeName = post.get(“storeName”, null);
if (post.get(“value”, null) == null) {
throw new APIException(422, “Bad store name value encountered!”);
}

If request contains all the required data, then store name & value are extracted as key-value pair from the request.

In the next steps, since the server is expected to store list of the store names for a particular user, First the identity is gathered from the already present authorization object in “serviceImpl” method. If the server finds a “null” identity, It throws an error with error code 400 and error message “Specified User Setting not found, ensure you are logged in”.

Else, server first checks if a JSONObject with key “stores” exists or not. If not, It will create an object and will put the key value pair in the new JSONObject. Otherwise it would anyways do so.

Since these details are for a particular account (i.e. for a particular user), these are placed in the Accounting.json file. For better knowledge, Look at the code snippet below.

if (accounting.getJSON().has("stores")) {
                accounting.getJSON().getJSONObject("stores").put(storeName, value);
            } else {
                JSONObject jsonObject = new JSONObject(true);
                jsonObject.put(storeName, value);
                accounting.getJSON().put("stores", jsonObject);
            }

            json.put("accepted", true);
            json.put("message", "You successfully updated your account information!");
            return new ServiceResponse(json);

Additional Resources :

Continue ReadingStore User’s Personal Information with SUSI

Creating an Installer for PSLab Desktop App

PSLab device is made useful with applications running on two platforms. One is Android and the other one is a desktop application developed using Python frameworks. Desktop application uses half a dozen of dependent libraries and they are required to be installed prior to installing the application itself.

For someone with zero or less knowledge on how to install packages in a Linux environment, this task will be quite difficult. To ease up the process of installing the desktop application in a computer, we can use a script to run specific commands which will install the dependencies and the application.

Dependencies required by PSLab  Desktop app

  • PyQt 4.7
  • Python 2.6, 2.7 or 3.x
  • NumPy, Scipy
  • pyqt4-dev-tools
  • Pyqtgraph
  • pyopengl and qt-opengl
  • iPython-qtconsole

These dependencies can be made installed using a bash script running with root permission. A bash script will have the file extension “.sh” and a header line;

#!/bin/bash

A bash script needs to be made executable by the user himself. To do this, user needs to type a one line command in the terminal as follows and enter his password;

sudo chmod +x <Name_of_the_script>.sh

The keyword “sudo” interprets as “Super User DO” and the line follows will be executed with root permission. In other words with administrative privileges to modify system settings such as copying content to system folders.

The keyword “chmod” stands for “Change Mode” which will alter the mode of a file. In current context, the file is made executable by adding the executable property to the bash script using “+x” syntax.

Once the script is made executable, it can be executed using;

sudo ./<Name_of_the_script>.sh

An installer can be made attractive by using different colors rather than the plain old text outputs. For this purpose we can use color syntax in bash script. They are represented using ANSI escape codes and following is a list of commonly used colors;

Black        0;30     Dark Gray     1;30
Red          0;31     Light Red     1;31
Green        0;32     Light Green   1;32
Brown/Orange 0;33     Yellow        1;33
Blue         0;34     Light Blue    1;34
Purple       0;35     Light Purple  1;35
Cyan         0;36     Light Cyan    1;36
Light Gray   0;37     White         1;37

As in any programming language, rather than using the same line in many places, we can define variables in a bash script. The syntax will be the variable name followed by an equal sign with the value. There cannot be spaces around the equal sign or it will generate an error.

GREEN='\033[0;32m'

These variables can be accessed using a special syntax as follows;

${GREEN}

Finally we can output a message to the console using the “echo” command

echo -e "${GREEN}Welcome to PSLab Desktop app installer${NOCOLOR}"

Note that the keyword “-e” is used to enable interpretation of the following backslash escapes.

In order to install the packages and libraries, we use two package management tools. One is “apt” which stands for “Advanced Packaging Tool” and the second is “pip” which is used to download python related packages from “Python Package Index”. The following two lines illustrates how the two commands can be accessed.

apt-get install python-pip python-dev build-essential -y

pip install pyqtgraph

The keyword “-y” avoids the confirmation prompt in console to allow installation by pressing “Y” key every time it installs a package from “apt”.

Resources:

Continue ReadingCreating an Installer for PSLab Desktop App

Restoring State after Orientation Change in Loklak Wok Android

During orientation change i.e. from portrait to landscape mode in Android, the current activity restarts again. As the activity restarts again, all the defined variables loose their previous value, for example the scroll position of a RecyclerView, or the data in the rows of RecyclerView etc. Just imagine a user searched some tweets in Loklak Wok Android, and as the user’s phone is in “Auto rotation” mode, the orientation changes from portrait to landscape. As a result of this, the user loses the search result and has to do the search again. This leads to a bad UX.

Saving state in onSavedInstanceState

The state of the app can be saved by inserting values in a Bundle object in onSavedInstanceState callback. Inserting values is same as adding elements to a Map in Java. Methods like putDouble, putFloat, putChar etc. are used where the first parameter is a key and the second parameter is the value we want to insert.

@Override
public void onSaveInstanceState(Bundle outState) {
   if (mLatitude != null && mLongitude != null) {
       outState.putDouble(PARCELABLE_LATITUDE, mLatitude);
       outState.putDouble(PARCELABLE_LONGITUDE, mLongitude);
   }
...
}

 

The values can be retrieved back when onCreate or onCreateView of the Activity or Fragment is called. Bundle object in the callback parameter is checked, whether it is null or not, if not the values are retrieved back using the keys provided at the time of inserting. The latitude and longitude of a location in TweetPostingFragment are retrieved in the same fashion

public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
   ...
   if (savedInstanceState != null) { // checking if bundle is null
       // extracting from bundle
       mLatitude = savedInstanceState.getDouble(PARCELABLE_LATITUDE);
       mLongitude = savedInstanceState.getDouble(PARCELABLE_LONGITUDE);
       // use extracted value
   }
}

Restoring Custom Objects, using Parcelable

But what if we want to restore custom object(s). A simple option can be serializing the objects using the native Java Serialization or libraries like Gson. The problem in these cases is performance, they are quite slow. Parcelable can be used, which leads the pack in performance and moreover it is provided by Android SDK, on top of that, it is simple to use.

The objects of class which needs to be restored implements Parcelable interface and the class must provide a static final object called CREATOR which implements Parcelable.Creator interface.

writeToParcel and describeContents method need to be override to implement Parcelable interface. In writeToParcel method the member variables are put inside the parcel, in our case describeContents method is not used, so, simply 0 is returned. Status class which stores the data of a searched tweet implements parcelable.

@Override
public int describeContents() {
   return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
   dest.writeString(mText);
   dest.writeInt(mRetweetCount);
   dest.writeInt(mFavouritesCount);
   dest.writeStringList(mImages);
   dest.writeParcelable(mUser, flags);
}

 

NOTE: The order in which variables are pushed into Parcel needs to be maintained while variables are extracted from the parcel to recreate the object. This is the reason why no “key” is required to push data into a parcel as we do in bundle.

The CREATOR object implements the creation of object from a Parcel. The CREATOR object overrides two methods createFromParcel and newArray. createFromParcel is the method in which we implement the way an object is created from a parcel.

public static final Parcelable.Creator<Status> CREATOR = new Creator<Status>() {
   @Override
   public Status createFromParcel(Parcel source) {
       return new Status(source); // a private constructor to create object from parcel
   }

   @Override
   public Status[] newArray(int size) {
       return new Status[size];
   }
};

 

The private constructor, note that the order in which variables were pushed is maintained while retrieving the values.

private Status(Parcel source) {
   mText = source.readString();
   mRetweetCount = source.readInt();
   mFavouritesCount = source.readInt();
   mImages = source.createStringArrayList();
   mUser = source.readParcelable(User.class.getClassLoader());
}

 

The status objects are restored the same way, latitude and longitude were restored. putParcelableArrayList in onSaveInstaceState and getParcelableArrayList in onCreateView methods are used to push into Bundle object and retrieve from Bundle object respectively.

@Override
public void onSaveInstanceState(Bundle outState) {
   super.onSaveInstanceState(outState);
   ArrayList<Status> searchedTweets = mSearchCategoryAdapter.getStatuses();
   outState.putParcelableArrayList(PARCELABLE_SEARCHED_TWEETS, searchedTweets);
   ...
}


// retrieval of the pushed values in bundle
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                            Bundle savedInstanceState) {
   ...
   if (savedInstanceState != null) {
       ...
       List<Status> searchedTweets =
               savedInstanceState.getParcelableArrayList(PARCELABLE_SEARCHED_TWEETS);
       mSearchCategoryAdapter.setStatuses(searchedTweets);
   }
   ...
   return view;
}

Resources:

Continue ReadingRestoring State after Orientation Change in Loklak Wok Android

Testing Presenter of MVP in Loklak Wok Android

Imagine working on a large source code, and as a new developer you are not sure whether the available source code works properly or not, you are surrounded by questions like, Are all these methods invoked properly or the number of times they need to be invoked? Being new to source code and checking manually already written code is a pain. For cases like these unit-tests are written. Unit-tests check whether the implemented code works as expected or not. This blog post explains about implementation of unit-tests of Presenter in a Model-View-Presenter (MVP) architecture in Loklak Wok Android.

Adding Dependencies to project

In app/build.gradle file

defaultConfig {
   ...
   testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

dependencies {
   ...
   androidTestCompile 'org.mockito:mockito-android:2.8.47'
   androidTestCompile 'com.android.support:support-annotations:25.3.1'
   androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
}

Setup for Unit-Tests

The presenter needs a realm database and an implementation of LoklakAPI interface. Along with that a mock of the View is required, so as to check whether the methods of View are called or not.

The LoklakAPI interface can be mocked easily using Mockito, but the realm database can’t be mocked. For this reason an in-memory realm database is created, which will be destroyed once all unit-test are executed. As the presenter is required for each unit-test method we instantiate the in-memory database before all the tests start i.e. by annotating a public static method with @BeforeClass, e.g. setDb method.

@BeforeClass
public static void setDb() {
   Realm.init(InstrumentationRegistry.getContext());
   RealmConfiguration testConfig = new RealmConfiguration.Builder()
           .inMemory()
           .name("test-db")
           .build();
   mDb = Realm.getInstance(testConfig);
}

 

NOTE: The in-memory database should be closed once all unit-tests are executed. So, for closing the databasse we create a public static method annotated with @AfterClass, e.g. closeDb method.

@AfterClass
public static void closeDb() {
   mDb.close();
}

 

Now, before each unit-test is executed we need to do some setup work like instantiating the presenter, a mock instance of API interface generated  by using mock static method and pushing in some sample data into the database. Our presenter uses RxJava and RxAndroid which depend on IO scheduler and MainThread scheduler to perform tasks asynchronously and these schedulers are not present in testing environment. So, we override RxJava and RxAndroid to use trampoline scheduler in place of IO and MainThread so that our test don’t encounter NullPointerException. All this is done in a public method annotated with @Before e.g. setUp.

@Before
public void setUp() throws Exception {
   // mocking view and api
   mMockView = mock(SuggestContract.View.class);
   mApi = mock(LoklakAPI.class);

   mPresenter = new SuggestPresenter(mApi, mDb);
   mPresenter.attachView(mMockView);

   queries = getFakeQueries();
   // overriding rxjava and rxandroid
   RxJavaPlugins.setIoSchedulerHandler(scheduler -> Schedulers.trampoline());
   RxAndroidPlugins.setMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline());

   mDb.beginTransaction();
   mDb.copyToRealm(queries);
   mDb.commitTransaction();
}

 

Some fake suggestion queries are created which will be returned as observable when API interface is mocked. For this, simply two query objects are created and added to a List after their query parameter is set. This is implemented in getFakeQueries method.

private List<Query> getFakeQueries() {
   List<Query> queryList = new ArrayList<>();

   Query linux = new Query();
   linux.setQuery("linux");
   queryList.add(linux);

   Query india = new Query();
   india.setQuery("india");
   queryList.add(india);

   return queryList;
}

 

After that, a method is created which provides the created fake data wrapped inside an Observable as implemented in getFakeSuggestionsMethod method.

private Observable<SuggestData> getFakeSuggestions() {
   SuggestData suggestData = new SuggestData();
   suggestData.setQueries(queries);
   return Observable.just(suggestData);
}

 

Lastly, the mocking part is implemented using Mockito. This is really simple, when and thenReturn static methods of mockito are used for this. The method which would provide the fake data is invoked inside when and the fake data is passed as a parameter to thenReturn. For example, stubSuggestionsFromApi method

private void stubSuggestionsFromApi(Observable observable) {
   when(mApi.getSuggestions(anyString())).thenReturn(observable);
}

Finally, Unit-Tests

All the tests methods must be annotated with @Test.

Firstly, we test for a successful API request i.e. we get some suggestions from the Loklak Server. For this, getSuggestions method of LoklakAPI is mocked using stubSuggestionFromApi method and the observable to be returned is obtained using getFakeSuggestions method. Then, loadSuggestionFromAPI method is called, the one that we need to test. Once loadSuggestionFromAPI method is invoked, we then check whether the method of the View are invoked inside loadSuggestionFromAPI method, this is done using verify static method. The unit-test is implemented in testLoadSuggestionsFromApi method.

@Test
public void testLoadSuggestionsFromApi() {
   stubSuggestionsFromApi(getFakeSuggestions());

   mPresenter.loadSuggestionsFromAPI("", true);

   verify(mMockView).showProgressBar(true);
   verify(mMockView).onSuggestionFetchSuccessful(queries);
   verify(mMockView).showProgressBar(false);
}

 

Similarly, a failed network request for obtaining is suggestions is tested using testLoadSuggestionsFromApiFail method. Here, we pass an IOException throwable – wrapped inside an Observable – as parameter to stubSuggestionsFromApi.

@Test
public void testLoadSuggestionsFromApiFail() {
   Throwable throwable = new IOException();
   stubSuggestionsFromApi(Observable.error(throwable));

   mPresenter.loadSuggestionsFromAPI("", true);
   verify(mMockView).showProgressBar(true);
   verify(mMockView).showProgressBar(false);
   verify(mMockView).onSuggestionFetchError(throwable);
}

 

Lastly, we test if our suggestions are saved in the database by counting the number of saved suggestions and asserting that, in testSaveSuggestions method.

@Test
public void testSaveSuggestions() {
   mPresenter.saveSuggestions(queries);
   int count = mDb.where(Query.class).findAll().size();
  // queries is the List that contains the fake suggestions
   assertEquals(queries.size(), count);
}

Resources:

Continue ReadingTesting Presenter of MVP in Loklak Wok Android

MVP in Loklak Wok Android using Dagger2

MVP stands for Model-View-Presenter, one of the most popular and commonly used design pattern in android apps. Where “Model” refers to data source, it can be a SharedPreference, Database or data from a Network call. Going by the word, “View” is the user interface and finally “Presenter”, it’s a mediator between model and view. Whatever events occur in a view are passed to presenter and the presenter fetches the data from the model and finally passes it back to the view, where the data is populated in ViewGroups. Now, the main question, why it is so widely used? One of the obvious reason is the simplicity to implement it and it completely separates the business logic, so, easy to write unit-tests. Though it is easy to implement, its implementation requires a lot of boilerplate code, which is one of its downpoints. But, using Dagger2 the boilerplate code can be reduced to a great extent. Let’s see how Dagger2 is used in Loklak Wok Android to implement MVP architecture.

Adding Dagger2 to the project

In app/build.gradle file

dependencies {
   ...
   compile 'com.google.dagger:dagger:2.11'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.11'
}

 

Implementation

First a contract is created which defines the behaviour or say the functionality of View and Presenter. Like showing a progress bar when data is being fetched, or the view when the network request is successful or it failed. The contract should be easy to read and going by the names of the method one should be able to know the functionality of methods. For tweet search suggestions, the contract is defined in SuggestContract interface.

public interface SuggestContract {

   interface View {

       void showProgressBar(boolean show);

       void onSuggestionFetchSuccessful(List<Query> queries);

       void onSuggestionFetchError(Throwable throwable);
   }

   interface Presenter {

       void attachView(View view);

       void createCompositeDisposable();

       void loadSuggestionsFromAPI(String query, boolean showProgressBar);

       void loadSuggestionsFromDatabase();

       void saveSuggestions(List<Query> queries);

       void suggestionQueryChanged(Observable<CharSequence> observable);

       void detachView();
   }
}

 

A SuggestPresenter class is created which implements the SuggestContract.Presenter interface. I will not be explaining how each methods in SuggestPresenter class is implemented as this blog solely deals with implementing MVP. If you are interested you can go through the source code of SuggestPresenter. Similarly, the view i.e. SuggestFragment implements SuggestContract.View interface.

So, till this point we have our presenter and view ready. The presenter needs to access the model and the view requires to have an instance of presenter. One way could be instantiating an instance of model inside presenter and an instance of presenter inside view. But, this way model, view and presenter would be coupled and that defeats our purpose. So, we just INJECT model into presenter and presenter into view using Dagger2. Injecting here means Dagger2 instantiates model and presenter and provides wherever they are requested.

ApplicationModule provides the required dependencies for accessing the “Model” i.e. a Loklak API client and realm database instance. When we want Dagger2 to provide a dependency we create a method annotated with @Provides as providesLoklakAPI and providesRealm.

@Provides
LoklakAPI providesLoklakAPI(Retrofit retrofit) {
   return retrofit.create(LoklakAPI.class);
}

@Provides
Realm providesRealm() {
   return Realm.getDefaultInstance();
}

 

If we look closely providesLoklakAPI method requires a Retrofit instance i.e. a to create an instance of LoklakAPI the required dependency is Retrofit, which is fulfilled by providesRetrofit method. Always remember that whenever a dependency is required, it should not be instantiated at the required place, rather it should be injected by Dagger2.

@Provides
Retrofit providesRetrofit() {
   Gson gson = Utility.getGsonForPrivateVariableClass();
   return new Retrofit.Builder()
           .baseUrl(mBaseUrl)
           .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
           .addConverterFactory(GsonConverterFactory.create(gson))
           .build();
}

 

As the ApplicationModule class provides these dependencies the class is annotated with @Module.

@Module
public class ApplicationModule {

   private String mBaseUrl;

   public ApplicationModule(String baseUrl) {
       this.mBaseUrl = baseUrl;
   }
   
   
   // retrofit, LoklakAPI, realm @Provides methods
}


After preparing the source to provide the dependencies, it’s time we request the dependencies.

Dependencies are requested simply by using @Inject annotation e.g. in the constructor of SuggestPresenter @Inject is used, due to which Dagger2 provides instance of LoklakAPI and Realm for constructing an object of SuggestPresenter.

public class SuggestPresenter implements SuggestContract.Presenter {

   private final Realm mRealm;
   private LoklakAPI mLoklakAPI;
   private SuggestContract.View mView;
   ...

   @Inject
   public SuggestPresenter(LoklakAPI loklakAPI, Realm realm) {
       this.mLoklakAPI = loklakAPI;
       this.mRealm = realm;
       ...
   }
   
   // implementation of methods defined in contract
}


@Inject can be used on the fields also. When @Inject is used with a constructor the class also becomes a dependency provider, this way creating a method with @Provides is not required in a Module class.

Now, it’s time to connect the dependency providers and dependency requesters. This is done by creating a Component interface, here ApplicationComponent. The component interface defines where are the dependencies required. This is only for those cases where dependencies are injected by using @Inject for the member variables. So, we define a method inject with a single parameter of type SuggestFragment, as the Presenter needs to be injected in SuggestFragment.

@Component(modules = ApplicationModule.class)
public interface ApplicationComponent {


   void inject(SuggestFragment suggestFragment);

}

 

The component interface is instantiated in onCreate method of LoklakWokApplication class, so that it is accessible all over the project.

public class LoklakWokApplication extends Application {

   private ApplicationComponent mApplicationComponent;

   @Override
   public void onCreate() {
       super.onCreate();
      ...
       mApplicationComponent = DaggerApplicationComponent.builder()
               .applicationModule(new ApplicationModule(Constants.BASE_URL_LOKLAK))
               .build();
   }

   public ApplicationComponent getApplicationComponent() {
       return mApplicationComponent;
   }
   
   ...
}


NOTE: DaggerApplicationComponent is created after building the project. So, AndroidStudio will show “Cannot resolve symbol …”, thus build the project : Build > Make Module ‘app’.

Finally, in the onCreateView callback of SuggestFragment we call inject method of DaggerApplicationComponent to tell Dagger2 that SuggestFragment is requesting dependencies.

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                        Bundle savedInstanceState) {
...   
   LoklakWokApplication application = (LoklakWokApplication) getActivity().getApplication();
   application.getApplicationComponent().inject(this);
   suggestPresenter.attachView(this);

   return rootView;
}

Resources:

Continue ReadingMVP in Loklak Wok Android using Dagger2

Animations in Loklak Wok Android

Imagine an Activity popping out of nowhere suddenly in front of the user. And even more irritating, the user doesn’t even know whether a button was clicked. Though these are very small animation implementations but these animations enhance the user experience to a new level. This blog deals with the animations in Loklak Wok Android, a peer message harvester of Loklak Server.

Activity transition animation

Activity transition is applied when we move from a current activity to a new activity or just go back to an old activity by pressing back button.

In Loklak Wok Android, when user navigates for search suggestions from TweetHarvestingActivity to SuggestActivity, the new activity i.e. SuggestActivity comes from right side of the screen and the old one i.e. TweetHarvestingActivity leaves the screen through the left side. This is an example of left-right activity transition. For implementing this, two xml files which define the animations are created, enter.xml and exit.xml are created.

<set
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:shareInterpolator="false">

   <translate
       android:duration="500"
       android:fromXDelta="100%"
       android:toXDelta="0%"/>
</set>

 

NOTE: The entering activity comes from right side, that’s why android:fromXDelta parameter is set to 100% and as the activity finally stays at extreme left, android:toXDelta parameter is set to 0%.

As the current activity, in this case TweetHarvestingActivity, leaves the screen from left to the negative of left. So, in exit.xml the android:fromXDelta parameter is set to 0% and android:toXDelta parameter is set to -100%.

Now, that we are done with defining the animations in xml, it’s time we apply the animations, which is really easy. The animations are applied by invoking Activity.overridePendingTransition(enterAnim, exitAnim) just after the startActivity method. For example, in openSuggestActivity

private void openSuggestActivity() {
   Intent intent = new Intent(getActivity(), SuggestActivity.class);
   startActivity(intent);
   getActivity().overridePendingTransition(R.anim.enter, R.anim.exit);
}

 

Touch Selectors

Using touch selectors background color of a button or any clickable can be changed, this way a user can see that the clickable responded to the click. The background is usually light accent color or a lighter shade of the icon present in button.

There are three states involved while a clickable is touched, pressed, activated and selected. And a default state, i.e. the clickable is not clicked. The background color of each state is defined in a xml file like media_button_selector, which is present in drawable directory.

<selector xmlns:android="http://schemas.android.com/apk/res/android">

   <item android:drawable="@color/media_button_touch_selector_backgroud" android:state_pressed="true"/>
   <item android:drawable="@color/media_button_touch_selector_backgroud" android:state_activated="true"/>
   <item android:drawable="@color/media_button_touch_selector_backgroud" android:state_selected="true"/>

   <item android:drawable="@android:color/transparent"/>
</selector>

 

The selector is applied by setting it as the background of a clickable, for example, touch selector applied on Location image button present in fragment_tweet_posting.xml .

<ImageButton
   android:layout_width="40dp"
   android:layout_height="40dp"
   
   android:background="@drawable/media_button_selector" />

 

Notice the change in the background color of the buttons when clicked.

Resources:

Some youtube videos for getting started:

Continue ReadingAnimations in Loklak Wok Android

List all the Users Registered on SUSI.AI

In this blog, I’ll be telling on how SUSI admins can access list of all the registered users from SUSI-server. Following this, they may modify/edit user role of any registered user.

What is User Role?

A UserRole defines the servlet access right. Not all users are allowed to access all the data and services. For  example, To list all the users, minimal user role expected is ADMIN. This classification of users are inspired by the wikipedia User Access Levels, see https://en.wikipedia.org/wiki/Wikipedia:User_access_levels.While querying SUSI, Users are classified into 7 different categories, namely :

  • BOT
  • ANONYMOUS
  • USER  
  • REVIEWER
  • ACCOUNTCREATOR
  • ADMIN
  • BUREAUCRAT

* Please see that these are as of the date of publish of this blog. These are subject to change, which is very unlikely.

All the users who are not logged in but interacting with SUSI are anonymous users. These are only subject to chat with SUSI, login, signup or may use forgot password service. Once a user login to the server, a token is generated and sent back to client to maintain the identity, hence acknowledging them. Privileged users are those who have special rights with them. These are more like moderators with much special rights than any other user. At the top level of the hierarchy are the admins. These users have more rights than anyone. They can change role of any other user, override decision of any privileged user as well.

Let us now look at the control flow of this.

First things first, make a component of User List in the project. Let us name it ListUsers and since it has to be accessible by those users who possess ADMIN rights, you will find it enclosed in Admin package in components folder. Open up

index.js file, import Listusers component  and add route to it in the following way :

...//other import statements
import ListUser from "./components/Admin/ListUser/ListUser";
...//class definition and other methods
<Route path="/listUser" component={ListUser}/>
//other routes defined

Find a suitable image for “List Users” option and add the option for List Users in static appbar component along with the image. We have used Material UI’s List image in our project.

...// other imports

import List from 'material-ui/svg-icons/action/list';

Class and method definition

<MenuItem primaryText="List Users"
          onTouchTap={this.handleClose}
          containerElement={<Link to="/listUser" />}
                rightIcon={<List/>}
      />

...//other options in top right corner menu

Above code snippet will add an option to redirect admins to ‘/listUsers’ route. Let us now have a closer look at functionality of both client and server. By now you must have known what ComponentDidMount does. {If not, I’ll tell you. This is a method which is given first execution after the page is rendered. For more information, visit this link}. As mentioned earlier as well that this list will be available only for admins and may be even extended for privileged users but not for anonymous or any other user, an AJAX call is made to server in ComponentDidMount of ‘listuser’ route which returns the base user role of current user. If user is an Admin, another method, fetchUsers() is called.

let url;
        url = "http://api.susi.ai/aaa/account-permissions.json";
        $.ajax({
            url: url,
            dataType: 'jsonp',
            jsonpCallback: 'py',
            jsonp: 'callback',
            crossDomain: true,
            success: function (response) {
                console.log(response.userRole)
                if (response.userRole !== "admin") {
                    console.log("Not an admin")
                } else {
                    this.fetchUsers();
                    console.log("Admin")
                }
            }.bind(this),
});

In fetchUsers method, an AJAX call is made to server which returns username in JSONArray. The response looks something likes this :

{
	"users" : {
		"email:""test@test.com",
...
},
"Username":["test@test.com", "test@anothertest.com"...]
}

Now, only rendering this data in a systematic form is left. To give it a proper look, we have used material-ui’s table. Import Table, TableBody, TableHeader,

   TableHeaderColumn, TableRow, TableRowColumn from material-ui/table.

In fetchUsers method, response is catched in data Oblect. Now the keys are extracted from the JSON response and mapped with an array. Iterating through array received as username array, we get list of all the registered users. Now, popuulate the data in the table you generated.

return (
                        <TableRow key={i}>
                            <TableRowColumn>{++i}>
                            <TableRowColumn>{name}</TableRowColumn>
                            <TableRowColumn> </TableRowColumn>
                            <TableRowColumn> </TableRowColumn>
                            <TableRowColumn> </TableRowColumn>
                            <TableRowColumn> </TableRowColumn>
                        </TableRow>
                    )

Above piece of code may help you while populating the table. These details are returned from susi server which gets a list of all the registered in the following manner. First, it checks if base url of this user is something apart from admin. If not, it returns error which may look like this :

Failed to load resource: the server responded with a status of 401 (Base user role not sufficient. Your base user role is 'ANONYMOUS', your user role is 'anonymous')

Otherwise, it will generate a client identity, use to to get an authorization object which will loop through authorization.json file and return all the users encoded as JSONArray.

Additional Resources

  1. Official Material UI Documentation on Tables from marterial-ui
  2. Answer by Marco Bonelli on Stackoverflow on How to map JSON response in JavaScript?
  3. Answer by janpieter_z on Stackoverflow – on Render JSON data in ReactJS table
Continue ReadingList all the Users Registered on SUSI.AI

SUSI.AI User Roles and How to Modify Them

In this blog, I discuss what is ‘user-role’ in SUSI.AI, what are the various roles and how SUSI admins can modify/update a user’s roles.

What is User Role?

A UserRole defines the servlet access right. Not all users are allowed to access all the data and services. For  example, To list all the users, minimal user role expected is ADMIN. This classification of users are inspired by the wikipedia User Access Levels, see https://en.wikipedia.org/wiki/Wikipedia:User_access_levels.While querying SUSI, Users are classified into 7 different categories, namely :

  • BOT
  • ANONYMOUS
  • USER  
  • REVIEWER
  • ACCOUNTCREATOR
  • ADMIN
  • BUREAUCRAT

* Please see that these are as of the date of publish of this blog. These are subject to change, which is very unlikely.

If SUSI is active as a bot on some bot integrated platform (like line or kik), the user role assigned to it will be that of BOT. This user role just has technical access to the server.

All the users who are not logged in but interacting with SUSI are ANONYMOUS users. These are only subject to chat, login and signup. They may use forgot password service and reset password services as well.

Once a user login to the server, a token is generated and sent back to client to maintain the identity, hence acknowledging them as USER(s).

Users with role assigned as “REVIEWERS” are expected to manage the Skill CMS. There might be some dispute or conflict in a skill. REVIEWERS then take the access of skill data and finalise the conflict there itself for smooth functionality.

ADMIN users are those who have special rights with them. These are more like moderators with much special rights than any other user.

At the top level of the hierarchy are the BUREAUCRATS. These users have more rights than anyone. They can change role of any other user, override decision of any ADMIN user as well. Both admins and bureaucrats have the access to all the settings file on the server. They not only can look at the list, but also download and upload them. Now these users also have right to upgrade or downgrade any other user as well.

All these user roles are defined in UserRole.java file.

In each request received by the server, the user role of user making the request is compared with the minimal user role in getMinimalUserRole() method. This method is defined in AbstractAPIHandler which validates if a user is allowed to access a particular servlet or not.

private void process(HttpServletRequest request, HttpServletResponse response, Query query) throws ServletException, IOException {
	// object initialisation and comparsions
// user authorization: we use the identification of the user to get the assigned authorization
        Authorization authorization = DAO.getAuthorization(identity);

        if (authorization.getUserRole().ordinal() < minimalUserRole.ordinal()) {
        	response.sendError(401, "Base user role not sufficient. Your base user role is '" + authorization.getUserRole().name() + "', your user role is '" + authorization.getUserRole().getName() + "'");
			return;
        }
// evaluations based on other request parameters.
}

Now that we know about what User Roles actually are, let us look at how the servlet which allows the users {with at least ADMIN login} to change user role of some other user works.

In the request, 2 parameters are expected. These are :

  • user : email id of the user whose role has to be changed.
  • role : new role which will be assigned to this user.

Using a switch case, we identify the user role which is requested. If role is found to be null or any other value apart from “bot”, “anonymous”, “user”, “reviewer”, “accountcreator”, “admin” or “bureaucrat”, an error with error code 400 and error message “Bad User role” is thrown.

In the next steps, server generates client identity in order to get the corresponding Authorization object. If the user is not found in the database, again an error is thrown with error code 400 and error message “role not found

ClientCredential credential = new ClientCredential(ClientCredential.Type.passwd_login, userTobeUpgraded);
            ClientIdentity identity = new ClientIdentity(ClientIdentity.Type.email, credential.getName());
            if (!DAO.hasAuthorization(identity)) {
                throw new APIException(400, "Username not found");
            }

By now, server is clear with the user identity and new role to be assigned. Since the user role is defined in authorization.json file, we overwrite the existing user role and finally server sends back the new user role of the use

Authorization auth = DAO.getAuthorization(identity);
            try {
                auth.setUserRole(userRole);
            } catch (IllegalArgumentException e) {
                throw new APIException(400, "role not found");
            }

            // Print Response
            result.put("newDetails", auth.getJSON());
            result.put("accepted", true);
            result.put("message", "User role changed successfully!!");
            return new ServiceResponse(result);

 

Continue ReadingSUSI.AI User Roles and How to Modify Them

Auto Updating SUSI Android APK and App Preview on appetize.io

This blog will cover the way in which the SUSI Android APK is build automatically after each commit and pushed to “apk” branch in the github repo. Other thing which will be covered is that how the app preview on appetize.io can be updated after each commit. This is basically for the testers who wish to test the SUSI Android App. There are four ways to test the SUSI Android App. One is to simply download the alpha version of the app from the Google PlayStore. Here is the link to the app. Join the alpha testing and report bugs on the github issue tracker of the repo. Other way is to build the app from Android Studio but you may need to set the complete project. If you are looking to contribute in the project, this is the advised way to test the app. The other two ways are explained below.

Auto Building of APK and pushing to “apk” branch

We have written a script which does following steps whenever a PR is merged:

  1. Checks if the commit is of a PR or a commit to repo
  2. If not of PR, configures a user whose github account will be used to push the APKs.
  3. Clones the repo, generates the debug and release APK.
  4. Deletes everything in the apk branch.
  5. Commits and Pushes new changes to apk branch.

This script is written for people or testers who do not have android studio installed in their computer and want to test the app. So, they can directly download the apk from the apk branch and install it in their phone. The APK is always updated after each commit. So, whenever a tester downloads the APK from apk branch, he will always get the latest app.

if [[ $CIRCLE_BRANCH != pull* ]]
then
    git config --global user.name "USERNAME"
    git config --global user.email "EMAIL"

    git clone --quiet --branch=apk https://USERNAME:$GITHUB_API_KEY@github.com/fossasia/susi_android apk > /dev/null
    ls
    cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/app/build/outputs/apk/app-debug.apk apk/susi-debug.apk
    cp -r ${HOME}/${CIRCLE_PROJECT_REPONAME}/app/build/outputs/apk/app-release-unsigned.apk apk/susi-release.apk
    cd apk

    git checkout --orphan workaround
    git add -A

    git commit -am "[Circle CI] Update Susi Apk"

    git branch -D apk
    git branch -m apk

    git push origin apk --force --quiet > /dev/null
fi

Auto Updating of App Preview on appetize.io

The APKs generated in the above step can now be used to set up the preview of the app on the appetize.io. Appetize.io is an online simulator to run mobile apps ( IOS and Android). Appetize.io provides a nice virtual mobile frame to run native apps with various options like screen size, mobile, OS version, etc. Appetize.io provides some API to update/publish the app. In SUSI, we once uploaded the app on appetize.io and now we are using the API provided by them to update the APK everytime a commit is pushed in the repository.

API information (Derived from official docs of appetize.io):

You may upload a new version of an existing app, or update app settings.

Send an HTTP POST request to

https://APITOKEN@api.appetize.io/v1/apps/PUBLICKEY

Replace APITOKEN with your API token and PUBLICKEY with the public key of the app you’re updating. Your API token must be permissioned to the same account as was used to upload the app. The POST body must be a JSON object. To delete a previously set field, use a value of null.

Optional Fields

  1. url: (string) a publicly accessible link to your .zip, .tar.gz, or .apk file, used to upload a new version of your app.
  2. note: (string) a note for your own purposes, will appear on your management dashboard.

For the url parameter, we have used https://github.com/fossasia/susi_android/raw/apk/susi-debug.apk and note can be anything. We have used Update SUSI Preview.

curl https://$APPETIZE_API_TOKEN@api.appetize.io/v1/apps/mbpprq4xj92c119j7nxdhttjm0 -H 'Content-Type: application/json' -d '{"url":"https://github.com/fossasia/susi_android/raw/apk/susi-debug.apk", "note": "Update SUSI Preview"}'

Summary

This blog covered about how to implement an automatic structure to generate APKs for testing and using that APK to build a preview on websites like appetize.io and then using the APIs provided by them to update the APK after each PR merge in the repo. Check out the resources below to learn more about the topic. So, if you are thinking of contributing to SUSI Android App, this may help you a little in testing the app. But if not, then you can also use the similar technique for your android app as well and ease the life of testers.

Resources

  1. Docs of appetize.io to learn more about the API https://appetize.io/docs
  2. Tutorial on using curl to make API requests https://curl.haxx.se/docs/httpscripting.html
  3. Tutorial on writing basic shell scripts https://ryanstutorials.net/bash-scripting-tutorial/
Continue ReadingAuto Updating SUSI Android APK and App Preview on appetize.io