Attendee details in the Open Event Android App

To be able to create an order we first need to create an attendee with whom we can associate an order. Let’s see how in Open Event Android App we are creating an attendee.

We are loading the event details from our local database using the id variable. Since only logged in users can create an attendee, if the user is not logged in then the user is redirected to the login screen. If any errors are encountered while creating an attendee then they are shown in a toast message to the user. When the user clicks on the register button a POST request is sent to the server with the necessary details of the attendee. In the POST request we are passing an attendee object which has the id, first name, last name and email of the attendee. The ticket id and event id is also sent.

attendeeFragmentViewModel.loadEvent(id)

if (!attendeeFragmentViewModel.isLoggedIn()) {
redirectToLogin()
Toast.makeText(context, "You need to log in first!", Toast.LENGTH_LONG).show()
}

 

attendeeFragmentViewModel.message.observe(this, Observer {
Toast.makeText(context, it, Toast.LENGTH_LONG).show()
})

attendeeFragmentViewModel.progress.observe(this, Observer {
it?.let { Utils.showProgressBar(rootView.progressBarAttendee, it) }
})

 

attendeeFragmentViewModel.event.observe(this, Observer {
it?.let { loadEventDetails(it) }
})

rootView.register.setOnClickListener {
val attendee = Attendee(id = attendeeFragmentViewModel.getId(),
firstname = firstName.text.toString(),
lastname = lastName.text.toString(),
email = email.text.toString(),
ticket = ticketId,
event = eventId)

attendeeFragmentViewModel.createAttendee(attendee)

 

We are using a method called loadEvent in the above code which is defined in the View Model let’s have a look. We are throwing an IllegalStateException if the id is equal to -1 because this should never happen. Then we are fetching the event from the database in a background thread. If we face any errors while fetching the event we report it to the user

 

fun loadEvent(id: Long) {
if (id.equals(-1)) {
throw IllegalStateException("ID should never be -1")
}
compositeDisposable.add(eventService.getEvent(id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
event.value = it
}, {
Timber.e(it, "Error fetching event %d", id)
message.value = "Error fetching event"
}))
}

 

This method is used to create an attendee. We are checking if the user has filled all the fields if any of the fields is empty a toast message is shown. Then we send a POST request to the server in a background thread. The progress bar starts loading as soon as the request is made and then finally when the attendee has been created successfully, the progress bar stops loading and a success message is shown to the user. If we face any errors while creating an attendee, an error message is shown to the user.

fun createAttendee(attendee: Attendee) {
if (attendee.email.isNullOrEmpty() || attendee.firstname.isNullOrEmpty() || attendee.lastname.isNullOrEmpty()) {
message.value = "Please fill all the fields"
return
}

compositeDisposable.add(attendeeService.postAttendee(attendee)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe {
progress.value = true
}.doFinally {
progress.value = false
}.subscribe({
message.value = "Attendee created successfully!"
Timber.d("Success!")
}, {
message.value = "Unable to create Attendee!"
Timber.d(it, "Failed")
}))
}

 

This function sends a POST request to the server and stores the attendee details in the local database.

fun postAttendee(attendee: Attendee): Single<Attendee> {
return attendeeApi.postAttendee(attendee)
.map {
attendeeDao.insertAttendee(it)
it
}

 

This is how the attendee details are inserted into the local database. In case of a conflict the attendee object gets replaced.

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAttendee(attendee: Attendee)

 

Resources

  1. ReactiveX official documentation : http://reactivex.io/
  2. Vogella RxJava 2 – Tutorial : http://www.vogella.com/tutorials/RxJava/article.html
  3. Androidhive RxJava Tutorial : https://www.androidhive.info/RxJava/
Continue ReadingAttendee details in the Open Event Android App

Making Bottomsheet responsive using Custom Gesture Detector in PSLab Android App

In the previous blog Creating Instruction Guide using Bottomsheet, I have created the Bottom Sheet guide in instrument activities in PSLab Android app. But simply adding the Bottom Sheet in the layout is not enough as it could lead to some UI issues like no proper way to show or hide the Bottom Sheet, therefore, he/she will find it difficult to work with Bottom Sheet that could degrade User Experience.

We need to make the Bottom Sheet responsive and interactive which we can do by capturing swipe gestures done by the user and overriding their functionality i.e. when the user slides up with the finger then the Bottom Sheet will reveal itself and when the user slides the finger down the Bottom Sheet will hide.

For this Android provides a class GestureDetector which is used with another class SimpleOnGestureListener which acts as a listener to capture Gesture events like swipe, pinch, scroll, long press etc.

In this blog, I will create a custom gesture listener that will listen to the swipe events and according to the gestures it will show/hide the Bottom Sheet.

I will start by creating a gesture listener class called “SwipeGestureListener” extending the class ‘GestureDetector.SimpleOnGestureListener’ and also as I need swipe gestures to control the Bottom Sheet, so I will pass the reference of the Bottom Sheet as a parameter in the constructor.

public class SwipeGestureListener extends GestureDetector.SimpleOnGestureListener{
   private  BottomSheetBehavior bottomSheet;

   public SwipeGestureDetector(BottomSheetBehavior bt) {
       bottomSheet = bt;
   }  
}

Now in this listener class as we are concerned with the swipe events so will only override the below method provided by ‘GestureDetector.SimpleOnGestureListener’ interface

public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)

This method is called whenever the user swipes its finger in any direction.

In the above code, we can see that the method provides with object e1 and e2 of type MotionEventThe MotionEvent class is used to report movements in terms of Action Codes like ACTION_DOWN, ACTION_UP and also contains other information about the touch like the pressure of the touch, x and y coordinate, orientation of the contact area etc. 

The e1 object will have the attribute values relating to the point when the swipe started and the e2 object will have attribute values relating to the point when the swipe has ended.

Now, the main thing we need to determine if the direction of the swipe which is not directly available using the MotionEvent object.

So, to determine the direction of the swipe I will fetch the coordinates of the initial point and terminal point of the swipe using the objects initial and final point i.e., e1 and e2.

//Initial Point
float x1 = e1.getX(), y1 = e1.getY();

//Final Point
float x2 = e2.getX(), y2 = e2.getY();

Then, using these coordinates to calculate the angle of the swipe and based on the angle I will return the direction of the swipe as shown in the code below

private Direction getDirection(float x1, float y1, float x2, float y2) {

       Double angle = Math.toDegrees(Math.atan2(y1 - y2, x2 - x1));

       if (angle > 45 && angle <= 135)
           return Direction.TOP;
       if (angle >= 135 && angle < 180 || angle < -135 && angle > -180)
           return Direction.LEFT;
       if (angle < -45 && angle>= -135)
           return Direction.DOWN;
       if (angle > -45 && angle <= 45)
           return Direction.RIGHT;

       return null;     // required by java to avoid error
   }

As of now, I have the direction of the swipe so I will apply switch case and handle the swipe up and swipe down gesture as below:

  1. When the user slides up:-  Show the Bottom Sheet by changing the state of the Bottom Sheet from STATE_HIDDEN to STATE_COLLAPSED(partially viewable).
                                          
  2. When the user slides down: – Hide the Bottom Sheet by changing the state of the Bottom Sheet to STATE_HIDDEN.

For doing this, we will modify the onFIing()’ method as shown below

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
   switch (getDirection(e1.getX(), e1.getY(), e2.getX(), e2.getY())) {
       case TOP:
           bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED);
           return true;
       case LEFT:
           return true;
       case DOWN:
           if(bottomSheet.getState()==BottomSheetBehavior.STATE_COLLAPSED){
               bottomSheet.setState(BottomSheetBehavior.STATE_HIDDEN);
           }
           return true;
       case RIGHT:
           return true;
       default:
           return false;
   }
}

Now, the custom gesture listener is implemented but it cannot start listening to the touch event on its own, so we need to resolve this by performing the following steps:

  1. Firstly, we need to create an object of class GestureDetector and pass the current activity context and the object of class ‘SwipeGestureListener’ as parameters. Also while creating the listener for ‘SwipeGestureListener’ we need to pass the object of the Bottom Sheet in it as a parameter.

    GestureDetector gestureDetector = new GestureDetector(this, new SwipeGestureListener(bottomSheetBehavior)); 
  2. Then we need to override the ‘onTouchEvent()’ method of our Activity and pass the event which is received as a parameter to the GestureDetector.
    Doing this will pass the touch event that it received to the GestureDetector for it to handle.

    @Override
    public boolean onTouchEvent(MotionEvent event) {
       gestureDetector.onTouchEvent(event);                
       return super.onTouchEvent(event);
    }
    

The Bottom Sheet is now responsive to the gestures on the screen and this will improve the User Experience.

Resources

  1. Detect Common Gestures – Android Developer Article –  Android documentation
  2. Choreographic animations with Android’s Bottom Sheet – Blog by Orkhan Gasimli

 

Continue ReadingMaking Bottomsheet responsive using Custom Gesture Detector in PSLab Android App

Implementing Check-in time chart in Orga App

Earlier in the Open event orga app there were no charts present to track the check-in time of the attendees. Hence it was quite cumbersome for the organiser to track the people and at what time they have checked-in. Using this feature of check-in time chart, the process has become quite easier.

Whenever an attendee checks-in, the data point is added to the chart and a chart is plotted. The Y-axis shows the number of attendees and the X-axis shows the time at which they have checked-in.

To implement this feature I have taken use of the MPAndroidCharts library which makes the job a lot easier. Following steps were followed to implement the charts:

  • Adding the following Library dependency in the build.gradle file
implementation “com.github.PhilJay:MPAndroidChart:v3.0.3”
  • Now the following code is added to the ticket_analytics.xml file. This is done so that the UI of the charts can be created. The following XML file consists of the LineChart XML tag which shows the check-in time chart on screen. Also the labelling of the axis needs to be done, so the X-axis is explicitly named as “TIME”.
<LinearLayout
  android:layout_width=“match_parent”
  android:layout_height=“wrap_content”
  android:orientation=“vertical”>

  <TextView
      android:layout_width=“wrap_content”
      android:layout_height=“wrap_content”
      android:layout_marginLeft=“@dimen/spacing_normal”
      android:layout_marginStart=“@dimen/spacing_normal”
      android:layout_marginTop=“@dimen/spacing_normal”
      android:text=“@string/check_in_summary”
      android:textAllCaps=“true”
      android:textSize=“@dimen/text_size_small” />

  <com.github.mikephil.charting.charts.LineChart
      android:id=“@+id/chartCheckIn”
      android:layout_width=“match_parent”
      android:layout_height=“200dp”
      android:layout_marginEnd=“@dimen/spacing_normal”
      android:layout_marginLeft=“@dimen/spacing_normal”
      android:layout_marginRight=“@dimen/spacing_normal”
      android:layout_marginStart=“@dimen/spacing_normal” />

  <TextView
      android:layout_width=“wrap_content”
      android:layout_height=“wrap_content”
      android:layout_gravity=“center”
      android:layout_marginBottom=“8dp”
      android:layout_marginTop=“8dp”
      android:text=“@string/check_in_time”
      android:textSize=“10sp” />

  <LinearLayout
      android:layout_width=“match_parent”
      android:layout_height=“wrap_content”
      android:orientation=“vertical”>

      <FrameLayout
          android:layout_width=“match_parent”
          android:layout_height=“1dp”
          android:background=“@color/color_shadow” />

      <FrameLayout
          android:layout_width=“match_parent”
          android:layout_height=“@dimen/spacing_small”
          android:background=“@color/color_bottom_surface” />
  </LinearLayout>
</LinearLayout>
  • Now the a method loadCheckIn( )  chart needs to added to the EventsDashboardPresenter. This is called from the EventsDashboardFragment. The loadDataCheckIn( ) is created in the ChartAnalyzer class. We pass getId( ) as the parameter.
private void loadCheckInTimesChart() {
  chartAnalyser.showChart(getView().getCheckinTimeChartView());
  chartAnalyser.loadDataCheckIn(getId())
      .compose(disposeCompletable(getDisposable()))
      .subscribe(() -> {
          getView().showChartCheckIn(true);
          chartAnalyser.showChart(getView().getCheckinTimeChartView());
      }, throwable -> getView().showChartCheckIn(false));
}
  • Now we add the method loadDataCheckIn( ) in the ChartAnalyzer class. This method returns a Completable and takes eventId as the single parameter.
public Completable loadDataCheckIn(long eventId) {
  clearData();
  isCheckinChart = true;
  return getAttendeeSource(eventId).doOnNext(attendee -> {
     String checkInTime = attendee.getCheckinTimes();
     int length = checkInTime.split(“,”).length;
     String latestCheckInTime = checkInTime.split(“,”)[length – 1];
     error = checkInTime == null ? true : false;
     addDataPointForCheckIn(checkInTimeMap, latestCheckInTime);
  })
    .toList()
    .doAfterSuccess(attendees -> this.attendees = attendees)
    .toCompletable()
    .doOnComplete(() -> {
        if (error)
            throw new IllegalAccessException(“No checkin’s found”);
        checkInDataSet = setDataForCheckIn(checkInTimeMap, “check-in time”);
        prepare();
    });
}

It calls the getAttendeeSource( ) which further gives a call to the method getAttendees( ) from the AttendeeRepository. All the inormation related to the attendees is returned from which the check-in times is extracted. The check-in times are returned in comma separated form and hence we need to extract the first element of the sequence.

private Observable<Attendee> getAttendeeSource(long eventId) {
  if (attendees == null || attendees.isEmpty())
      return attendeeRepository.getAttendees(eventId, false);
  else
      return Observable.fromIterable(attendees);
}
  • After the success of loading the attendees, the method addDataPointForCheckIn is called. We call it by inserting the parameters Map<Integer, Long> and the dateString which we had passed from the loadDataCheckIn( ). Following is the code for it. A map is created out of the data. The key in the map is the time and value is the number of people who have checked-in at that time.
private void addDataPointForCheckIn(Map<Integer, Long> map, String dateString) {
  int hour = DateUtils.getDate(dateString).getHour();
  Long numberOfCheckins = map.get(hour);

  if (numberOfCheckins == null)
      numberOfCheckins = 0L;

  map.put(hour, ++numberOfCheckins);
}
  • After the map is created it is passed on to the setDataForCheckIn( ) and the label is provided as “check-in times”. Following is the code for setDataForCheckIn( ). All the values of the map are parsed and a new entry object is made in which the value of the key and value pairs are passed. This object is then added to the ArrayList.
private LineDataSet setDataForCheckIn(Map<Integer, Long> map, String label) throws ParseException {
  List<Entry> entries = new ArrayList<>();
  for (Map.Entry<Integer, Long> entry : map.entrySet()) {
      entries.add(new Entry(entry.getKey(), entry.getValue()));
  }
  Collections.sort(entries, new EntryXComparator());

  // Add a starting point 2 hrs ago
  entries.add(0, new Entry(entries.get(0).getX() – 2, 0));
  return new LineDataSet(entries, label);
}
  • The object LineDataSet is returned with all the entries stored in the ArrayList. Now the prepare( ) is called. It is in this method that we add the code for the UI of the chart.
private void prepare() {
  if (isCheckinChart) {
      initializeLineSet(checkInDataSet, R.color.light_blue_500, R.color.light_blue_100);
      lineData.addDataSet(checkInDataSet);
  } else {
      initializeLineSet(freeSet, R.color.light_blue_500, R.color.light_blue_100);
      initializeLineSet(paidSet, R.color.purple_500, R.color.purple_100);
      initializeLineSet(donationSet, R.color.red_500, R.color.red_100);
      lineData.addDataSet(freeSet);
      lineData.addDataSet(paidSet);
      lineData.addDataSet(donationSet);
      lineData.setDrawValues(false);
  }
lineData.setDrawValues(false);
}

initializeLineSet( ) is the method where we add the color which will be used for plotting the data set.In our case the color is blue.

  • We also need to plot the time stamps in the X-axis. Unfortunately MPAndroidCharts doesn’t have a functionality for that. So to handle it an inner class MyMyAxisValueFormatter is created which extends IAxisValueFormatter. Following is the code for it.
public class MyAxisValueFormatter implements IAxisValueFormatter {

  @Override
  public String getFormattedValue(float value, AxisBase axis) {
      if (value < 0)
          return values[24 + (int) value];
      return values[(int) value];
  }
}

The values array list consists of the time stamps that will be present on the X-Axis.

String[] values = new String[] {“00:00”, “1:00”, “2:00”, “3:00”, “4:00”, “5:00”, “6:00”, “7:00”, “8:00”, “9:00”, “10:00”, “11:00”, “12:00”, “13:00”, “14:00”, “15:00”, “16:00”, “17:00”, “18:00”, “19:00”, “20:00”, “21:00”, “22:00”, “23:00”};
  • Finally the showChart( ) is called in which we specify details regarding the grid color, legend, visibility of X and Y axis etc. We also specify the animation that needs to be done whenever the chart is on screen.

 

public void showChart(LineChart lineChart) {
  lineChart.setData(lineData);
  lineChart.getXAxis().setEnabled(true);
  lineChart.getAxisRight().setEnabled(false);
  lineChart.getDescription().setEnabled(false);
  lineChart.getLegend().setEnabled(false);

  YAxis yAxis = lineChart.getAxisLeft();
  yAxis.setGridLineWidth(1);
  yAxis.setGridColor(Color.parseColor(“#992ecc71”));
  if (!isCheckinChart)
      if (maxTicketSale > TICKET_SALE_THRESHOLD)
          yAxis.setGranularity(maxTicketSale / TICKET_SALE_THRESHOLD);
  else {
      XAxis xAxis = lineChart.getXAxis();
      xAxis.setValueFormatter(new MyAxisValueFormatter());
      yAxis.setGranularity(1);
      lineChart.getXAxis().setPosition(XAxis.XAxisPosition.BOTTOM);
      lineChart.getXAxis().setGranularity(1f);
  }

  Description description = new Description();
  description.setText(“”);
  lineChart.setDescription(description);
  lineChart.animateY(1000);
}

 

Continue ReadingImplementing Check-in time chart in Orga App

Implementing Horizontal Stepper In Open Event Orga App

Currently while event creation the user has to fill a form, all of which is present on a single screen. This process is a bit cumbersome. To improve the user interface and make the app more interactive, a horizontal stepper is implemented in the app. Implementing this would also make it consistent with the frontend project. Horizontal stepper actually divides the current fragment into 3 different fragments and the contents are distributed over all of them. The user can navigate through these fragments either by swiping or by using NEXT and PREV button which are present on the screen.

To implement this the following steps are taken:

  • I am using the following library in the app. This is added to the build.gradle file and sync is done.
//stepper
implementation ‘com.github.badoualy:stepper-indicator:1.0.7’
  • Now changes need to be made to the CreateEventActivity which acts as the base activity for the 3 fragments which we will be implementing later. We hover over to the activity_create.xml where we will add the Stepper tag and the viewpager. Following code is written there.
<android.support.v4.view.ViewPager
  android:id=“@+id/pager”
  android:layout_width=“match_parent”
  android:layout_height=“wrap_content”
  android:layout_alignParentLeft=“true”
  android:layout_alignParentStart=“true”
  android:layout_alignParentTop=“true”
  android:layout_marginTop=“100dp” />

<com.badoualy.stepperindicator.StepperIndicator
  android:id=“@+id/stepper_indicator”
  android:layout_width=“match_parent”
  android:layout_height=“wrap_content”
  android:layout_marginLeft=“16dp”
  android:layout_marginRight=“16dp”
  android:layout_marginTop=“32dp”
  app:stpi_animDuration=“200”
  app:stpi_circleColor=“@color/blue_200”
  app:stpi_circleRadius=“10dp”
  app:stpi_indicatorColor=“@color/green_500”
  app:stpi_labels=“@array/stepLabels”
  app:stpi_showDoneIcon=“true” />
  • To add the NEXT, PREVIOUS and SUBMIT buttons on the screen for navigation we will add the following code.
<LinearLayout
  android:layout_width=“match_parent”
  android:layout_height=“wrap_content”
  android:layout_gravity=“bottom”
  android:orientation=“horizontal”
  android:background=“@color/color_accent”>

  <Button
      android:id=“@+id/btn_prev”
      android:layout_width=“0dp”
      android:layout_weight=“0.5”
      android:layout_height=“wrap_content”
      android:background=“@color/color_accent”
      android:text=“Previous”
      android:layout_gravity=“bottom|start”
      android:textColor=“@android:color/white” />

  <Button
      android:id=“@+id/btn_next”
      android:layout_width=“0dp”
      android:layout_height=“wrap_content”
      android:background=“@color/color_accent”
      android:text=“Next”
      android:layout_weight=“0.5”
      android:layout_gravity=“bottom”
      android:textColor=“@android:color/white” />

  <Button
      android:id=“@+id/btn_submit”
      android:layout_width=“0dp”
      android:layout_height=“wrap_content”
      android:background=“@color/color_accent”
      android:text=“Create”
      android:layout_weight=“0.5”
      android:visibility=“gone”
      android:layout_gravity=“bottom”
      android:textColor=“@android:color/white” />

</LinearLayout>
  • A pager adapter class is made which extends the FragmentPagerAdapter. This class handles the position of the adapter and the fragments which need to be displayed at each step. All this is handled in the getItem( ) method.
public class PagerAdapter extends FragmentPagerAdapter {
  public PagerAdapter(FragmentManager fm) {
      super(fm);
  }

  @Override
  public Fragment getItem(int position) {
      switch (position) {
          case 0:
              return EventDetailsStepOne.newInstance();
          case 1:
              return EventDetailsStepTwo.newInstance();
          case 2:
              return EventDetailsStepThree.newInstance();
          default:
              return null;
      }
  }

  @Override
  public int getCount() {
      return 3;
  }
}
  • To link stepper indicator with the view pager the following code is added
assert pager != null;
pager.setAdapter(new PagerAdapter(getSupportFragmentManager()));

indicator.setViewPager(pager, pager.getAdapter().getCount());
  • A page listener is added to the viewpager so that the visibility of the buttons can be handled easily.
pager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
  @Override
  public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
      if (position == 0) {
          btnPrev.setVisibility(View.GONE);
          btnNext.setVisibility(View.VISIBLE);
          btnSubmit.setVisibility(View.GONE);
      } else if (position == 1) {
          btnPrev.setVisibility(View.VISIBLE);
          btnNext.setVisibility(View.VISIBLE);
          btnSubmit.setVisibility(View.GONE);
      } else if (position == 2) {
          btnPrev.setVisibility(View.VISIBLE);
          btnNext.setVisibility(View.GONE);
          btnSubmit.setVisibility(View.VISIBLE);
      }
  }
  • Now 3 different fragments are created along with their layouts and a SharedViewModel is also added which is shared by the fragments. SharedViewModel is different from a normal ViewModel where we need to provide the Activity context in the ViewModelProviders.of(context).

 

  • The current CreateEventFragment is divided into 3 fragments namely EventDetailsStepOne, EventDetailsStepTwo and EventDetailsStepThree and the code for all of them is redistributed to all of these fragments. So each and every fragment now has a different responsibility which is handled separately.

 

  • Now the current CreateEventFragment is changed to UpdateEventFragment. This will now only handle the editing of events when the Edit Event is selected.

Resources:

  1. Library for implementing horizontal stepper

https://github.com/badoualy/stepper-indicator

Continue ReadingImplementing Horizontal Stepper In Open Event Orga App

Implementing Contextual Action Bar in the Open Event Organizers App

The following blog post is regarding the using of Contextual Action Bar in the Open Event Orga App. In the Open Event Orga App, whenever an item was long pressed, then we had implemented a non standard way of showing the edit and delete button at the top. This would eventually degrade the UX of the app. So as to handle this, CAB was implemented.

When the user long presses on any item, we sometimes see a list of items such as Share, Delete or Edit on the toolbar. This is a standard functionality known as the Contextual Action Bar which is present in many apps. (image for reference given below)

Apart from the standard Action Bar, Android also provides us with the implementation of the Contextual Action Menu. A context menu is a floating menu that appears when the user performs a long-click on an element. It provides actions that affect the selected content or context frame.( image given below for reference)

(Contextual Action Bar)

(Contextual Action Menu)

To implement the standard CAB in the Orga App, the following steps given below have been followed.

In the FaqlistFragment class, an interface ActionMode.Callback is implemented. In its callback methods, you can specify the actions for the contextual action bar, respond to click events on action items, and handle other lifecycle events for the action mode.

Each of the callback methods have been discussed below:

  • onCreateActionMode( ) ->  This method is called first when the ActionMode is started. Here with the help of MenuInflater, the menu which has been made in the menus.xml file is inflated. Also when the app goes into Actions Mode , the status bar is black in color. Hence to handle that the color of the status bar is being changed programmatically.

 

  • onPrepareActionMode( ) -> This should generally return true.

  • onActionItemClicked( ) -> The action mode consists of many items which have separate action. The separate actions for the items are defined here. For eg.In the code snippet given below, when the icon with the id “del is selected then the showDeleteDialog( ) must be called.

 

  • onDestroyActionMode( ) -> When the user exits the Actions Mode, the mode should be assigned a null value and the app should be set to default state.
public ActionMode.Callback actionCallback = new ActionMode.Callback() {
  @Override
  public boolean onCreateActionMode(ActionMode mode, Menu menu) {
      MenuInflater inflater = mode.getMenuInflater();
      inflater.inflate(R.menu.menu_faqs, menu);
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
          //hold current color of status bar
          statusBarColor = getActivity().getWindow().getStatusBarColor();
          //set the default color
          getActivity().getWindow().setStatusBarColor(getResources().getColor(R.color.color_top_surface));
      }
      return true;
  }

  @Override
  public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
       return true;
  }

  @Override
  public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
      switch (item.getItemId()) {
          case R.id.del:
              showDeleteDialog();
              break;
          default:
              return false;
      }
      return false;
  }

  @Override
  public void onDestroyActionMode(ActionMode mode) {
      actionMode = null;
      getPresenter().resetToDefaultState();
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
          //return to “old” color of status bar
          getActivity().getWindow().setStatusBarColor(statusBarColor);
      }
  }
};
  1. In the styles.xml, the XML Tag given below needs to be added so that the Action Mode replaces the current Action Bar otherwise, a new Action Bar is created above the Action Bar which is already present.

 <item name=“windowActionModeOverlay”>true</item>

  1. In the Orga App, as the MVP pattern is followed we need to add the following to methods to the interface so that the View and Presenter can communicate via these methods.
