Making custom dialog boxes in PSLab app

This blog covers the topic of how to create custom dialog boxes in an Android app by taking an example of how it was implemented in PSLab Android application. The custom dialog box that will be illustrated in this blog will have a layout similar to that from my PR feat: added a guide for Logic Analyzer Instrument using Alert Dialog Box in PSLab Android repository. But all the main functionalities that can be added to a custom dialog will be illustrated in this blog.

How custom dialog was implemented in the PSLab app?

  • The first step is to make a layout for the custom dialog box. The layout of custom dialog should be such that it shouldn’t occupy the whole screen as the idea here is to overlay the dialog over the activity and not replace the whole activity with the dialog layout. The layout can be implemented by using the following attributes :
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent">

   <RelativeLayout
       android:layout_width="match_parent"
       android:layout_height="wrap_content">

       <TextView
           android:id="@+id/custom_dialog_text"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"/>

       <ImageView
           android:id="@+id/custom_dialog_schematic"
           android:layout_width="wrap_content"
           android:layout_height="@dimen/schematic_height" />

       <TextView
           android:id="@+id/description_text"
           android:layout_width="match_parent"
           android:layout_height="wrap_content" />

       <CheckBox
           android:id="@+id/toggle_show_again"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content" />

       <RelativeLayout
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:layout_below="@id/toggle_show_again">

           <Button
               android:id="@+id/dismiss_button"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content" />

       </RelativeLayout>
   </RelativeLayout>
</ScrollView>

The result of the above layout after adding proper margins and padding is as shown in figure 1.

Figure 1. The output of the layout made for custom dialog

  • Now as the layout is ready, we can focus on adding Java code to inflate the layout over a particular activity. Make a function in the activity or the fragment in which the custom dialog is to be inflated. Add the following code (if the layout is same as that in PSLab app else modify the code as per the layout) in the function to display the custom dialog box.
public void howToConnectDialog(String title, String intro, int iconID, String desc) {
   try {
       final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
       LayoutInflater inflater = getLayoutInflater();
       View dialogView = inflater.inflate(R.layout.custom_dialog_box, null);

 // Find and set all the attributes used in the layout and then create the dialog as shown
       
       builder.setView(dialogView);
       builder.setTitle(title);
       final AlertDialog dialog = builder.create();
       ok_button.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
                dialog.dismiss();
           }
       });
       dialog.show();
   } catch (Exception e) {
       e.printStackTrace();
   }
}

Here, the LayoutInflater inflates the custom dialog layout in the Alertdialog builder. The Alertdialog builder is used to hold and set values for all the views present in the inflated layout. The builder then creates a final custom dialog when builder.create() method is called. Finally, the dialog is shown by calling method dialog.show() and is dismissed by calling method dialog.dismiss().

  • Now the dialog box is ready and it will be inflated when the above method is called. But the “Don’t show again” checkbox currently has no functionality and so the dialog box will pop up although “Don’t show again” checkbox is ticked mark by the user. For adding this functionality, we will need to use Shared Preferences to save the state of the dialog box. For using Shared Preferences, first, add the following lines of code in the above method
final SharedPreferences settings = getActivity().getSharedPreferences(PREFS_NAME, 0);
Boolean skipMessage = settings.getBoolean("skipMessage", false);

The PREFS_NAME is the key to distinctly identify the state of the given dialog box from the stored state of many dialog boxes (if any). A suggested key name is “customDialogPreference”. The skipMessage is a boolean used here to check the state if the dialog box should be inflated or not.

Now format the onClickListener of the OK button with the following code :

Boolean checkBoxResult = false;
if (dontShowAgain.isChecked())
   checkBoxResult = true;
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("skipMessage", checkBoxResult);
editor.apply();
dialog.dismiss();

The following code checks the state of “Don’t show again” dialog and then inflates the custom dialog if applicable. If the checkbox is ticked mark then on pressing the OK button the dialog won’t be inflated again.

Resources

  1. https://developer.android.com/guide/topics/ui/dialogs – Android Documentation on Dialogs (Great Resource to make a custom dialog)
Continue Reading

Move Photos from one Path to Another in Phimpme Android Application

In the Phimpme Android application, users can perform various operations on images such as editing an image, sharing an image, printing a pdf version of the image and many more. However, another important functionality that has been implemented is the option to move an image from one path(folder) to another. So in this blog post, I will be discussing how we achieved the functionality to move any image.

Step 1

First, we need to add an option in the overflow menu to move any no of images from one folder to another. The option to move an image has been added by implementing the following lines of code in the  menu_view_pager.xml file.

<item
  android:id=“@+id/action_move”
  app:showAsAction=“never”
  android:title=“Move”/>

Step 2

Now after the user opts for the move operation, a bottomSheetDialog containing all the albums/folders in the storage to choose for moving the photos into will be displayed. Once the user selects the required location to move the photos to, a method moveSelectedMedia would be invoked passing-in the context and the destination path as the parameters. Thereafter inside the method moveSelectedMedia, first a check would be performed to determine whether the selected photos(to move) are already present in the folder(path folder and destination folder both are same), if true the operation would be terminated as the photos are already present in the destination folder, and if false the selected photos would be moved to the destination folder one by one by the use of another method moveMedia. The code snippets used to implement the moveSelectedMedia is provided below.

public int moveSelectedMedia(Context context, String targetDir) {
 int n = 0;
 try
 {
    int index=-1;
    for(int i =0;i<selectedMedias.size();i++)
    {
       String s = selectedMedias.get(i).getPath();
       int indexOfLastSlash = s.lastIndexOf(“/”);
       String fileName = s.substring(indexOfLastSlash + 1);

       if(!selectedMedias.get(i).getPath().equals(targetDir+“/”+fileName)){
          index=-1;
       }else{
          index=i;
          break;

       }
    }
    if(index!=-1)
    {
       n = –1;
    }else{
       for (int i = 0; i < selectedMedias.size(); i++) {

          if (moveMedia(context, selectedMedias.get(i).getPath(), targetDir)) {
             String from = selectedMedias.get(i).getPath();
             scanFile(context, new String[]{ from, StringUtils.getPhotoPathMoved(selectedMedias.get(i).getPath(), targetDir) },
                   new MediaScannerConnection.OnScanCompletedListener() {
                      @Override
                      public void onScanCompleted(String s, Uri uri) {
                         Log.d(“scanFile”, “onScanCompleted: “ + s);
                      }
                   });
             media.remove(selectedMedias.get(i));
             n++;
          }
       }
       setCount(media.size());
    }
 } catch (Exception e) { e.printStackTrace(); }
 return n;
}

Step 3

In this step, I’d discuss the use and implementation of the moveMedia method mentioned in the earlier step. So the moveMedia is used to move a single photo to a particular destination, taking-in the context, the oldpath of the photo and the destination folder as parameters. Inside this method another method moveFile of the ContentHelper class is invoked passing-in the context, File object of the photo and File object of the destination folder as parameters. Now inside the moveFile method the actual move operation takes place. First the photo is tried to move by doing a normal rename operation by using the renameTo function of the File class. The success of the rename operation updates the path of the photo to the destination path and deletes the original path of the photo whereas if the normal rename operation fails, then the photo is moved by first performing a copy operation to copy the photo to the destination folder and then deleting the original photo. The code snippets used to implement the moveMedia and moveFile functions are provided below.

private boolean moveMedia(Context context, String source, String targetDir) {
 File from = new File(source);
 File to = new File(targetDir);
 return ContentHelper.moveFile(context, from, to);
}
public static boolean moveFile(Context context, @NonNull final File source, @NonNull final File targetDir) {
 // First try the normal rename.
 File target = new File(targetDir, source.getName());

 boolean success = source.renameTo(target);

 if (!success) {
    success = copyFile(context, source, targetDir);
    if (success) {
       success = deleteFile(context, source);
    }
 }

 //if (success) scanFile(context, new String[]{ source.getPath(), target.getPath() });
 return success;
}

And at last, after the photos are moved successfully the media adapter is called with the updated photos list to display thus displaying the remaining photos in the folder.

This is how we have implemented the functionality to move images from one path to another in the Phimpme Android application. To get the full source code, please refer to the Phimpme Android Github repository listed in the resource section below.

Resources

1. Android Developer documentation –              https://developer.android.com/reference/java/io/File

2. Github-Phimpme Android Repository – https://github.com/fossasia/phimpme- android/

3. Renaming a file in java – http://stacktips.com/tutorials/java/how-to-delete-and-rename-a-file-in-java

Continue Reading

Implementing Trash Bin in Phimpme Android Application

In the Phimpme Android application, users can perform various operations/actions on the photos available, some of the important operations involve renaming an image, sharing an image, deleting image etc. However, when the user performs delete operation for image/images the images are permanently deleted from the storage and there is no option to restore/recover the deleted image. Now imagine a situation where the user accidentally or in a rush deletes photo/photos, and now there is no possible way for the user to recover those accidentally deleted photos, so in such circumstances, the Trash Bin feature could prove to be a breather for the user. So, in this blog post, I will be discussing the implementation of the Trash Bin feature.

Step 1

Firstly, we need to implement the functionality to move the images to bin folder whenever the delete operation is performed. For this, we’d programmatically create a folder naming it .nomedia, so that this folder is not picked up by any other similar application while scanning folders. Now when the user deletes images, a check would be performed first to determine whether the bin folder already exists or not and actions would be performed accordingly(a folder would be created if it doesn’t already exist and the selected photos for deletion would be moved to the bin folder or the deleted photos would be moved directly to the bin folder if its there). Code snippets used to implement the bin folder is provided below.

