Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Android Wear Demo App - TodayMenu

0.00/5 (No votes)
19 Jan 2016 1  
It's all about Android Wear Demo App TodayMenu implementation

Introduction

In this article, I will be walking you through the Android Wear Demo app named "TodayMenu". I am going to reuse the sample code developed by “Martin Knudsen”. The entire credit for developing the application goes to Martin Knudsen.

https://github.com/zaifrun/TodayMenu

This project is about Today’s menu, the author developed this one as a learning material for students.

The sample project utilizes some of the most commonly used Android Wear components. So it’s always good to have some understanding about android widgets like BoxInsetLayout, WearableListView, FrameLayout, LinearLayout etc.

TodayMenu shows screen in four fragments, the main fragment shows the list of choices which is nothing but food items. The second one shows the statistics of the selected food items. The third one accepts or takes in a voice input nothing but a food item name and temporarily saves in SQLite database. The last fragment shows two buttons, one to reset the statistics and the other to rest food choices.

Let us have a look into the SQLite code and understand how the app can save data in SQLite DB. Below are the topics you can navigate and learn.

Background

Please take a look into the below link to have some understanding about Android Wear.

http://www.codeproject.com/Articles/1038337/Introduction-to-Android-Wear

 

TodayMenu Application UI  

Before we dig into the TodayMenu application functionality, let us take a quick look into the application UI screens.

TodayMenu App

Database custom class

We have a class name Database which extends itself from SQLiteOpenHelper. There are two methods that we are overriding i.e onCreate andonUpdate . Below is the code snippet of onCreate override. We are executing SQL script to create tables for menu and choices.

@Override
public void onCreate(SQLiteDatabase db) {
   db.execSQL("CREATE TABLE menu ( id INTEGER PRIMARY KEY AUTOINCREMENT," +
         "name TEXT, weekday INTEGER);");
   db.execSQL("CREATE TABLE choices ( id INTEGER PRIMARY KEY AUTOINCREMENT," +
         "name TEXT);");             
}

Here’s the code snippet for onUpgrade override. If the old version was 1 and the new version is 2, then we are executing a SQL script to create new table for user defined choices. The onUpgrade gets executed based on the app version. If you want to add functionality for your application which requires database changes, here’s the place where you can handle. 

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
   if (oldVersion==1 && newVersion==2)
      db.execSQL("CREATE TABLE choices ( id INTEGER PRIMARY KEY AUTOINCREMENT," +
         "name TEXT);");
}

This class ‘Database’ has few other methods that deal with reading choices, adding food etc.  We will soon take a look into those. 

MainActivity XML and Code

Now let us take a look into the main activity xml and the associated code. Below is the code snippet of the activity.xml. You can notice a BoxInsetLayout is being used so the same UI can be displayed on rounded or square watches. It composes GridViewPager and DotsPageIndicator.

<android.support.wearable.view.BoxInsetLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="match_parent"
    android:layout_width="match_parent">

    <!-- This is the gridviewpager, it makes sure we can swipe between different views -->
   <android.support.wearable.view.GridViewPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

    <!-- This is the DotsPageIndicator, it makes sure we can use the small
    dots on the bottom of the screen to indicate the current page of the app is displayed -->

    <android.support.wearable.view.DotsPageIndicator
        android:id="@+id/page_indicator"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal|bottom">
    </android.support.wearable.view.DotsPageIndicator>

