Setting up SUSI Desktop Locally for Development and Using Webview Tag and Adding Event Listeners

SUSI Desktop is a cross platform desktop application based on electron which presently uses chat.susi.ai as a submodule and allows the users to interact with susi right from their desktop.

Any electron app essentially comprises of the following components

    • Main Process (Managing windows and other interactions with the operating system)
    • Renderer Process (Manage the view inside the BrowserWindow)

Steps to setup development environment

      • Clone the repo locally.
$ git clone https://github.com/fossasia/susi_desktop.git
$ cd susi_desktop
      • Install the dependencies listed in package.json file.
$ npm install
      • Start the app using the start script.
$ npm start

Structure of the project

The project was restructured to ensure that the working environment of the Main and Renderer processes are separate which makes the codebase easier to read and debug, this is how the current project is structured.

The root directory of the project contains another directory ‘app’ which contains our electron application. Then we have a package.json which contains the information about the project and the modules required for building the project and then there are other github helper files.

Inside the app directory-

  • Main – Files for managing the main process of the app
  • Renderer – Files for managing the renderer process of the app
  • Resources – Icons for the app and the tray/media files
  • Webview Tag

    Display external web content in an isolated frame and process, this is used to load chat.susi.ai in a BrowserWindow as

    <webview src="https://chat.susi.ai/"></webview>
    

    Adding event listeners to the app

    Various electron APIs were used to give a native feel to the application.

  • Send focus to the window WebContents on focussing the app window.
  • win.on('focus', () => {
    	win.webContents.send('focus');
    });
    
  • Display the window only once the DOM has completely loaded.
  • const page = mainWindow.webContents;
    ...
    page.on('dom-ready', () => {
    	mainWindow.show();
    });
    
  • Display the window on ‘ready-to-show’ event
  • win.once('ready-to-show', () => {
    	win.show();
    });
    

    Resources

    1. A quick article to understand electron’s main and renderer process by Cameron Nokes at Medium link
    2. Official documentation about the webview tag at https://electron.atom.io/docs/api/webview-tag/
    3. Read more about electron processes at https://electronjs.org/docs/glossary#process
    4. SUSI Desktop repository at https://github.com/fossasia/susi_desktop.

    Installing Query Server Search and Adding Search Engines

    The query server can be used to search a keyword/phrase on a search engine (Google, Yahoo, Bing, Ask, DuckDuckGo and Yandex) and get the results as json or xml. The tool also stores the searched query string in a MongoDB database for analytical purposes. (The search engine scraper is based on the scraper at fossasia/searss.)

    In this blog, we will talk about how to install Query-Server and implement the search engine of your own choice as an enhancement.

    How to clone the repository

    Sign up / Login to GitHub and head over to the Query-Server repository. Then follow these steps.

    1. Go ahead and fork the repository

    https://github.com/fossasia/query-server

    2. Star the repository

    3. Get the clone of the forked version on your local machine using

    git clone https://github.com/<username>/query-server.git

    4. Add upstream to synchronize repository using

    git remote add upstream https://github.com/fossasia/query-server.git

    Getting Started

    The Query-Server application basically consists of the following :

    1. Installing Node.js dependencies

    npm install -g bower
    
    bower install

    2. Installing Python dependencies (Python 2.7 and 3.4+)

    pip install -r requirements.txt

    3. Setting up MongoDB server

    sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
    
    echo "deb http://repo.mongodb.org/apt/ubuntu "$(lsb_release   -sc)"/mongodb-org/3.0   multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.0.list
    
    sudo apt-get update
    
    sudo apt-get install -y mongodb-org
    
    sudo service mongod start

    4. Now, run the query server:

    python app/server.py

    Go to http://localhost:7001/

    How to contribute :

    Add a search engine of your own choice

    You can add a search engine of your choice apart from the existing ones in application.

    • Just add or edit 4 files and you are ready to go.

    For adding a search engine ( say Exalead ) :

    1. Add exalead.py in app/scrapers directory :

    from __future__ import print_function
    
    from generalized import Scraper
    
    
    class Exalead(Scraper): # Exalead class inheriting Scraper
    
        """Scrapper class for Exalead"""
    
    
        def __init__(self):
    
           self.url = 'https://www.exalead.com/search/web/results/'
    
           self.defaultStart = 0
    
           self.startKey = ‘start_index’
    
    
        def parseResponse(self, soup):
    
           """ Parses the reponse and return set of urls
    
           Returns: urls (list)
    
                   [[Tile1,url1], [Title2, url2],..]
    
           """
    
           urls = []
    
           for a in soup.findAll('a', {'class': 'title'}): # Scrap data with the corresponding tag
    
               url_entry = {'title': a.getText(), 'link': a.get('href')}
    
               urls.append(url_entry)
    
    
           return urls

    Here, scraping data depends on the tag / class from where we could find the respective link and the title of the webpage.

    2. Edit generalized.py in app/scrapers directory

    from __future__ import print_function
    
    import json
    
    import sys
    
    from google import Google
    
    from duckduckgo import Duckduckgo
    
    from bing import Bing
    
    from yahoo import Yahoo
    
    from ask import Ask
    
    from yandex import Yandex
    
    from exalead import Exalead   # import exalead.py
    
    
    
    scrapers = {
    
        'g': Google(),
    
        'b': Bing(),
    
        'y': Yahoo(),
    
        'd': Duckduckgo(),
    
        'a': Ask(),
    
        'yd': Yandex(),
    
        't': Exalead() # Add exalead to scrapers with index ‘t’
    
    }

    From the scrapers dictionary, we could find which search engines had supported the project.

    3. Edit server.py in app directory

    @app.route('/api/v1/search/<search_engine>', methods=['GET'])
    
    def search(search_engine):
    
        try:
    
           num = request.args.get('num') or 10
    
           count = int(num)
    
           qformat = request.args.get('format') or 'json'
    
           if qformat not in ('json', 'xml'):
    
               abort(400, 'Not Found - undefined format')
    
    
           engine = search_engine
    
           if engine not in ('google', 'bing', 'duckduckgo', 'yahoo', 'ask', ‘yandex' ‘exalead’): # Add exalead to the tuple
    
               err = [404, 'Incorrect search engine', qformat]
    
               return bad_request(err)
    
    
           query = request.args.get('query')
    
           if not query:
    
               err = [400, 'Not Found - missing query', qformat]
    
               return bad_request(err)
    
    

    Checking, if the passed search engine is supporting the project, or not.

    4.  Edit index.html in app/templates directory

         <button type="submit" value="ask" class="btn btn-lg  search btn-outline"><img src="{{ url_for('static', filename='images/ask_icon.ico') }}" width="30px" alt="Ask Icon"> Ask</button>
    
         <button type="submit" value="yandex" class="btn btn-lg  search btn-outline"><img src="{{ url_for('static', filename='images/yandex_icon.png') }}" width="30px" alt="Yandex Icon"> Yandex</button>
    
         <button type="submit" value="exalead" class="btn btn-lg  search btn-outline"><img src="{{ url_for('static', filename='images/exalead_icon.png') }}" width="30px" alt="Exalead Icon"> Exalead</button> # Add button for exalead
    
    • In a nutshell,

    Scrape the data using the anchor tag having specific class name.

    For example, searching fossasia using exalead

    https://www.exalead.com/search/web/results/?q=fossasia&start_index=1

    Here, after inspecting element for the links, you will find that anchor having class name as title is having the link and title of the webpage. So, scrap data using title classed anchor tag.

    Similarly, you can add other search engines as well.

    Resources

    UI automated testing using Selenium in Badgeyay

    With all the major functionalities packed into the badgeyay web application, it was time to add some automation testing to automate the review process in case of known errors and check if code contribution by contributors is not breaking anything. We decided to go with Selenium for our testing requirements.

    What is Selenium?

    Selenium is a portable software-testing framework for web applications. Selenium provides a playback (formerly also recording) tool for authoring tests without the need to learn a test scripting language. In other words, Selenium does browser automation:, Selenium tells a browser to click some element, populate and submit a form, navigate to a page and any other form of user interaction.

    Selenium supports multiple languages including C#, Groovy, Java, Perl, PHP, Python, Ruby and Scala. Here, we are going to use Python (and specifically python 2.7).

    First things first:
    To install these package run this code on the CLI:

    pip install selenium==2.40
    pip install nose
    

    Don’t forget to add them in the requirements.txt file

    Web Browser:
    We also need to have Firefox installed on your machine.

    Writing the Test
    An automated test automates what you’d do via manual testing – but it is done by the computer. This frees up time and allows you to do other things, as well as repeat your testing. The test code is going to run a series of instructions to interact with a web browser – mimicking how an actual end user would interact with an application. The script is going to navigate the browser, click a button, enter some text input, click a radio button, select a drop down, drag and drop, etc. In short, the code tests the functionality of the web application.

    A test for the web page title:

    import unittest
    from selenium import webdriver
    
    class SampleTest(unittest.TestCase):
    
        @classmethod
        def setUpClass(cls):
            cls.driver = webdriver.Firefox()
            cls.driver.get('http://badgeyay-dev.herokuapp.com/')
    
        def test_title(self):
            self.assertEqual(self.driver.title, 'Badgeyay')
    
        @classmethod
        def tearDownClass(cls):
            cls.driver.quit()
    

     

    Run the test using nose test.py

    Clicking the element
    For our next test, we click the menu button, and check if the menu becomes visible.

    elem = self.driver.find_element_by_css_selector(".custom-menu-content")
    self.driver.find_element_by_css_selector(".glyphicon-th").click()
    self.assertTrue(elem.is_displayed())
    

     

    Uploading a CSV file:
    For our next test, we upload a CSV file and see if a success message pops up.

    def test_upload(self):
            Imagepath = os.path.abspath(os.path.join(os.getcwd(), 'badges/badge_1.png'))
            CSVpath = os.path.abspath(os.path.join(os.getcwd(), 'sample/vip.png.csv'))
            self.driver.find_element_by_name("file").send_keys(CSVpath)
            self.driver.find_element_by_name("image").send_keys(Imagepath)
            self.driver.find_element_by_css_selector("form .btn-primary").click()
            time.sleep(3)
            success = self.driver.find_element_by_css_selector(".flash-success")
            self.assertIn(u'Your badges has been successfully generated!', success.text)
    

     

    The entire code can be found on: https://github.com/fossasia/badgeyay/tree/development/app/tests

    We can also use the Phantom.js package along with Selenium for UI testing purposes without opening a web browser. We use this for badgeyay to run the tests for every commit in Travis CI which cannot open a program window.

    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:

    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 :

    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:

    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:

    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:

    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:

    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: