Implementing User Email Verification in Open Event Frontend

Open Event Server provides the functionality of user email verification after a user registers, but it was not implemented on Open Event Frontend until recently. For users, this meant they were still not able to verify themselves, even after receiving confirmation links in their inboxes, which were sent by the server. Thus, implementing it on frontend was crucial for a complete user registration workflow.

Since the server had already exposed an endpoint to perform the registration, all that was required on the frontend to be done was to make a call to this endpoint with the necessary data. The entire process can be summarized as follows:

  1. The recently registered user clicks on the verification link she receives on her email
  2. The above step opens the link, which is of the format http://fossasia.github.io/open-event-frontend/verify?token=
  3. As soon as the frontend server receives this request, it extracts the token from the URL query parameter
  4. The token is now sent to the backend server as a patch request
  5. The response of the above request confirms whether the user verification is successful or not, and an appropriate message is displayed

In the frontend code, the above algorithm is spread across 3 files: the router, verify route and verify controller. A new route named /verify was implemented for the user verification, and was registered in the project’s main router.js file. After that, in the verify route, the beforeModel() method is used to trigger the above algorithm before the page is loaded:

// in app/routes/verify.js

beforeModel(transition) {
this.controllerFor('verify').verify(transition.queryParams.token);
}

The main algorithm above is implemented in the verify controller:

// in app/controllers/verify.js
...
queryParams : ['token'],
token       : null,
success     : false,
error       : null,

verify(tokenVal) {
let payload = {
data: {
token: tokenVal
}
};
return this.get('loader')
.post('auth/verify-email', payload)
.then(() => {
this.set('success', true);
})
.catch(reason => {
this.set('error', reason);
this.set('success', false);
});
}
});

 

A template for displaying the success or failure messages to the user was also created. It uses the value of the success boolean set above to decide the message to be displayed to the user. The user registration workflow is now complete and the user sees the following message after clicking on the verification link she receives:

Screen Shot 2018-08-21 at 7.10.43 PM

Resources

Continue ReadingImplementing User Email Verification in Open Event Frontend

Scan and detect available networks in SUSI.AI Android app

We all have experienced Apple HomPad, Google Home, Alexa etc or read about smart speakers which offer interactive actions over voice commands. Smart speaker uses hotword for activation. They utilize WiFi, bluetooth and other wireless protocols.

SUSI.AI is also coming with an Open Source smart speaker using  as simple as a RaspberryPi for the speaker which can perform various actions like playing music etc over voice commands. To use SUSI Smart Speaker, you have to connect it to the SUSI iOS or Android App. You can manage your connected devices in SUSI Android,SUSI iOS and Web clients. Here we will see initial setups for connecting SUSI Smart Speaker with an Android device.

Unlike iOS, Android allows any app to change the WiFi settings of the phone if the specific permissions are added in the app and on the run time while using the app. To connect to a particular WiFi in android given that permissions are enabled a few lines of code is sufficient enough to connect to the new WiFi network, but in our case before connecting to the WiFi we must know that the SUSI.AI hotspot is available around or not.

To know if the SUSI hotspot is available or not we have to scan for the available WiFi networks present and then choose the SUSI WiFi hotspot.

The app simply detects the available WiFi networks and displays a set of them that have the name SUSI.AI. To detect the available WiFi networks we have used a BroadCastReceiver that listens for the action :

WifiManager.SCAN_RESULTS_AVAILABLE_ACTION

The broadcast receiver listens to this action and fetches the list of networks available. The broadcast receiver is registered after the objects of WifiReceiver class and WifiManager class are declared. Once the broadcast receiver is registered the object of WifiManager is used to start the scanning process of the available Wifi networks.

Here is the object of the WifiManager and WifiReceiver declared in the onCreate() method of the DeviceActivity.kt file :

mainWifi = application.getSystemService(Context.WIFI_SERVICE) as WifiManager
receiverWifi = WifiReceiver()

The broadcast receiver class that scans the available networks is as follows :