public interface FaqListView extends Progressive, Erroneous, Refreshable, Emptiable<Faq> {

  void exitContextualMenuMode();

  void enterContextualMenuMode();
}

 

  1. In the enterContextualMenuMode( ) implementation in the FaqListFragment the startActionMode()  is called to enable the contextual action mode.
@Override
public void enterContextualMenuMode() {
  actionMode = getActivity().startActionMode(actionCallback);
}

References:

  • Official Android documentation for Menu’s

https://developer.android.com/guide/topics/ui/menus#CAB

  • Sample CAB implementation

http://www.technotalkative.com/contextual-action-bar-cab-android/

  • Action mode over Toolbar

http://www.androhub.com/android-contextual-action-mode-over-toolbar/            

Continue ReadingImplementing Contextual Action Bar in the Open Event Organizers App

Use Of DBFlowChangeListener for Automatic Updation of FAQ List in the Open Event Orga App

In the Open Event Orga App, whenever a faq is created in via the CreatefaqFragment, the new faqs aren’t updated in the FAQ’s list. The user has to swipe down and refresh to see the newly created FAQ. This seems to be a major problem and hence to solve this issue a DBFlowChangeListener class has been created.

Following blog post shows the implementation of the ChangeListener class.

Steps of Implementation

In the FaqListPresenter constructor pass an argument of the type DatabaseChangeListener<Faq> and assign it to local variable faqChangeListener.

@Inject
public FaqListPresenter(FaqRepository faqRepository, DatabaseChangeListener<Faq> faqChangeListener) {
  this.faqRepository = faqRepository;
  this.faqChangeListener = faqChangeListener;
}

A method listenChanges( ) is made in the Presenter that will initiate the listening of database. Following code shows the listenChanges( ) . startListening( ) is a method in the DBFlowDatabaseChangeListener which is accessed with the help of DatabaseChangeListener interface.

private void listenChanges() {
  
  faqChangeListener.startListening();
  
  faqChangeListener.getNotifier()
      .compose(dispose(getDisposable()))
      .map(DbFlowDatabaseChangeListener.ModelChange::getAction)
      .filter(action -> action.equals(BaseModel.Action.INSERT) || action.equals(BaseModel.Action.DELETE))
      .subscribeOn(Schedulers.io())
      .subscribe(faqModelChange -> loadFaqs(false), Logger::logError);
}

In the startListening( )  in DatabaseChangeListener, DirectModelNotifier is used to get notified of database changes. It is ultimately registered for any kind of model change as shown in the following code snippet. ReplaySubject is one of the 4 kinds of subjects ( Publish Subject, Replay Subject, Behavior Subject, Async Subject) that RxJava provides. It emits all the items of the source Observable, regardless of when the subscriber subscribes.

public void startListening() {
  if (disposable == null || disposable.isDisposed())
      replaySubject = ReplaySubject.create();

  modelModelChangedListener = new DirectModelNotifier.ModelChangedListener<T>() {

      @Override
      public void onTableChanged(@Nullable Class<?> aClass, @NonNull BaseModel.Action action) {
          replaySubject.onNext(new ModelChange<>(null, action));
      }

      @Override
      public void onModelChanged(@NonNull T model, @NonNull BaseModel.Action action) {
          replaySubject.onNext(new ModelChange<>(model, action));
      }
  };

  DirectModelNotifier.get().registerForModelChanges(classType, modelModelChangedListener);
}

When the faqChangeListener is subscribed it calls the loadFaqs( ) , it ultimately loads the updated values from the database. Hence all the updated values are now displayed in the FaqListFragment.

public void loadFaqs(boolean forceReload) {
  getFaqSource(forceReload)
      .compose(dispose(getDisposable()))
      .compose(progressiveErroneousRefresh(getView(), forceReload))
      .toList()
      .compose(emptiable(getView(), faqs))
      .subscribe(Logger::logSuccess, Logger::logError);
}

Hence the use of DBFlowListener makes it possible to communicate between 2 fragments. Its usability has also been exploited in Tracks, Tickets, Sponsors fragments as well.

References:

  1. RaizLab’s DBFLow:      https://github.com/Raizlabs/DBFlow
  2. Docs related to DBFlow:             https://agrosner.gitbooks.io/dbflow/content/Observability.html
  3. Subjects used in RxJavahttps://blog.mindorks.com/understanding-rxjava-subject-publish-replay-behavior-and-async-subject-224d663d452f
Continue ReadingUse Of DBFlowChangeListener for Automatic Updation of FAQ List in the Open Event Orga App

“STOP” action in SUSI Android App

Generally whenever there was a long query asked from SUSI through speech, it would respond to the user with a speech output, similarly the output is given through speech whenever the user clicks on the “Try it” button on the skill details activity.

The long answers for e.g. asking SUSI “How to cook biryani ?” gives a very long response which when conveyed through the speech output takes a long amount of time. Also most of the time users don’t want to listen to such long answers, but at the same time there is no option or feature to stop SUSI. The user either needs to switch over to another activity or has to close the app.

So, to solve this problem such that neither the user has to shut down the app nor the user has to switch over to another activity, the “STOP” action was added in SUSI.

The “STOP” action was integrated in the server and is of following type :

 

“actions”: [{“type”: “stop”}],

How to define STOP response ?

To integrate this action type in the app, a separate response type was added and checked for. In the file ParseSusiResponseHandler.kt the action type stop was added as :

Constant.STOP -> try {
  stop = susiResponse.answers[0].actions[1].type
} catch (e: Exception) {

}

So, now whenever the query of type “stop” was entered by the user it would be caught by this block and further processing could be done. The stop variable takes the value from the JSON response fetched and the value extracted is the one stored against the type key in the second field of the actions JSON in the first field of the answers JSON.

What to be done when executing STOP?

After the STOP action was caught the task to do was to define the behaviour of the app when this response was caught. So, first thing that should happen when STOP is received is that the TextToSpeech Engine should close so that SUSI no longer can speak the sentence but defining top is more than just closing the TTS engine. STOP signifies that any activity that is being continued right now should be stopped.

So, Using the android lifecycle methods I added a function in the IChatView interface to define what actions should take place in the stopping process.

override fun stopMic() {
  onPause()
  registerReceiver(networkStateReceiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))

  window.setSoftInputMode(
          WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN
  )

  if (recordingThread != null)
      chatPresenter.startHotwordDetection()

  if (etMessage.text.toString().isNotEmpty()) {
      btnSpeak.setImageResource(R.drawable.ic_send_fab)
      etMessage.setText(“”)
      chatPresenter.micCheck(false)
  }

  chatPresenter.checkPreferences()
}

The above function sends the activity to the onPause() lifecycle method to put the activity in a pausing state so that all that is taking place right now in the activity stops and this definitely serves our purpose. But after pausing the activity, there was a further need to perform some functionality that had to done on the start of the activity and therefore the code for that was also added in this function.

How to catch STOP?

The below code was added in ChatPresenter.kt file in which if the actionType from the psh object of type ParseSusiResponseHelper is “STOP” then the view function stopMic() is called which was defined above.

val psh = ParseSusiResponseHelper()
psh.parseSusiResponse(susiResponse, i, utilModel.getString(R.string.error_occurred_try_again))

var setMessage = psh.answer
if (psh.actionType == Constant.ANSWER && (PrefManager.checkSpeechOutputPref() && check || PrefManager.checkSpeechAlwaysPref())) {
  setMessage = psh.answer

  var speechReply = setMessage
  if (psh.isHavingLink) {
      speechReply = setMessage.substring(0, setMessage.indexOf(“http”))
  }
  chatView?.voiceReply(speechReply, susiResponse.answers[0].actions[i].language)
} else if (psh.actionType == Constant.STOP) {
  setMessage = psh.stop
  chatView?.stopMic()
}

Final Output

References

  1. Stop  json response from susi server : https://api.susi.ai/susi/chat.json?timezoneOffset=-330&q=susi+stop
  2. Android life cycle methods – Google: https://developer.android.com/guide/components/activities/activity-lifecycle
  3. Interaction between view and presenters : https://medium.com/@cervonefrancesco/model-view-presenter-android-guidelines-94970b430ddf
Continue Reading“STOP” action in SUSI Android App

Adding Code of Conduct in Open Event Web app

Open Event Server sends JSON data as a response of its REST (Representational State Transfer) API. The main eventyay platform allows organizers to add code of conduct to their event, as a result the JSON data sent by the server contains code of conduct key value pair, this value is extracted from the data and is used to create a separate page for Code of Conduct in any event.

The steps for data extraction and compilation are as follows:

Extracting code of conduct

Since open event server has two types of JSON data formats v1 and v2, both of them contains code of conduct. The key for code of conduct in v1 is code_of_conduct and for v2 is code-of-conduct. The data extraction for v1 data format occurs in fold_v1.js and the main event details are stored in an object urls as shown below:

fold_v1.js

const urls= {
 ....
 ....
 ....

 email: event.email,
 orgname: event.organizer_name,
 location_name: event.location_name,
 featuresection: featuresection,
 sponsorsection: sponsorsection,
 codeOfConduct: event.code_of_conduct
};

 

fold_v2.js