Below is the code snippet of main activity onCreate override. We will be digging in to understand how the GridViewPager is being set with the data. You can see below, how a SampleGridPagerAdapter instance is set to the activity GridViewPager. Also the DotsPageIndicator is set with the pager instance so it gives a visual feedback on the fragment the user is in. 

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity);

    final Resources res = getResources();
     final GridViewPager pager = (GridViewPager) findViewById(R.id.pager);
     pager.setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListener() {
            @Override
            public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
                final boolean round = insets.isRound();
                int rowMargin = res.getDimensionPixelOffset(R.dimen.page_row_margin);
                int colMargin = res.getDimensionPixelOffset(round ?
                        R.dimen.page_column_margin_round : R.dimen.page_column_margin);
                pager.setPageMargins(rowMargin, colMargin);
                pager.onApplyWindowInsets(insets);
                return insets;
            }
        });

     pager.setAdapter(new SampleGridPagerAdapter(this, getFragmentManager()));
     pagerGlobal = pager;
     DotsPageIndicator dotsPageIndicator = (DotsPageIndicator) findViewById(R.id.page_indicator);
     dotsPageIndicator.setPager(pager);
     Database db = new Database(this);
     db.readChoices(); //when the app starts we need to read the choices from the database file
}

There is something interesting happens when the Main activity gets loaded. I.e. we also read choices. Here’s the code snippet which deals with reading choices and it’s within the Database class. First, we issue a SELECT query to fetch all choices order by id. If the count is 0 which means there are no user input choices, so we will be looping through the choices list and insert into our choices table. Else, we are iterating over the choices, gather all of them and set the same to a static string array Choices.ELEMENTS.

public String[] readChoices() {

   SQLiteDatabase database = getReadableDatabase();
   Cursor cursor = database.rawQuery("SELECT name FROM choices ORDER BY id",null);
   int count = cursor.getCount();

   if (count==0) //there is nothing, so first time we start the app.
   {
         for (String choice : Choices.ELEMENTS_RESET)
         database.execSQL("INSERT INTO choices (name) VALUES ('"+choice+"')");
         Choices.ELEMENTS = new String[Choices.ELEMENTS_RESET.length];
         System.arraycopy(Choices.ELEMENTS_RESET, 0, Choices.ELEMENTS, 0, 
             Choices.ELEMENTS_RESET.length);
         cursor.close();
         return Choices.ELEMENTS_RESET; //we just have default values.
   }
   else
   {
      String[] elements = new String[count];
      int index = 0;

      while (cursor.moveToNext())
      {              
         elements[index] = cursor.getString(0);
         index++;
      }

      Choices.ELEMENTS = elements;   //overwrite the elements array
      cursor.close();
      return elements;
   }
}

Here’s the code snippet of Choices class. We have the initial list of choices and also the user defined or input choices.

public class Choices {
    public static String[] ELEMENTS_RESET =  { "Chicken", "Beef", "Pork", "Lamb","Duck","Turkey" };
    public static String[] ELEMENTS;
}

It’s time to have a look into SampleGridPagerAdapter logic. It’s a custom class extends itself from FragmentGridPagerAdapter. Below is the code snippet for the same. As of now, we are dealing with four fragments. Within the constructor, we create a new instance of each of the fragments that we are going to display on a GridViewPager. There’s an override method getFragment that you can see below returns the fragment instance based on the column. As and when the user swipes from left to right, these fragments get displayed on the wearable device. 

GridViewPager Adaper

Below is the code snippet of GridViewPager Adapter.

public class SampleGridPagerAdapter extends FragmentGridPagerAdapter {

    MenuFragment menuFragment;
    ClearFragment clearFragment;
    StatsFragment statsFragment;
    SpeechFragment speechFragment;

    public SpeechFragment getSpeechFragment()
    {
       return speechFragment;
    }
    
    public ClearFragment getClearFragment()
    {
       return clearFragment;
    }
    
    public StatsFragment getStatsFragment()
    {
       return statsFragment;
    }
    
    public MenuFragment getMenuFragment()
    {
       return menuFragment;
    }

    public SampleGridPagerAdapter(Context ctx, FragmentManager fm) {
        super(fm);
        menuFragment = new MenuFragment();
        clearFragment = new ClearFragment();
        statsFragment = new StatsFragment();
        statsFragment.setContext(ctx); //we need the context in this class
        speechFragment = new SpeechFragment();
    }