inner class WifiReceiver : BroadcastReceiver() {
  override fun onReceive(p0: Context?, p1: Intent?) {
      Timber.d(“Inside the app”)
      if (p1 != null) {
          if (p1.action.equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) {
              var wifiList: List<ScanResult> = ArrayList<ScanResult>()
              wifiList = mainWifi.getScanResults()
              devicePresenter.inflateList(wifiList)
          }

In the above code all the available networks on scanning are sent to the presenter for further processing.

In the presenter class in the method inflateList() the available WiFi networks list is scanned on the basis of their SSID and all the networks with name are “SUSI.AI” are selected and then displayed as a list to the user.

The inflateList() function is as follows :

override fun inflateList(list: List<ScanResult>) {
  Timber.d(“size “ + list.size)
  connections = ArrayList<String>()
  for (i in list.indices) {
      if (list[i].SSID.equals(utilModel.getString(R.string.device_name)))
          connections.add(list[i].SSID)
  }

  if (connections.size > 0) {
      deviceView?.setupAdapter(connections)
  } else {
      deviceView?.onDeviceConnectionError(utilModel.getString(R.string.no_device_found), utilModel.getString(R.string.setup_tut))
  }
}

Now after the wifi list is scanned the sorted and selected list is sent to the Recycler adapter that will display the list to available SUSI networks to the user.

CONCLUSION :

REFERENCES :

  1. Android Manipulation of wifi using WifiManager : https://medium.com/@josiassena/android-manipulating-wifi-using-the-wifimanager-9af77cb04c6a
  2. Scan and list all Wifi in Android : https://www.nplix.com/scan-list-wifi-network-android/
  3. Kotlin android broadcast receivers : https://www.techotopia.com/index.php/Kotlin_Android_Broadcast_Intents_and_Broadcast_Receivers

 

Continue ReadingScan and detect available networks in SUSI.AI Android app

Voltage Measurement through Channels in PSLab

The Pocket Science Lab multimeter has got three channels namely CH1,CH2 and CH3 with different ranges for measuring the voltages.This blog will give a brief description on how we measure voltages in channels.

Measuring Voltages at channels can be divided into three parts:-

        1. Communication between between device and Android.
        2. Setting up analog channel (analog constants)
        3. Voltage measuring function of android.

Communication between PSLab device and Android App

The communication between the PSLab device and  Android occurs through the help of UsbManger package of CommunicationHandler class of the app. The main two functions involved in the communication are read and write functions in which we send particular number of bytes and then we receive certain bytes.

The read function :-

public int read(byte[] dest, int bytesToBeRead, int timeoutMillis) throws IOException {
    int numBytesRead = 0;
    //synchronized (mReadBufferLock) {
    int readNow;
    Log.v(TAG, "TO read : " + bytesToBeRead);
    int bytesToBeReadTemp = bytesToBeRead;
    while (numBytesRead < bytesToBeRead) {
        readNow = mConnection.bulkTransfer(mReadEndpoint, mReadBuffer, bytesToBeReadTemp, timeoutMillis);
        if (readNow < 0) {
            Log.e(TAG, "Read Error: " + bytesToBeReadTemp);
            return numBytesRead;
        } else {
            //Log.v(TAG, "Read something" + mReadBuffer);
            System.arraycopy(mReadBuffer, 0, dest, numBytesRead, readNow);
            numBytesRead += readNow;
            bytesToBeReadTemp -= readNow;
            //Log.v(TAG, "READ : " + numBytesRead);
            //Log.v(TAG, "REMAINING: " + bytesToBeRead);
        }
    }
    //}
    Log.v("Bytes Read", "" + numBytesRead);
    return numBytesRead;
}

Similarly the write function is –

public int write(byte[] src, int timeoutMillis) throws IOException {
    if (Build.VERSION.SDK_INT < 18) {
        return writeSupportAPI(src, timeoutMillis);
    }
    int written = 0;
    while (written < src.length) {
        int writeLength, amtWritten;
        //synchronized (mWriteBufferLock) {
        writeLength = Math.min(mWriteBuffer.length, src.length - written);
        // bulk transfer supports offset from API 18
        amtWritten = mConnection.bulkTransfer(mWriteEndpoint, src, written, writeLength, timeoutMillis);
        //}
        if (amtWritten < 0) {
            throw new IOException("Error writing " + writeLength +
                " bytes at offset " + written + " length=" + src.length);
        }
        written += amtWritten;
    }
    return written;
}

Although these are the core functions used for communication but the data received through these functions are further processed using another class known as PacketHandler. In the PacketHandler class also there are two major functions i.e sendByte and getByte(), these are the main functions which are further used in other classes for communication.

The sendByte function:-

public void sendByte(int val) throws IOException {
    if (!connected) {
        throw new IOException("Device not connected");
    }
    if (!loadBurst) {
        try {
            mCommunicationHandler.write(new byte[] {
                (byte)(val & 0xff), (byte)((val >> 8) & 0xff)
            }, timeout);
        } catch (IOException e) {
            Log.e("Error in sending int", e.toString());
            e.printStackTrace();
        }
    } else {
        burstBuffer.put(new byte[] {
            (byte)(val & 0xff), (byte)((val >> 8) & 0xff)
        });
    }
}

As we can see that in this function also the main function used is the write function of communicationHandler but in this class the data is further processed.

Setting Up the Analog Constants

For setting up the ranges, gains and other properties of channels, a different class of AnalogConstants is implemented in the android app, in this class all the properties which are used by the channels are defined which are further used in the sendByte() functions for communication.

public class AnalogConstants {

    public double[] gains = {1, 2, 4, 5, 8, 10, 16, 32, 1 / 11.};
    public String[] allAnalogChannels = {"CH1", "CH2", "CH3", "MIC", "CAP", "SEN", "AN8"};
    public String[] biPolars = {"CH1", "CH2", "CH3", "MIC"};
    public Map<String, double[]> inputRanges = new HashMap<>();
    public Map<String, Integer> picADCMultiplex = new HashMap<>();

    public AnalogConstants() {

        inputRanges.put("CH1", new double[]{16.5, -16.5});
        inputRanges.put("CH2", new double[]{16.5, -16.5});
        inputRanges.put("CH3", new double[]{-3.3, 3.3});
        inputRanges.put("MIC", new double[]{-3.3, 3.3});
        inputRanges.put("CAP", new double[]{0, 3.3});
        inputRanges.put("SEN", new double[]{0, 3.3});
        inputRanges.put("AN8", new double[]{0, 3.3});

        picADCMultiplex.put("CH1", 3);
        picADCMultiplex.put("CH2", 0);
        picADCMultiplex.put("CH3", 1);
        picADCMultiplex.put("MIC", 2);
        picADCMultiplex.put("AN4", 4);
        picADCMultiplex.put("SEN", 7);
        picADCMultiplex.put("CAP", 5);
        picADCMultiplex.put("AN8", 8);

    }
}

Also in the AnalogInput sources class many other properties such as CHOSA( a variable assigned to denote the analog to decimal conversion constant of each channel) are also defined

public AnalogInputSource(String channelName) {
    AnalogConstants analogConstants = new AnalogConstants();
    this.channelName = channelName;
    range = analogConstants.inputRanges.get(channelName);
    gainValues = analogConstants.gains;
    this.CHOSA = analogConstants.picADCMultiplex.get(channelName);
    calPoly10 = new PolynomialFunction(new double[] {
        0.,
        3.3 / 1023,
        0.
    });
    calPoly12 = new PolynomialFunction(new double[] {
        0.,
        3.3 / 4095,
        0.
    });
    if (range[1] - range[0] < 0) {
        inverted = true;
        inversion = -1;
    }
    if (channelName.equals("CH1")) {
        gainEnabled = true;
        gainPGA = 1;
        gain = 0;
    } else if (channelName.equals("CH2")) {
        gainEnabled = true;
        gainPGA = 2;
        gain = 0;
    }
    gain = 0;
    regenerateCalibration();
}

Also in this constructor a polynomial function is also called which further plays an important  role in measuring voltage as it is through this polynomial function we get the voltage of channels in the science lab class , also it is also used in oscilloscope for plotting the graph . So this was the setup of analog channels.

Voltage Measuring Functions

There are two major functions for measuring voltages which are present in the scienceLab class

  • getAverageVoltage
  • getRawableVoltage

Here are the functions

private double getRawAverageVoltage(String channelName) {
    try {
        int chosa = this.calcCHOSA(channelName);
        mPacketHandler.sendByte(mCommandsProto.ADC);
        mPacketHandler.sendByte(mCommandsProto.GET_VOLTAGE_SUMMED);
        mPacketHandler.sendByte(chosa);
        int vSum = mPacketHandler.getVoltageSummation();
        mPacketHandler.getAcknowledgement();
        return vSum / 16.0;
    } catch (IOException | NullPointerException e) {
        e.printStackTrace();
        Log.e(TAG, "Error in getRawAverageVoltage");
    }
    return 0;
}

This is the major function which takes the data from the communicationHandler class via packetHandler. Further this function is used in the getAverageVoltage function.

private double getAverageVoltage(String channelName, Integer sample) {
    if (sample == null) sample = 1;
    PolynomialFunction poly;
    double sum = 0;
    poly = analogInputSources.get(channelName).calPoly12;
    ArrayList < Double > vals = new ArrayList < > ();
    for (int i = 0; i < sample; i++) {
        vals.add(getRawAverageVoltage(channelName));
    }
    for (int j = 0; j < vals.size(); j++) {
        sum = sum + poly.value(vals.get(j));
    }
    return sum / vals.size();
}

This function uses the data from the getRawableVoltage function and uses it the polynomial generated in the analog lasses to calculate the final voltage. Thus this was the core backend of calculating the voltages through channels in PSLab.

Resources:

Continue ReadingVoltage Measurement through Channels in PSLab

Display image responses in SUSI.AI Android app

SUSI.AI Android app has many response functionalities ranging from giving simple ANSWER type responses to complex TABLE and MAP type responses. Although, even after all these useful response types there were some missing action types all related to media. SUSI.AI app was not capable of playing any kind of image responses.So, to do this the links received in the response were used to fetch the corresponding image stored in the link.

Since, the app now has two build flavors corresponding to the F-Droid version and PlayStore version respectively it had to be considered that while adding the feature to display images any proprietary software was not included with the F-Droid version.

The JSON response from the server whenever a query for was asked gave the link to the image.  For eg : on querying : “Image of cat “ the server gave the response as :

Actions”: [

       {

         “language”: “en”,

         “type”: “answer”,

         “expression”: “https://pixabay.com/get/e830b1072ef5023ed1584d05fb1d4790e076e7d610ac104496f0c77ea0e9bcbf_640.jpg”

       }

So in the android app just like the usual answer type response a message was displayed with the link to the image present in the message.

Catching IMAGE response :

The image responses are of the type “answer” as seen in the server response above. So we a method had to be devised so that the responses which display the images are caught and displayed. In the file ChatFeedRecyclerAdapter.java a separate code to detect the IMAGE response was added as :

private static final int IMAGE = 17;

Next we must specify that what type of view that is to be used whenever an IMAGE response is encountered by the app. Since the action type is “answer” a specification was required to choose the type of the viewholder. Since the images are only displayed through the pixabay the URL of the images end with either “.jpg” or “.png”. So in the expression of the response if we check that it is a link and also it ends with either “.jpg” or “.png” it will be certain that the response given from the server is an image.

The code to identify the view type :

@Override
public int getItemViewType(int position) {
  ChatMessage item = getItem(position);

  if (item.getId() == -404) return DOTS;
  else if (item.getId() == -405) return NULL_HOLDER;
  else if (item.isDate()) return DATE_VIEW;
  else if (item.getContent().endsWith(“.jpg”) || item.getContent().endsWith(“.png”))
      return IMAGE;

Inflating the layout of type IMAGE

Now after determining that the response will be an image we have to inflate the layout of the viewholder to support images in the onCreateViewHolder() method . The layout  of the image response was inflated as follows :

case IMAGE:
  view = inflater.inflate(R.layout.image_holder, viewGroup, false);
  return new ImageViewHolder(view, clickListener);

Here ImageViewHolder is the view holder that is used for displaying the images , we will discuss it later in the post. Also now in the onBindViewHolder() method of the ChatFeedRecyclerAdapter.java file we have to specify the instance of the view holder if it was to support the image response. It was done as follows :

else if (holder instanceof ImageViewHolder) {
  ImageViewHolder imageViewHolder = (ImageViewHolder) holder;
  imageViewHolder.setView(getData().get(position));
}

ViewHolder for Images :

Like all other viewholders for different variety responses from the server a viewholder to display the images was also needed to be included in the app. So, ImageViewHolder.java was created which handles the images to de displayed in the app. In the setView() method ChatMessage object is passed and this object is  used to get the image url so that it is used to display the image using Picasso.

public void setView(ChatMessage model) {
  this.model = model;

  if (model != null) {
      imageURL = model.getContent();
      try {
          Picasso.with(itemView.getContext())
                  .load(imageURL)
                  .placeholder(R.drawable.ic_susi)
                  .into(imageView);
      } catch (Exception e) {
          Timber.e(e);
      }
  }

And on clicking the image we can view the image on full screen using a custom chrome tab as follows  :

imageView.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View view) {
      CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
      CustomTabsIntent customTabsIntent = builder.build();
      customTabsIntent.launchUrl(itemView.getContext(), Uri.parse(imageURL));
  }
});

Conclusion :

References :

  1. Susi server response in case of images : https://api.susi.ai/susi/chat.json?timezoneOffset=-330&q=image+of+cat
  2. Pixabay is the source used for fetching images : https://pixabay.com
  3. Display images using Picasso : https://guides.codepath.com/android/Displaying-Images-with-the-Picasso-Library

 

Continue ReadingDisplay image responses in SUSI.AI Android app

Using Multimeter in PSLab Android Application

The Pocket Science Lab as we all know is on the verge of development and new features and UI are added almost every day. One such feature is the implementation of multimeter which I have also discussed in my previous blogs.

Figure (1) : Screenshot of the multimeter

But although many functionality of multimeter such as resistance measurement are working perfectly, there are still various bugs in the multimeter. This blog is dedicated to using multimeter in the android app.

Using the multimeter

Figure (2): Screenshot showing guide of multimeter

Figure (2) shows the guide of the multimeter, i.e how basic basic elements such as resistance and voltage are measured using the multimeter. The demonstration of measuring the resistance and voltage are given below.

Measuring the resistance

The resistance is measure by connecting the SEN pin to the positive end of resistor and the GND pin to the negative end of resistor and then clicking the RESISTANCE button.

                   Figure (3) : Demonstration of resistance measurement

Measuring the voltage

To measure the voltage as said in the Guide, directly connect the power source to the the channel pins, although currently only the CH3 pin will show the most accurate results, work is going on other channel improvisation as well.

Figure (4) : Demonstration of Voltage measurement

 

And thus this is how the multimeter is used is used in the android app.  Of course there are still many many features such as capacitance measurements which are yet to be implemented and the work is going on them

Resources:

Continue ReadingUsing Multimeter in PSLab Android Application

Adding Filters for Lists of Skills on the SUSI.AI Server

In this blog post, we will learn how to add filters to the API responsible for fetching the list of skills i.e. the endpoint – https://api.susi.ai/cms/getSkillList.json. The purpose of adding filters is to return a list of skills based on some parameters associated with the skill, that would be required to allow the user to get the desired response that s/he may be using to display it on the UI.

Overview of the API

  • API to fetch the list of skills –
    • URL –  https://api.susi.ai/cms/getSkillList.json
    • It takes 5 optional parameters –
      • model – It is the name of the model that user is requesting
      • group – It is the name of the group that user is requesting
      • language – It is the name of the language that user is requesting
      • skill – It is the name of the skill that user is requesting
      • applyFilter – It has true/false values, depending whether filtering is required
    • If the request URL contains the parameter applyFilter as true, in that case the other 2 compulsory parameters are –
      • filter_name – ascending/descending, depending upon the type of sorting the user wants
      • filter_type – lexicographical, rating, etc based on what basis the filtering is going to happen

So, we will now look into adding a new filter_type to the API.

Detailed explanation of the implementation

  • We can add filters based on the key values of the Metadata object of individual skills. The Metadata object for each skill is similar to the following object –

{
  "model": "general",
  "group": "Knowledge",
  "language": "en",
  "developer_privacy_policy": null,
  "descriptions": "A skill that returns the anagrams for a word",
  "image": "images/anagrams.jpg",
  "author": "vivek iyer",
  "author_url": "https://github.com/Remorax",
  "author_email": null,
  "skill_name": "Anagrams",
  "protected": false,
  "terms_of_use": null,
  "dynamic_content": true,
  "examples": ["Anagram for best"],
  "skill_rating": {
    "negative": "0",
    "positive": "0",
    "stars": {
      "one_star": 0,
      "four_star": 0,
      "five_star": 0,
      "total_star": 0,
      "three_star": 0,
      "avg_star": 0,
      "two_star": 0
    },
    "feedback_count": 0
  },
  "creationTime": "2017-12-17T14:32:15Z",
  "lastAccessTime": "2018-06-19T17:50:01Z",
  "lastModifiedTime": "2017-12-17T14:32:15Z"
}

 

  • We will now add provision for URL parameter, filter_type=feedback in the API, which will filter the results based on the feedback_count key, which tells the number of feedback/comments a skill has received.
  • In the serviceImpl method of the ListSkillService class, we can see a code snippet that handles the filtering part, It checks the filter_type parameter received in the URL on if-else block. The code snippet looks like this –

if (filter_type.equals("date")) {
 .
 .
} else if (filter_type.equals("lexicographical")) {
 .
 .
} else if (filter_type.equals("rating")) {
 .
 .
}

 

  • Similarly, we will need to add an else if condition with feedback_type=feedback and write the code block inside it. Here is the code for it, which is explained in detail.

.
.
else if (filter_type.equals("feedback")) {
  if (filter_name.equals("ascending")) {
    Collections.sort(jsonValues, new Comparator<JSONObject>() {

      @Override
      public int compare(JSONObject a, JSONObject b) {
      Integer valA;
      Integer valB;
      int result=0;

      try {
        valA = a.getJSONObject("skill_rating").getInt("feedback_count");
        valB = b.getJSONObject("skill_rating").getInt("feedback_count");
        result = Integer.compare(valA, valB);

      } catch (JSONException e) {
        e.printStackTrace();
      }
      return result;
      }
    });
  }
  else {

    Collections.sort(jsonValues, new Comparator<JSONObject>() {

      @Override
      public int compare(JSONObject a, JSONObject b) {
      Integer valA;
      Integer valB;
      int result=0;

      try {
        valA = a.getJSONObject("skill_rating").getInt("feedback_count");
        valB = b.getJSONObject("skill_rating").getInt("feedback_count");
        result = Integer.compare(valB, valA);
      } catch (JSONException e) {
        e.printStackTrace();
      }
      return result;
    }
  });
  }
}
.
.

Working of the above code snippet

  • The first if condition checks for the filter_type the user has requested and enters the condition if it is equal to feedback
  • The next if-else handles the case of ascending and descending and sorts the list of skills accordingly.
  • The variable jsonValues passed in the Collections.sort function contains a List of JSONObject. Here, each object stands for the metadata object for a skill.
  • Since, the sorting is not a simple linear sort, the sort function is overloaded with a comparator function that specifies the key, based on which the sorting would take place.
  • The feedback_count value is stored in valA and valB for the two variables that is considered at an instance while sorting. For any other key based filtering, we need to replace the feedback_count with the desired key name.
  • The compare() method of Integer class of java.lang package compares two integer values (x, y) given as a parameter and returns the value zero if (x==y), if (x < y) then it returns a value less than zero and if (x > y) then it returns a value greater than zero.
  • The value returned to the sort function determines the order of the sorted array.
  • For, ascending and descending, the parameters of the compare function is swapped, so that we can achieve the exact opposite results from one another.

This was the implementation for the filtering of Skill List based on a key value present in the Skill Metadata object. I hope, you found the blog helpful in making the understanding of the implementation better.

Resources

Continue ReadingAdding Filters for Lists of Skills on the SUSI.AI Server

Implementing Custom Forms viewing under Attendee Details in Open Event Android App

This blog will illustrate about how order custom forms are viewed for events for which they are required in Open Event Android. These forms help the event organizer in gathering more information from the user or attendee. For example, in an event X, the organizer might want to know the state from which the user is from. Let’s head onto the code.

1. Create the CustomForm model

The custom form model is to be created as per the API.

@Type(“custom-form”)
@JsonNaming(PropertyNamingStrategy.KebabCaseStrategy::class)
data class CustomForm(
@Id(IntegerIdHandler::class)
val id: Long,
val form: String,
val fieldIdentifier: String,
val type: String,
val isRequired: Boolean? = false,
val isIncluded: Boolean? = false,
val isFixed: Boolean? = false,
val ticketsNumber: Int? = null,
@Relationship(“event”)
var event: EventId? = null
)

We can observe that there are some special fields in the model. For example, field “field identifier”, identifies the extra field of an attendee to to be taken in while making an attendee POST request. The field “isRequired” specifies whether the field is required to be taken.

2. Add the function to get custom forms from API

    @GET(“events/{id}/custom-forms”)
fun getCustomFormsForAttendees(@Path(“id”) id: Long): Single<List<CustomForm>>

3. Modify the code of Attendee Fragment

   attendeeFragmentViewModel.getCustomFormsForAttendees(eventId.id)

           attendeeFragmentViewModel.forms.observe(this, Observer {
               it?.let {
                   fillInformationSection(it)
                   if (!it.isEmpty()) {
                       rootView.moreAttendeeInformation.visibility = View.VISIBLE
                   }
               }
               rootView.register.isEnabled = true
           })

           rootView.register.setOnClickListener {
               if (selectedPaymentOption == “Stripe”)
                   sendToken()

               val attendees = ArrayList<Attendee>()
               ticketIdAndQty?.forEach {
                   for (i in 0..it.second) {
                       val attendee = Attendee(id = attendeeFragmentViewModel.getId(),
                               …
                               city = getAttendeeField(“city”),
                               address = getAttendeeField(“address”),
                               state = getAttendeeField(“state”),
                               …
                       attendees.add(attendee)
                   }
               }

2. Create two more methods

We create two more methods which help in filling the area, whether it should be a text view or image view. As we are only taking type of text, we are creating EditText for all fields!

   private fun fillInformationSection(forms: List<CustomForm>) {
val layout = rootView.attendeeInformation

for (form in forms) {
if (form.type == “text”) {
val inputLayout = TextInputLayout(context)
val editTextSection = EditText(context)
editTextSection.hint = form.fieldIdentifier.capitalize()
inputLayout.addView(editTextSection)
inputLayout.setPadding( 0, 0, 0, 20)
layout.addView(inputLayout)
identifierList.add(form.fieldIdentifier)
editTextList.add(editTextSection)
}
}
}

fun getAttendeeField(identifier: String): String {
val index = identifierList.indexOf(identifier)
return if (index == -1) “” else index.let { editTextList[it] }.text.toString()
}

Thus, we have successfully implemented Custom Forms viewing in the app.

Resources

Tags: GSoC18, FOSSASIA, Open Event, Android, Custom Forms

Continue ReadingImplementing Custom Forms viewing under Attendee Details in Open Event Android App

Adding a no search results message

The Open Event Android app allows users to search any event. The results are fetched from the server and the data is displayed in the recycler view. But, how do we handle situations when there are no results found? This is where we arrive at implementing a No search results message in the Open Event android app.

First of all, we will create a search icon vector drawable. We can make our own vector drawable in Android Studio.

Right clicking on any folder, we then select new Vector Drawable option. After that, a dialogue will open where we can select our preferred icon. In this case, we select search icon. After that, on clicking next, we successfully complete the process!

Coming back to the actual matter, we create a layout containing the message and the drawable.

We will create a LinearLayout containing an Image View and a TextView. We add this layout in fragment_search.

<LinearLayout

  android:id=“@+id/noSearchResults”

  android:layout_width=“match_parent”

  android:layout_height=“wrap_content”

  android:orientation=“vertical”

  android:gravity=“center”

  android:visibility=“gone”

  android:layout_gravity=“center”>

  <ImageView

      android:layout_width=“@dimen/item_image_view”

      android:layout_height=“@dimen/item_image_view”

      app:srcCompat=“@drawable/ic_search_grey_24dp”/>

  <TextView

      android:layout_width=“wrap_content”

      android:layout_height=“wrap_content”

      android:textSize=“@dimen/text_size_medium”

      android:text=“@string/no_search_results”/>

</LinearLayout>

As we can clearly observe, the layout will not be visible at first. We have to make the layout visible only when there are no search results found.

Heading onto the backend code. We move onto the SearchFragment.kt file. There we will handle the visibility of the layout.

searchViewModel.events.observe(this, Observer {

  it?.let {

      eventsRecyclerAdapter.addAll(it)

      eventsRecyclerAdapter.notifyDataSetChanged()

      handleVisibility(it)

  }

  Timber.d(“Fetched events of size %s”, eventsRecyclerAdapter.itemCount)

})

We observe that we have added a line of code handleVisibility(it). This is a function which will take care of events fetched. Let’s head onto the code of that function.

fun handleVisibility(events: List<Event>){

  rootView.noSearchResults.visibility = if (events.isEmpty()) View.VISIBLE else View.GONE

}

We already know the id of the layout, that is noSearchResults. So, in the above function, we are setting the visibility of the layout as visible whenever the events list is empty. This satisifes our logic completely. If it is the other case around, we set the visibility of the layout as View.GONE, which means we are making the layout completely invisible.

Thus, this is how we are able to achieve a better UX effect whenever there are no results found in the Open Event android app.

Additional Resources

Tags: GSoC18, FOSSASIA, Open Event, Android, CustomTabsIntent

Continue ReadingAdding a no search results message

Adding settings to the Open Event Android app

The Open Event Android app has a lot of fragments and some activities. But, when it comes to settings of an app, we need not create a whole new fragment, with different text views (clickable), with multiple sections. This is where a specific fragment called PreferenceFragment comes into place. This blog will illustrate about how we have implemented Settings in the Open Event android app by using a Preference Fragment.

Initially, we need to add the preference dependency in the app.gradle file.

implementation “com.takisoft.fix:preference-v7:${versions.support_lib}.0″

After that, we need to set up our settings layout. We create an xml file. Let us head into the code below.

<?xml version=“1.0” encoding=“utf-8”?>

<PreferenceScreen xmlns:android=“http://schemas.android.com/apk/res/android” >

  <PreferenceCategory

      android:title=“@string/about_settings” >

      <Preference

          android:key=“@string/key_rating”

          android:title=“@string/rating_settings” />

      <Preference

          android:key=“@string/key_suggestion”

          android:title=“@string/suggestion_settings” />

  </PreferenceCategory>

  <PreferenceCategory

      android:title=“@string/profile” >

      <Preference

          android:key=“@string/key_profile”

          android:title=“@string/logged_in_account” />

  </PreferenceCategory>

  <PreferenceCategory

      android:title=“@string/app_name_capital” >

      <Preference

          android:key=“@string/key_version”

          android:title=“@string/version_settings” />

  </PreferenceCategory>

</PreferenceScreen>

Thus a settings layout is a Preference Screen. Inside a preference screen, we can put containers like PreferenceCategories. Inside a preference category, preferences are used. A preference contains a key and a title. The key is used in the actual backend code to refer to the Preference. We can perform actions if the Preference is clicked, by referring to that key. The above layout looks exactly like the feature image!

Let us dive into the backend part. We will observe that a PreferenceFragmentCompat will have methods similar to a Fragment. For example, the method onCreatePreferencesFix is a method overriden in a PreferenceFragmentCompat. In this method, we set the layout, which the Fragment will use. We can also set text details to any view using keys in this method.

override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {

  // Load the preferences from an XML resource

  setPreferencesFromResource(R.xml.settings, rootKey)

  val activity =  activity as? AppCompatActivity

  activity?.supportActionBar?.setDisplayHomeAsUpEnabled(true)

  activity?.supportActionBar?.title = “Settings”

  setHasOptionsMenu(true)

  //Set Email

  email = arguments?.getString(EMAIL)

  preferenceScreen.findPreference(resources.getString(R.string.key_profile)).summary = email

  //Set Build Version

  preferenceScreen.findPreference(resources.getString(R.string.key_version)).title = “Version ” + BuildConfig.VERSION_NAME

}

After this, we need to implement another method called onPreferenceTreeClick. This method is useful and is helpful for us to perform any actions whenever any preference is clicked! The below code demonstrates that.

override fun onPreferenceTreeClick(preference: Preference?): Boolean {

  if (preference?.key == resources.getString(R.string.key_rating)) {

      //Opens our app in play store

      startAppPlayStore(activity?.packageName.nullToEmpty())

      return true

  }

  if (preference?.key == resources.getString(R.string.key_suggestion)) {

      //Links to suggestion form

      context?.let {

          Utils.openUrl(it, FORM_LINK)

      }

      return true

  }

  if (preference?.key == resources.getString(R.string.key_profile)) {

      //Logout Dialog shown

      showDialog()

      return true

  }

  return false

}

Thus, we have our SettingsFragment. But, it’s not totally complete. We need to add a preference theme in styles.xml. Let us observe the code.

<style name=“AppTheme” parent=“@style/Theme.Preference”>

  <!– Customize your theme here. –>

  <item name=“colorPrimary”>@color/colorPrimary</item>

  <item name=“colorPrimaryDark”>@color/colorPrimaryDark</item>

  <item name=“colorAccent”>@color/colorAccent</item>

</style>

<style name=“Theme.Preference” parent=“@style/PreferenceFixTheme.Light.DarkActionBar”>

  <item name=“preferenceCategory_marginBottom”>0dp</item>

  <item name=“android:dividerHeight”>1dp</item>

</style>

So we see that we are changing the parent theme of AppTheme to Theme.Preference. The theme Theme.Preference is stated below the AppTheme. It has a parent theme of PreferenceFixTheme.Light.DarkActionBar. Thus all preference themes need to have a parent of the form PreferenceFixTheme. (…). This is how the Settings screen is implemented in the Open Event android app

Additional Resources

Tags: GSoC18, FOSSASIA, Open Event, Android, Settings

Continue ReadingAdding settings to the Open Event Android app