const urls= {
 ....
 ....
 ....

 email: event.email,
 orgname: event['organizer-name'],
 location_name: event['location-name'],
 featuresection: featuresection,
 sponsorsection: sponsorsection,
 codeOfConduct: event['code-of-conduct']
};

Adding template for CoC

Now we have extracted the data and have stored the value for code of conduct in an object, we need to render this in a template. For this, we created a template named CoC.hbs and the data for code of conduct is accessed via {{{eventurls.codeOfConduct}}} as shown below.

{{>navbar}}
<div class="main-coc-container container">
 <div class="row">
   <div class="middle col-sm-12">
     <h2 class="filter-heading track-heading text-center">
       <span>Code of Conduct</span>
     </h2>
   </div>
 </div>

 <div class="row">
   <div class="col-sm-12 col-md-12">
     <div class="coc">
       {{{eventurls.codeOfConduct}}}
     </div>
   </div>
 </div>
</div>
{{>footer}}

Compiling and minifying

Now we have stored the event details in an object we copy this object as a key to jsonData, this data is passed as an argument for compiling the code of conduct template namely CoC.hbs to a HTML file and is lately minified. For minification purpose gulp module is used.

if(jsonData.eventurls.codeOfConduct) {
 setPageFlag('CoC');
 fs.writeFileSync(distHelper.distPath + '/' + appFolder + '/CoC.html', minifyHtml(codeOfConductTpl(jsonData)));
}

Adding link to CoC page

Till now, we have successfully compiled a HTML page for code of conduct of an event. This page is linked under a heading in the footer section of every page by placing reference to it in footer.hbs.

{{#if eventurls.codeOfConduct}}
 <li><a target="_self" href="CoC.html">Code of Conduct</a></li>
{{/if}}

Customizing the CoC container

The code of conduct page is customized by placing the container in the center and aligning the text. Styling like background-color, padding and margin are set on the container to provide a better appearance to the page.

.coc {
 margin: auto;
 text-align: justify;
 width: 60%;

 a {
   &:hover {
     color: $dark-black;
   }
 }
}

.main-coc-container {
 background-color: $main-background;
 margin-bottom: 4%;
 margin-top: 2%;
 padding-bottom: 50px;
 padding-top: 2%;
}

Resources

Continue ReadingAdding Code of Conduct in Open Event Web app

Implementing job queue in Open Event Web app

Open Event Web app enables multiple request handling by the implementation of queue system in its generator. Every request received from the client is saved and stored in the queue backed by redis server. The jobs are then processed one at a time using the FCFS (First come First Serve) job scheduling algorithm. Processing the requests one by one prevents the crashing of app and also prevents the loss of requests from the client.

Initialising job queue

The job queue is initialised with a name and the connection object of redis server as the arguments.

const redisClient =  require('redis').createClient(process.env.REDIS_URL);
const Queue = require('bee-queue');
const queue = new Queue('generator-queue', {redis: redisClient});

Handling jobs in queue

The client emits an event namely ‘live’ when request for event generation is received, the corresponding event is listened and a new job for the request is created and enqueued in the job queue. Every request received by the client is saved to ensure that there is no loss of request. The queue is then searched for the requests or the jobs which are in ‘waiting’ state, if the current request status for the job Id is waiting the socket emits an event namely ‘waiting’.

socket.on('live', function(formData) {
 const req = {body: formData};
 const job = queue.createJob(req);

 job.on('succeeded', function() {
   console.log('completed job ' + job.id);
 });

 job.save(async function(err, currentJob) {
   if (err) {
     console.log('job failed to save');
   }
   emitter = socket;
   console.log('saved job ' + currentJob.id);
   const jobs = await queue.getJobs('waiting', {start: 0, end: 25});
   const jobIds = await jobs.map((currJob) => currJob.id);

   if(jobIds.indexOf(currentJob.id) !== -1) {
     socket.emit('waiting');
   }
 });

});

Updating the status of request

If the socket emits the event ‘waiting’ it signifies that some other job is currently in process and the status of the current request is ‘waiting’.

socket.on('waiting', function () {
 updateStatusAnimate('Request status: Waiting');
});

Processing the jobs

When the queue is in ready state and no job is currently in process, it starts processing the saved job. The job is not completed until it receives a callback. The generator starts generating the event when the processing of request starts.

queue.on('ready', function() {
 queue.process(function(job, done) {
   console.log('processing job ' + job.id);
   generator.createDistDir(job.data, emitter, done);
 });
 console.log('processing jobs...');
});

 

The generator calls the callback function for the current job when the event generation completes or it is halted in between due to some error. As soon as the current job completes, next job in the queue starts being processed.

generator.createDistDir() = function(req, socket, callback){
  
  .....
  .....
  .....

  mailer.uploadAndsendMail(req.body.email, eventName, socket, (obj) => {
    if(obj.mail)
      logger.addLog('Success', 'Mail sent succesfully', socket);
    else
      logger.addLog('Error', 'Error sending mail', socket);

    if(emit) {
      socket.emit('live.ready', {
        appDir: appFolder,
        url: obj.url
      });
      callback(null);
    }
    else {
      callback(appFolder);
    }

    done(null, 'write');
   });
}

Resources

Continue ReadingImplementing job queue in Open Event Web app