    public void notifyStatsSetChanged() {
        statsFragment.updateUI();
    }

    public void listViewDataSetChanged()  {
       menuFragment.resetList();
    }
    

    @Override
    public Fragment getFragment(int row, int col) {
        if (col==0)
           return menuFragment;  
        else if (col==1)
           return statsFragment; 
        else if (col==2)
           return speechFragment;
        else 
           return clearFragment;
    }

    @Override
    public int getRowCount() {
       return 1;  //we just have 1 row of pages, meaning scrolling horizontally
    }

    @Override
    public int getColumnCount(int rowNum) {
       return 4; //we just have 4 columns - fixed in this app.
    }
}

Let us now dig into each of the above fragments to understand more about the inner working. Below is the code snippet of MenuFragment which extends itself from Fragment and implements WearableListView.ClickListener. The onCreateView override has a code to inflate the layout so we can fine the WearableListView and set its adapter with the list of choices.

Menu Fragment

Here's the code snippet of Menu fragment.

public class MenuFragment extends Fragment implements WearableListView.ClickListener {
   
   WearableListView listView;
   
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
       View view = inflater.inflate(R.layout.select, container, false);
       
      listView =(WearableListView) view.findViewById(R.id.wearable_list);
            
      if (listView!=null)
      {
         listView.setAdapter(new Adapter(getActivity().getApplicationContext(), Choices.ELEMENTS));
         // Set a click listener - using this activity
         listView.setClickListener(this);
         listView.setGreedyTouchMode(true);
      }
       return view;
    }
       &hellip;.
}     

Below is the snapshot of the “TodayMenu” app main screen. You can see below its showing up the Menu fragment containing a ListView

Menu_Fragment

Here’s the code snippet where we are handing the wearable listview onClick event. 

1)    First, we need to get the index of the selected listview item, we are obtaining the same from a “Tag” object. coming next, you will see details on how we set the tag. 

2)     Get the food choice based on the tag value.

3)     Create an instance of Database class and make a call to addFood so we can save our choice.

4)    An Intent instance is created to show a Success confirmation to the user. 

5)    In the end, we are going to update the stats fragment UI so when the user navigates, she/he can see the updated statistics of the choices the user has selected. 

@Override
public void onClick(WearableListView.ViewHolder v) {
   Integer tag = (Integer) v.itemView.getTag();
   int index = tag.intValue();
   
   String chosen = Choices.ELEMENTS[index];
   Database db = new Database(getActivity());
   db.addFood(chosen);

   Intent intent = new Intent(getActivity().getApplicationContext(), ConfirmationActivity.class);
   intent.putExtra(ConfirmationActivity.EXTRA_ANIMATION_TYPE,
                   ConfirmationActivity.SUCCESS_ANIMATION);
   intent.putExtra(ConfirmationActivity.EXTRA_MESSAGE,getResources()
         .getString(R.string.saved)+" "+chosen);
   startActivity(intent);
   ((SampleGridPagerAdapter) MainActivity.getPager().getAdapter()).notifyStatsSetChanged();
}

Menu Fragment ListView binding

Below is the code snippet of Menu fragment list view adapter. The adapter takes two parameters, one is the context and the other is a dataset instance. The dataset has all the list of choices to be displayed. There are two main overrides that we need to take care of. i.e onCreateViewHolder and onBindViewHolder
Within the onCreateViewHolder method, all we have to do is return an instance of WearableListView.ViewHolder.

Create a new instance of ItemViewHolder with the view. A LayoutInflator instance is used to inflate the layout R.layout.list_item. The onBindViewHolder method internally gets called where we get the ViewHolder instance and then get the TextView so that we can set appropriate text from the dataset by position. Also you can notice we are making a call to set the tag object with the position value so we can use the same in onClick event so we can get the right choice and save in database. 

private static final class Adapter extends WearableListView.Adapter {
    private String[] mDataset;
    private final Context mContext;
    private final LayoutInflater mInflater;

    public Adapter(Context context, String[] dataset) {
        mContext = context;
        mInflater = LayoutInflater.from(context);
        mDataset = dataset;
    }

    public static class ItemViewHolder extends WearableListView.ViewHolder {
        private TextView textView;
        public ItemViewHolder(View itemView) {
            super(itemView);
            textView = (TextView) itemView.findViewById(R.id.name);
        }
    }

    @Override
    public WearableListView.ViewHolder onCreateViewHolder(ViewGroup parent,
                                                          int viewType) {
        return new ItemViewHolder(mInflater.inflate(R.layout.list_item, null));
    }

    @Override
    public void onBindViewHolder(WearableListView.ViewHolder holder,
                                 int position) {
        ItemViewHolder itemHolder = (ItemViewHolder) holder;
        TextView view = itemHolder.textView;
        view.setText(mDataset[position]);
        holder.itemView.setTag(position);
    }

    @Override
    public int getItemCount() {
        return mDataset.length;
    }
}

Main Fragment Resetting choice list

Now let us see how to reset the list of choices. Below is the code snippet for the same. First we need to get the choice length and copy all the choices temporarily into a string array so we can reset the Choices.ELEMENTS  and the ListView component by setting the adapter and refreshing the same by making a call to invalidate method.

public void resetList() {
   int len = Choices.ELEMENTS_RESET.length;
   String[] newElements = new String[len];
   System.arraycopy(Choices.ELEMENTS_RESET, 0, newElements, 0, len);
   Choices.ELEMENTS = newElements;
   listView.setAdapter(new Adapter(getActivity().getApplicationContext(), Choices.ELEMENTS));
   listView.invalidate();
}

Statistics Fragment 

Now let us take a look into the StatsFragment, where it shows the detailed statistics about the food choices the user chooses. Below is the partial code snippet of StatsFragment. We are making use of a LinearLayout where we have one TextView with a text set to “Statistics”. Coming next you will see how we are adding one more TextView component to LinearLayout so that we can show the real statistics to user.

<LinearLayout 
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:layout_gravity="center"
        android:orientation="vertical"
        android:id="@+id/statslayout">

<TextView 
    android:layout_height="wrap_content"
        android:layout_width="wrap_content" 
        android:gravity="center_horizontal"
        android:textSize="24sp"
        android:layout_gravity="center_horizontal"
        android:textColor="@color/blue"
        android:text="@string/statistics"/>
</LinearLayout>

Here’s the snapshot of the statistics fragment.

Statistics Fragment

Updating Statistics UI

It’s time to see how the statistics information is being shown to the user. Within the StatsFragment onCreate override, we are making a call to update the UI. Below is the code snippet for the same. Here’s what we do.

1)    Create an instance of Database and get the latest statistics from SQLite DB. Hold the same in ArrayList of Item type.

2)    Get the child count for the LinearLayout instance so that we can remove and add a view so the user can see the up to date refreshed view.

3)    The next few set of lines, we are looping through all the statistics, create a TextView instance and then set text, color, font etc. and add the same to LinearLayout instance. 

public void updateUI() {
   Database db = new Database(context);
   ArrayList<Item> items = db.getStats();
   Collections.sort(items); //items are now sorted according to the frequency
 
   int children = parent.getChildCount();
   if (children>1) //do we have more than the "statistics" label, then remove them
   {
      parent.removeViews(1, children-1);
   }

   for (Item item : items) {
      TextView text = new TextView(getActivity());
      
      String p = String.format("%.1f", item.getPercent());
      text.setText(item.getName() + " : "+item.getFreq()+ " ("+p+" %)");
      text.setTextColor(Color.WHITE);
      text.setTextSize(22);
      text.setLayoutParams(new LayoutParams(
              LayoutParams.WRAP_CONTENT,
              LayoutParams.WRAP_CONTENT));
      parent.addView(text); //add the textview to the layout.
   }
}

Speech Fragment

Let us take a look into the speech fragment and see the inner workings. The speech being highly important part of Android wear, the main functionality of this fragment being, accept new speech input from user and save the same as choices. The Speech fragment implements onClickListener, so it can handle user click. Here’s the code snippet for onCreateView override.

Within the onCreateView, first we get the view instance by inflating the speech layout. So we can find speech and add item buttons and attach onClick events for the same. Also do not forget to reset the textinput.

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
          Bundle savedInstanceState) {
   View view = inflater.inflate(R.layout.speech, container, false);
   Button button = (Button) view.findViewById(R.id.speechButton);
   button.setOnClickListener(this);
   button = (Button) view.findViewById(R.id.addItemButton);
   button.setOnClickListener(this);
   textView = (TextView) view.findViewById(R.id.speechText);
   textInput = "";
   return view;
}

Here’s the snapshot of the speech fragment.

Speech_Fragment

Handling the Speech Fragment onClick event

Let’s see how to handle the onClick event for speech and add item buttons. Here’s the code snippet where a call to displaySpeechRecognizer method is made to start the speech recognizer to accept the voice input. 

@Override
public void onClick(View v) {
   if (v.getId()==R.id.speechButton) {
      displaySpeechRecognizer();
   }
   else if (v.getId()==R.id.addItemButton) {
      if (textInput.length()>0)
         addData();
      else {
         Toast toast = Toast.makeText(getActivity().getApplicationContext(),
                       "No input to add",Toast.LENGTH_LONG);
         toast.show();;
      }
   }
}

Here’s the code snippet for displaying the speech recognizer. We have to create an Intent instance with the appropriate Intent action so the activity can be started with the intent and speech code. 

private void displaySpeechRecognizer() {
    Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
    intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
            RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
    startActivityForResult(intent, SPEECH_CODE);
}

The next important thing after receiving the voice input is to add the input text. Here’s the code snippet for the same. First we show a confirmation screen by making use of an Intent but we really add the textinput through MenuFragment

Please note - Adding a new choice can be done within the speech fragment itself. But we should not forget to refresh the MenuFragment ListView UI.

public void addData() {
   Intent intent = new Intent(getActivity(), ConfirmationActivity.class);
   intent.putExtra(ConfirmationActivity.EXTRA_ANIMATION_TYPE,
                      ConfirmationActivity.SUCCESS_ANIMATION);
   intent.putExtra(ConfirmationActivity.EXTRA_MESSAGE,getResources()
                     .getString(R.string.choiceAdded));
   startActivity(intent);
   MenuFragment frag = ((SampleGridPagerAdapter) MainActivity.getPager()
                       .getAdapter()).getMenuFragment();
   frag.addData(textInput);
}

Clear Fragment

The Clear Fragment is the final or last fragment that gets displayed on the GridViewPager. It extends from a Fragment class and implements OnClickListener. Below is the code snippet of onCreateView method override, where you can see we are setting the onClickListerner to handle the user click.

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {
   View view = inflater.inflate(R.layout.clear, container, false);

   Button button = (Button) view.findViewById(R.id.clearDataButton);
   button.setOnClickListener(this);
   button = (Button) view.findViewById(R.id.clearChoicesButton);
   button.setOnClickListener(this);
   
   return view;
}

Here’s the snapshot of the clear fragment. 

Clear_Fragment

It’s time to take a look into the onClick override and try to understand how we are actually handling the Clear Data and Clear Choices button click. On each of the button clicks, we are showing a custom dialog so the user can take suitable action.

MyDialogFragment is a custom class extends itself from DiaglogFragment and overrides onCreateDialog. It has two methods named positiveClick and negativeClick with no implementation and allows one to override. Below you see how we handle the positiveClick method to clear data by making a call to clearData method.

@Override
public void onClick(View v) {
   if (v.getId()==R.id.clearDataButton) {
      MyDialogFragment dialog = new MyDialogFragment() {
         @Override
         protected void positiveClick() {
            super.positiveClick();
            clearData();
         }

         @Override
         protected void negativeClick() {
            super.negativeClick();
         }
      };
      Bundle bundle = new Bundle();
      bundle.putString("title",getResources().getString(R.string.deleteStatsTitle));
      bundle.putString("message",getResources().getString(R.string.deleteStatsMessage));
      dialog.setArguments(bundle);
      dialog.show(this.getFragmentManager(),"test"); //test is just a tag - not shown to the user
   }
   else if (v.getId()==R.id.clearChoicesButton) {
      DialogFragment newFragment = MyWearDialog.newInstance();
      newFragment.show(getFragmentManager(), "dialog");
   }
}

It’s time to see the clearData code and understand the code behind. In Database class, we have an implementation to clear the menu items and then we show a confirmation activity with the success animation.

Finally there is one important thing we need to do, that is – Notify the SampleGridPagerAdapter by making a call to notifyStatsSetChanged method on its adapter.

public void clearData() {
    Database db = new Database(getActivity());
    db.clearData();
    db.close();

    Intent intent = new Intent(getActivity(), ConfirmationActivity.class);
    intent.putExtra(ConfirmationActivity.EXTRA_ANIMATION_TYPE,
                   ConfirmationActivity.SUCCESS_ANIMATION);
    intent.putExtra(ConfirmationActivity.EXTRA_MESSAGE,
          getResources().getString(R.string.statsDeleted));
    startActivity(intent);
    ((SampleGridPagerAdapter) MainActivity.getPager().getAdapter()).notifyStatsSetChanged();
}

Now let us see how we handle the clear choices button click event. On clear choices button click, you can see there is a code to show a DialogFragment. We are making use of MyWearDialog, which is nothing but a custom dialog fragment as it extends itself from a DiaglogFragment. Here’s the code snippet of MyWearDialog which handles the “OK” and “Cancel” button click events. You can see below the cancel just dismisses the diaglog. On click of ‘OK’ button make a call to clearChoices which makes use of Database instance to clear all choices. 

Please note – After clearing choices, one should never forget to refresh the GridViewPager by notifying the same.

@Override
public void onClick(View v) {
    if (v.getId()==R.id.cancel_btn) {
        dismiss(); //just do nothing
    }
    else if (v.getId()==R.id.ok_btn) {
        clearChoices();//do something
        dismiss(); //then dismiss
    }
}

public void clearChoices() {
    Database db = new Database(getActivity());
    db.clearChoices();
    db.close();
 
    ((SampleGridPagerAdapter)MainActivity.getPager().getAdapter()).listViewDataSetChanged();

    Intent intent = new Intent(getActivity(), ConfirmationActivity.class);
    intent.putExtra(ConfirmationActivity.EXTRA_ANIMATION_TYPE,
            ConfirmationActivity.SUCCESS_ANIMATION);
    intent.putExtra(ConfirmationActivity.EXTRA_MESSAGE,getResources()
          .getString(R.string.choicesDeleted));
    startActivity(intent);
}

Debugging on Smartwatch

Please take a look into the below article to know more about how to debug apps on wearables.

http://www.codeproject.com/Articles/1034397/Android-Wear-through-ADB

References

This article uses codes sample developed by "Martin Knudsen". Feel free to take a look into the below Github link. All attribution to the author is made starting from the beginning of the article.

https://github.com/zaifrun/TodayMenu

Points of Interest

The sample application developed by "Martin Knudsen", helped me a lot in understanding how to develop an Android Wear app. Without which, I would be having a hard time in getting an idea and coding the same on Android Wear.

There's one important thing to mention. Please refer and understand the Android Wearable Design guidelines and perform changes to the demo app to make it a production ready app.

History

Version 1.0 - Initial publication of the article with code sample - 10/14/2015.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here