private boolean addToTrash(){
  int no = 0;
  boolean succ = false;
  File file = new File(Environment.getExternalStorageDirectory() + “/” + “.nomedia”);
  if(file.exists() && file.isDirectory()){
      if(!all_photos && !fav_photos && editMode){
          no = getAlbum().moveSelectedMedia(getApplicationContext(), file.getAbsolutePath());
      }else if(all_photos && !fav_photos && editMode){
          no = getAlbum().moveAllMedia(getApplicationContext(), file.getAbsolutePath(), selectedMedias);
      }else if(!editMode && !all_photos && !fav_photos){
          no = getAlbum().moveAllMedia(getApplicationContext(), file.getAbsolutePath(), getAlbum().getMedia());
      }
  else{
      if(file.mkdir()){
          if(!all_photos && !fav_photos && editMode){
              no = getAlbum().moveSelectedMedia(getApplicationContext(), file.getAbsolutePath());
          }else if(all_photos && !fav_photos && editMode){
              no = getAlbum().moveAllMedia(getApplicationContext(), file.getAbsolutePath(), selectedMedias);
          }else if(!editMode && !all_photos && !fav_photos){
              no = getAlbum().moveAllMedia(getApplicationContext(), file.getAbsolutePath(), getAlbum().getMedia());
          }
         // no = getAlbum().moveSelectedMedia(getApplicationContext(), file.getAbsolutePath());
       }
 // clearSelectedPhotos();
  return succ;
}

Step 2

Now if all the photos(selected by the user for deletion) are successfully moved to the bin folder, realm objects corresponding to those image files would be created and added to the realm database. The corresponding Realm object would consist of attributes namely oldpath, trashbinpath, time of delete and duration. The realm objects would be used at the time of implementing restore functionality for the trash bin images. The realm model class representing the Trash bin object is provided below.

public class TrashBinRealmModel extends RealmObject {

  @PrimaryKey
  private String trashbinpath;
  private String oldpath;
  private String datetime;
  private String timeperiod;

  public TrashBinRealmModel(){

  }

  public TrashBinRealmModel(String oldpath, String newpath, String datetime, String timeperiod){
      this.oldpath = oldpath;
      this.trashbinpath = newpath;
      this.datetime = datetime;
      this.timeperiod = timeperiod;
  }

  public void setTrashbinpath(String trashbinpath){
      this.trashbinpath = trashbinpath;
  }

  public String getTrashbinpath(){
      return trashbinpath;
  }
  public void setDatetime(String datetime){
      this.datetime = datetime;
  }

  public String getDatetime(){
      return datetime;
  }

  public void setOldpath(String oldpath){
      this.oldpath = oldpath;
  }

  public String getOldpath(){
      return oldpath;
  }

  public void setTimeperiod(String timeperiod){
      this.timeperiod = timeperiod;
  }

  public String getTimeperiod(){
      return timeperiod;
  }
}

The implementation for the function to add trash bin objects to Realm database is provided below.

for(int i = 0; i < media.size(); i++){
  int index = media.get(i).getPath().lastIndexOf(“/”);
  String name = media.get(i).getPath().substring(index + 1);
  realm.beginTransaction();
  String trashpath = trashbinpath + “/” + name;
  TrashBinRealmModel trashBinRealmModel =   realm.createObject(TrashBinRealmModel.class, trashpath);
  trashBinRealmModel.setOldpath(media.get(i).getPath());
  trashBinRealmModel.setDatetime(new SimpleDateFormat(“dd/MM/yyyy HH:mm:ss”).format(new Date()));
  trashBinRealmModel.setTimeperiod(“null”);
  realm.commitTransaction();
}

Step 3

Now we need to provide an option to the user to navigate to the Trash bin section to perform the delete(permanently) or restore option. So for the aforementioned action, we’d be adding an option in the navigation drawer menu titled “Trash Bin” clicking on which the Trash Bin section would be displayed to the user. The XML code changes committed to implementing the option has been provided below.

<LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
            android:id=“@+id/ll_drawer_trashbin”
            android:layout_width=“match_parent”
            android:layout_height=“wrap_content”
            android:background=“@drawable/ripple”
            android:clickable=“true”
            android:orientation=“horizontal”>

  <com.mikepenz.iconics.view.IconicsImageView
      android:id=“@+id/Drawer_trash_Icon”
      android:layout_width=“@dimen/icon_width_height”
      android:layout_height=“@dimen/icon_width_height”
      android:layout_gravity=“center_vertical”
      android:layout_marginLeft=“@dimen/big_spacing”
      android:layout_marginRight=“@dimen/big_spacing”
      app:iiv_icon=“gmd-file-upload”/>

  <TextView
      android:id=“@+id/Drawer_trash_Item”
      android:layout_width=“wrap_content”
      android:layout_height=“wrap_content”
      android:paddingBottom=“16dp”
      android:paddingTop=“16dp”
      android:text=“Trash Bin”
      android:textColor=“@color/md_dark_background”
      android:textSize=“16sp”/>
</LinearLayout>

This is how we have implemented the functionality to rename an image in the Phimpme Android application. To get the full source code, please refer to the Phimpme Android Github repository listed in the resource section below.

Resources

1. Android Developer documentation –https://developer.android.com/reference/java/io/File

2. Github-Phimpme Android Repository –https://github.com/fossasia/phimpme-android/

3. Realm for Android tutorial –https://www.androidhive.info/2016/05/android-working-with-realm-database-replacing-sqlite-core-data/

4. Hiding directories using .nomedia file –http://www.easycodeway.com/2016/08/hide-files-in-android-using-nomedia-file.html

Continue Reading

Option to pin albums in Phimpme Android Application

In the Phimpme Android application, users are provided with options to perform various operations on the albums available such as move, creating a zip file of the album, rename an album and many more. However, one another useful functionality that has been added to the Phimpme Android application is the option to pin albums. So in this post, I will be discussing the implementation of the pin to top functionality.

Step 1

First, we need to add an option in the overflow menu to pin the album which has been selected, to the top. The pin to top option can be added in the overflow menu by implementing the following lines of code in the menu_albums.xml file(This file contains the overflow menu options for albums).

<item
  android:id=“@+id/set_pin_album”
  android:title=“@string/pin”
  app:showAsAction=“never” />

Step 2

Now when the user selects the option to pin any album to the top, the user’s choice of the selected album is retrieved by the getSelectedAlbum method of Handling Albums class. Thereafter the togglePin method of class Album Settings is invoked, passing in the context as a parameter. In the togglePin method an instance of the CustomHelper class is obtained first, then its pinAlbum method is called passing in the album path and a variable pinned as the parameters. Here depending on the boolean value of the variable pinned, the selected album will be pinned or unpinned. The code snippets used to implement getSelectedAlbum and togglePin method are provided below.

public Album getSelectedAlbum(int index) {
return selectedAlbums.get(index);
}
public void togglePin(Context context) {
this.pinned = !pinned;
CustomAlbumsHelper h = CustomAlbumsHelper.getInstance(context);
h.pinAlbum(path, pinned);
}

The getSelectedAlbum method and the togglePin method are subsequently used by the following line of code.

getAlbums().getSelectedAlbum(0).settings.togglePin(getApplicationContext());

Step 3

In this step, I’d discuss the implementation of the pinAlbum method invoked inside the togglePin method. In the pinAlbum method, first a writable instance of album_settings SQLite database is obtained. The album_settings database maintains information about the albums in a table with the name of the table Albums and the columns representing info about the album such as path, excluded, pinned, sortorder etc. Next, the checkAndCreateAlbum method would be invoked passing in the writable instance of the database and path of the album as parameters. Now inside the checkAndCreateAlbum method, a check would be performed to determine whether a column representing info about the selected album is already present or not, if not a row is created for the selected album. The code snippet used to implement the checkAndCreateAlbum is provided below.

private void checkAndCreateAlbum(SQLiteDatabase db, String path) {

  Cursor cursor = db.query(TABLE_ALBUMS, null,  ALBUM_PATH+“=?”,
          new String[]{ path }, null, null, null);

  if (cursor.getCount() == 0) {
      ContentValues values = new ContentValues();
      values.put(ALBUM_PATH, path);
      values.put(ALBUM_SORTING_MODE, SortingMode.DATE.getValue());
      values.put(ALBUM_SORTING_ORDER, SortingOrder.DESCENDING.getValue());
      values.put(ALBUM_EXCLUDED, 0);
      db.insert(TABLE_ALBUMS, null, values);
  }

  cursor.close();
}

Now in the pinAlbum method through the writable instance of the album_settings database, the pinned information of the selected album is updated in the Albums table accordingly. The code snippets used to implement the pinAlbum method is provided below.

void pinAlbum(String path, boolean status) {
  SQLiteDatabase db = this.getWritableDatabase();
  checkAndCreateAlbum(db, path);
  ContentValues values = new ContentValues();
  values.put(ALBUM_PINNED, status ? 1 : 0);
  db.update(TABLE_ALBUMS, values, ALBUM_PATH+“=?”, new String[]{ path });
  db.close();
}

At last, the notifyDataSetChanged method of the AlbumsAdapter class would be called so as to display the pinned albums at the top irrespective of the sorting order.

A screenshot displaying the pinned albums at the top in albums view is provided below.

This is how we have implemented the functionality to pin an album to top in the Phimpme Android application. To get the full source code, please refer to the Phimpme Android Github repository listed in the resource section below.

Resources

1. Android Developer documentation –https://developer.android.com/reference/android/database/sqlite/SQLiteDatabase

2. Github-Phimpme Android Repository – https://github.com/fossasia/phimpme-android/

3. Sqlite database tutorial for android –https://www.androidhive.info/2011/11/android-sqlite-database-tutorial/.

Continue Reading

Adding a Countdown to Orders Page in Open Event Frontend

This blog post will illustrate how you can add a countdown to orders page which on finishing expires the ticket in Open Event. In Open Event we allow some predefined time for users to fill in their details and once the time expires order gets expired and tickets get released. Users can order the tickets again if they want.

We start by adding a createdAt field to orders model so that we can keep track of remaining time. To calculate the time when the order should expire we add predefined time in which user should fill their details to createdAt time. In this way, we get the time when the order will expire.

So now to calculate the remaining time we just subtract the expiring time from current time. And then we render this data into the template. We define getRemainingTime property in our template and fetch the data for that property with help of javascript.

To see the template code visit this link.

The challenge here is to update the time remaining after every second. For this, we take the help of ember runloop. The run.later() function of ember runloop helps us to calculate the property after every second and set it. Code for setting the remaining time with the help of javascript is given below.

// app/components/forms/orders/order-form.js

getRemainingTime: computed('data', function() {
    let willExpireAt = this.get('data.createdAt').add(10, 'minutes');
    this.timer(willExpireAt, this.get('data.identifier'));
  }),

  timer(willExpireAt, orderIdentifier) {
    run.later(() => {
      let currentTime = moment();
      let diff = moment.duration(willExpireAt.diff(currentTime));
      this.set('getRemainingTime', moment.utc(diff.asMilliseconds()).format('mm:ss'));
      if (diff > 0) {
        this.timer(willExpireAt, orderIdentifier);
      } else {
        this.get('data').reload();
        this.get('router').transitionTo('orders.expired', orderIdentifier);
      }
    }, 1000);
  }

 

As given in the code. We pass expiring time and order’s model instance to the timer function. Timer function calculates the remaining time and sets it to getRemainingTime property of template. Timer function runs after every second with the help of run.later() function of ember runloop. To format the remaining time into MM:SS we take help of moment.js library and format the data accordingly.

Once the remaining time is less than zero (time expires) we reload the model and transition current route to expired route. We do not have to set order status as expired inside the FE. Server sets the order as expired after the predefined time. So we just reload the model from the server and we get the updated status of the order.

Resources:
Continue Reading

Adding Helper and Adding Action Buttons to Orders List in Open Event Frontend

This blog post will illustrate how to add a helper to orders list and add action buttons to orders list to delete and cancel an order in Open Event Frontend. To cancel or delete an order item we need to communicate to the server. The API endpoints to which we communicate are:

  • PATCH        /v1/orders/{order_identifier}
  • DELETE    /v1/orders/{orders_identifier}

We will define the action buttons in ui-table component of open event frontend. We will use the cell-actions file to define the cell buttons that will be present in cell-actions column. The following handlebars code will render the buttons on website.

//components/ui-table/cell/events/view/tickets/orders/cell-actions.hbs

class="ui vertical compact basic buttons"> {{#if (and (not-eq record.status 'cancelled') (can-modify-order record))}} {{#ui-popup content=(t 'Cancel order') click=(action (confirm (t 'Are you sure you would like to cancel this Order?') (action cancelOrder record))) class='ui icon button' position='left center'}} class="delete icon"> {{/ui-popup}} {{/if}} {{#if (can-modify-order record)}} {{#ui-popup content=(t 'Delete order') click=(action (confirm (t 'Are you sure you would like to delete this Order?') (action deleteOrder record))) class='ui icon button' position='left center'}} class="trash icon"> {{/ui-popup}} {{/if}} {{#ui-popup content=(t 'Resend order confirmation') class='ui icon button' position='left center'}} class="mail outline icon"> {{/ui-popup}}

 

In above code you can see two things. First is can-modify-order which is a helper. Helper is used to simplify conditional logics which cannot be easily placed in handlebars. Second thing is action. There are two actions defined: cancelOrder and deleteOrder. We will see implementation of these later. First let’s see how we define can-modify-order helper.

In can-modify-order helper we want to return true or false in case we want cancel button and delete button to display or not respectively. We write the code of can-modify-order in helpers/can-modify-order.js file. When we want to get result from this helper we call it from handlebars file and pass any parameter that we want to use in helper. Code for can-modify-order helper is given below.

// helpers/can-modify-order.js

import Helper from '@ember/component/helper';

export function canModifyOrder(params) {
 let [order] = params;
 if (order.amount !== null && order.amount > 0) {
   // returns false if order is paid and completed
   return order.status !== 'completed';
 }
 // returns true for free ticket
 return true;
}

export default Helper.helper(canModifyOrder);

 

We extract the parameter and store it in order variable. We see if it satisfies our conditions we return true else false.

Now lets see how we can define actions to perform delete and cancel action on a order. We define these actions in controllers section of app. After performing suitable operation with order we call save to update modified order and destroyRecord() to delete an order. Let see the code implementation for these actions.

actions: {
   deleteOrder(order) {
     this.set('isLoading', true);
     order.destroyRecord()
       .then(() => {
         this.get('model').reload();
         this.notify.success(this.get('l10n').t('Order has been deleted successfully.'));
       })
       .catch(() => {
         this.notify.error(this.get('l10n').t('An unexpected error has occurred.'));
       })
       .finally(() => {
         this.set('isLoading', false);
       });
   },
   cancelOrder(order) {
     this.set('isLoading', true);
     order.set('status', 'cancelled');
     order.save()
       .then(() => {
         this.notify.success(this.get('l10n').t('Order has been cancelled successfully.'));
       })
       .catch(() => {
         this.notify.error(this.get('l10n').t('An unexpected error has occurred.'));
       })
       .finally(() => {
         this.set('isLoading', false);
       });
   }

 
After defining these actions, buttons in the orders list start working. In this way, we can make use of helper to simplify the conditional logic inside templates and define proper actions.

Resources:
Continue Reading

How to Make Promotional Codes Applicable on Tickets During Ordering in Open Event Frontend

This blog illustrate how to enable application of promotional codes on tickets during ordering tickets in Open Event Frontend to avail discounts and access to special tickets. Open event allows organizers to add some promotional codes on some tickets, which can be used by users to avail additional offers on tickets while ordering. Promotional codes can be of three types:

  1. Discount Codes: Allows customers to buy a ticket at discounted rates.
  2. Access Codes: Allows customers to access some hidden tickets which are accessible only to special customers.
  3. Discount + Access Code: Allows customer to access special tickets and avail discount at the same time.

Creating a discount/access code:

Organizers and admin can create an access code or a discount code from the event dashboard. They can specify the validity period of the code and can also specify the tickets on which the code will be applicable.

Validating promotional code after user enters the code:

User is allowed to enter the promotional code on events page upon selecting the tickets. IF promotional code is valid then suitable discount is provided on applicable tickets and if promotional code is an access code then hidden tickets for which the promotional code is valid are shown.

To check the validity of the promotional code we deal with the following APIs on the open event server:

  • GET             /v1/discount-codes/{Code}              (For Discount code)
  • GET             /v1/access-codes/{Code}                  (For Access code)

Code snippet to check the validity for access code is given below:

let promotionalCode = this.get('promotionalCode');
 let order = this.get('order');
   try {
     let accessCode = await this.get('store').findRecord('access-code', promotionalCode, {});
     order.set('accessCode', accessCode);
     let tickets = await accessCode.get('tickets');
     tickets.forEach(ticket => {
     ticket.set('isHidden', false);
     this.get('tickets').addObject(ticket);
     this.get('accessCodeTickets').addObject(ticket);
     this.set('invalidPromotionalCode', false);
  });
  } catch (e) {
     this.set('invalidPromotionalCode', true);
  }

 

Full code can be seen here https://github.com/fossasia/open-event-frontend/blob/development/app/components/public/ticket-list.js

Similarly for discount code we fetch the details of the discount code via the api and then validate the code. After the validation we apply the discount to the tickets applicable. Code snippet for the discount code part is given below:

try {
  let discountCode = await this.get('store').findRecord('discount-code', promotionalCode, { include: 'tickets' });
  let discountType = discountCode.get('type');
  let discountValue = discountCode.get('value');
  order.set('discountCode', discountCode);
  let tickets = await discountCode.get('tickets');
  tickets.forEach(ticket => {
     let ticketPrice = ticket.get('price');
     if (discountType === 'amount') {
       ticket.set('discount', Math.min(ticketPrice, discountValue));
       this.get('discountedTickets').addObject(ticket);
     } else {
       ticket.set('discount', ticketPrice * (discountValue / 100));
       this.get('discountedTickets').addObject(ticket);
     }
     this.set('invalidPromotionalCode', false);
  });
} catch (e) {
   if (this.get('invalidPromotionalCode')) {
      this.set('invalidPromotionalCode', true);
   }
}

 

Full code can be seen https://github.com/fossasia/open-event-frontend/blob/development/app/components/public/ticket-list.js

After promotional codes are verified we apply them to the selected tickets. In this way we apply the promotional codes to the tickets.

Resources

 

Continue Reading

A Workflow of Auto Executing Services on SUSI.AI Smart Speaker

As we plan to create a headless client on RaspberryPi, the requirement was that the SUSI.AI programs should run automatically. To do so, we had to figure out a way to boot up various scripts on startup.

We had the following options to execute the scripts on startup:

  1. Editing Rc.local file
  2. Systemd Rules
  3. Crontab

We decided to proceed with Systemd Rules because using Rc.local and Crontab requires modifying the default system files which in case of any error would make the os functionalities to crash very soon.

We then created the SystemD rules for the following services:

1.factory-daemon.service
2. python-flask.service
3. susi-server.service
4. update-daemon.service
5. susi-linux.service

Now I’ll demonstrate the working and the functionality of each service being implemented.

1. Factory-Daemon Service

This service initiates the factory daemon with the raspberry Pi startup and then keeps it running continuously looking for any input from the GPiO port.

[Unit]
Description=SUSI Linux Factory Daemon
After=multi-user.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 /home/pi/SUSI.AI/susi_linux/factory_reset/factory_reset.py

[Install]
WantedBy=multi-user.target

2. Python-Flask Service

This service starts a python Server to allow handshake between mobile apps and the Smart Speaker which will allow the user to configure SUSI Smart Speaker accordingly.

[Unit]
Description=Python Server for SUSI Linux
After=multi-user.target

[Service]
Type=simple
ExecStart=/usr/bin/python3  /home/pi/SUSI.AI/susi_linux/access_point/server/server.py

[Install]
WantedBy=multi-user.target

3.SUSI-Server Service

This service starts the Local SUSI Server as soon as the Raspberry Pi starts up which in turn allows the SUSI Linux programs to fetch responses of queries very quickly.

[Unit]
Description=Starting SUSI Server for SUSI Linux
After=multi-user.target

[Service]
Type=oneshot
ExecStart=/home/pi/SUSI.AI/susi_linux/susi_server/susi_server/bin/restart.sh

[Install]
WantedBy=multi-user.target

4. Update-Daemon Service

This Service creates a Daemon which starts with the Raspberry Pi and fetches the latest updates from the repository from the upstream branch.

[Unit]
Description=Update Check- SUSI Linux
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/home/pi/SUSI.AI/susi_linux/update_daemon/update_check.sh

[Install]
WantedBy=multi-user.target

5. SUSI-Linux Service

This Service finally runs the main SUSI Linux software after everything has started.

[Unit]
Description=Starting SUSI Linux
Wants=network-online.target
After=network-online.target

[Service]
Type=idle
WorkingDirectory=/home/pi/SUSI.AI/susi_linux/
ExecStart=/usr/bin/python3 -m main

[Install]
WantedBy=multi-user.target

This blog gives a brief workflow of auto-executing services on SUSI Smart Speaker.

Resources

Continue Reading

Configuring LED Lights with SUSI Smart Speaker

To make the SUSI Smart Speaker more interactive and to improve the visual aesthetics, we configured SUSI Smart Speaker’s response with 3 RGB led lights. We have used a new piHat as an external hardware to configure the LEDs.

Now the new hardware specs of the SUSI Smart Speaker are:

  1. Raspberry Pi
  2. ReSpeaker PiHat 2 Mic Array
  3. External Speakers

Using an external PiHat not only added the RGB light functionality but also eliminated the need to use a USB microphone and configured a factory reset button

Configuring the PiHat as the default Audio driver

To Use the PiHat as the default input driver, we use the package called PulseAudio.

And we use the following command in the installation script.

pacmd set-sink-port alsa_output.platform-soc_sound.analog-stereo analog-output-headphones

Configuring PiHat’s GPIO Button with Factory Reset

There is an onboard User Button, which is connected to GPIO17. We use the python library RPi.GPIO to detect the user button. The python script is used in the following way

GPIO.setmode(GPIO.BCM)
GPIO.setup(17,GPIO.IN)
i = 1
while True:
if GPIO.input(17) == 1:
       time.sleep(0.1)
       pass
   elif GPIO.input(17) == 0 :
       start = time.time()
       while GPIO.input(17) == 0 :
           time.sleep(0.1)
       end = time.time()
       total = end – start
       if total >= 7 :
           subprocess.call([‘bash’,‘factory_reset.sh’])  # nosec #pylint-disable type: ignore
       else :
           mixer = alsaaudio.Mixer()
           value = mixer.getvolume()[0]
           if value != 0:
               mixer.setvolume(0)
           else:
               mixer.setvolume(50)
       print(total)
       time.sleep(0.1)

 

This script checks on the button which is configured on GPIO port 17 on the PiHat. If the button is pressed for than 7 secs, the factory reset process takes place, else the device is muted.

Configuring PiHat’s LED with Speaker’s Response

We use a python library called SPIDEV to sync the LED lights with SUSI’s response. SPIDEV is usually used to send a response to the bus devices on the Raspberry Pi.

The first step was installing spidev

sudo pip install spidev

Now we create a class where we store all the methods where we send the signal to the bus port. We treat the LED lights as a circular array and then have a rotation of RGB lights

class LED_COLOR:
    # Constants
   MAX_BRIGHTNESS = 0b11111    LED_START = 0b11100000
    def __init__(self, num_led, global_brightness=MAX_BRIGHTNESS,
                order=‘rgb’, bus=0, device=1, max_speed_hz=8000000):
       self.num_led = num_led
       order = order.lower()
       self.rgb = RGB_MAP.get(order, RGB_MAP[‘rgb’])
       if global_brightness > self.MAX_BRIGHTNESS:
           self.global_brightness = self.MAX_BRIGHTNESS
       else:
           self.global_brightness = global_brightness
        self.leds = [self.LED_START, 0, 0, 0] * self.num_led
       self.spi = spidev.SpiDev()
       self.spi.open(bus, device)
       if max_speed_hz:
           self.spi.max_speed_hz = max_speed_hz
    def clear_strip(self):
        for led in range(self.num_led):
           self.set_pixel(led, 0, 0, 0)
       self.show()
    def set_pixel(self, led_num, red, green, blue, bright_percent=100):
       if led_num < 0:
           return          if led_num >= self.num_led:
           return
       brightness = int(ceil(bright_percent * self.global_brightness / 100.0))
       ledstart = (brightness & 0b00011111) | self.LED_START
        start_index = 4 * led_num
       self.leds[start_index] = ledstart
       self.leds[start_index + self.rgb[0]] = red
       self.leds[start_index + self.rgb[1]] = green
       self.leds[start_index + self.rgb[2]] = blue
    def set_pixel_rgb(self, led_num, rgb_color, bright_percent=100):
       self.set_pixel(led_num, (rgb_color & 0xFF0000) >> 16,
                      (rgb_color & 0x00FF00) >> 8, rgb_color & 0x0000FF, bright_percent)
    def rotate(self, positions=1):
       cutoff = 4 * (positions % self.num_led)
       self.leds = self.leds[cutoff:] + self.leds[:cutoff]
    def show(self):
       data = list(self.leds)
       while data:
           self.spi.xfer2(data[:32])
           data = data[32:]
       self.clock_end_frame()
    def cleanup(self):
       self.spi.close()  # Close SPI port
    def wheel(self, wheel_pos):
       “””Get a color from a color wheel; Green -> Red -> Blue -> Green”””
        if wheel_pos > 255:
           wheel_pos = 255  # Safeguard
       if wheel_pos < 85:  # Green -> Red
           return self.combine_color(wheel_pos * 3, 255 – wheel_pos * 3, 0)
       if wheel_pos < 170:  # Red -> Blue
           wheel_pos -= 85
           return self.combine_color(255 – wheel_pos * 3, 0, wheel_pos * 3)
       wheel_pos -= 170
       return self.combine_color(0, wheel_pos * 3, 255 – wheel_pos * 3)

 

Now we use the threading to create non-blocking code which will allow SUSI to send response as well as change the LED’s simultaneously.

class Lights:
   LIGHTS_N = 3
    def __init__(self):
        self.next = threading.Event()
       self.queue = Queue.Queue()
       self.thread = threading.Thread(target=self._run)
       self.thread.daemon = True
       self.thread.start()
    def wakeup(self, direction=0):
       def f():
           self._wakeup(direction)
        self.next.set()
       self.queue.put(f)
    def listen(self):
       self.next.set()
       self.queue.put(self._listen)
    def think(self):
       self.next.set()
       self.queue.put(self._think)
    def speak(self):
       self.next.set()
       self.queue.put(self._speak)
    def off(self):
       self.next.set()
       self.queue.put(self._off)

This is how LED lights are configured with SUSI’s response

Resources

Additional Resources

Continue Reading

Connecting the Smart Speaker with Mobile Clients

The beauty of SUSI Smart Speaker lies in it being customizable according to the user’s needs. And we allow the user to customize it by providing an interface through the mobile clients. To do so, we create a local server on the Raspberry Pi itself. The Raspberry Pi is started in an Access Point mode and the mobile clients hit the endpoints in a specific order and then the configuration is sent to the server and stored according to the user.

 

The following API’s are required to be executed by the mobile clients

1> /speaker_config

2> /wifi_credentials

3> /auth

4> /config

 

The following is the order of API execution

1. /speaker_config

This endpoint only takes the room name as a parameter. And then send send to the server to store the location of the device under the user’s account

def speaker_config():
   room_name = request.args.get(‘room_name’)
   config = json_config.connect(config_json_folder)
   config[‘room_name’] = rogom_name

 

2. /wifi_credentials

This endpoint takes the wifi ssid and wifi password as the parameters and then stores it in the raspberry Pi wifi config file.

 

def wifi_config():
   wifi_ssid = request.args.get(‘wifissid’)
   wifi_password = request.args.get(‘wifipassd’)
   subprocess.call([‘sudo’, ‘bash’, wifi_search_folder + ‘/wifi_search.sh’, wifi_ssid, wifi_password])
   display_message = {“wifi”:“configured”, “wifi_ssid”:wifi_ssid, “wifi_password”: wifi_password}
   resp = jsonify(display_message)
   resp.status_code = 200
   return resp

 

Now the script wifi_search is called which stores the wifi credentials in the wifi_config file using the following command

 

cat >> /etc/wpa_supplicant/wpa_supplicant.conf <<EOF
network={
   ssid=“$SSID”
   psk=“$PSK”
}
EOF

 

3. /auth

This endpoint takes the SUSI’s login credentials as parameters, i.e. the registered email id and the corresponding password.

 

def login():
   auth = request.args.get(‘auth’)
   email = request.args.get(’email’)
   password = request.args.get(‘password’)
   subprocess.call([‘sudo’, ‘bash’, access_point_folder + ‘/login.sh’, auth, email, password])
   display_message = {“authentication”:“successful”, “auth”: auth, “email”: email, “password”: password}
   resp = jsonify(display_message)
   resp.status_code = 200
   return resp

 

4. /config

Finally, this endpoint takes the stt, tts, hotword detection engine and wake button as the parameters and configures the speaker accordingly.

 

def config():
   stt = request.args.get(‘stt’)
   tts = request.args.get(‘tts’)
   hotword = request.args.get(‘hotword’)
   wake = request.args.get(‘wake’)
   subprocess.Popen([‘sudo’, ‘bash’, access_point_folder + ‘/config.sh ‘, stt, tts, hotword, wake])
   display_message = {“configuration”:“successful”, “stt”: stt, “tts”: tts, “hotword”: hotword, “wake”:wake}
   resp = jsonify(display_message)
   resp.status_code = 200
   return resp

 

Now, this function runs a script called config.sh which in turn runs a script called rwap.sh to convert the Raspberry Pi to normal mode and then finally start SUSI on startup.

 

#!/bin/bash

if [ $EUID -ne 0 ]
then echo “Must be root”
exit
fi

cd /etc/hostapd/
sed -i ‘1,14d’ hostapd.conf

cd /etc/
sed -i ‘57,60d’ dhcpcd.conf

cd /etc/network/
sed -i ‘9,17d’ interfaces

echo “Please reboot”
sudo reboot

 

After successfully hitting all the endpoint from the client, your Smart Speaker would restart and would see the following screen on your client.

 

References

Additional Resources

Tags

fossasia, susi, susi.ai, gsoc, gsoc’18, handshake

Continue Reading
Close Menu
%d bloggers like this: