The MyRuns app that you will build over the next 6 weeks as a set of thematic programming assignments is a simple fitness app for Android. It allows you to capture your runs and walks and view the stats on Google Maps. It uses sensors (viz. GPS, accelerometers) in the phone to infer your activity (e.g., running) in an automatic manner. We start by building out the UI then add GPS, Google maps, the inference model and database components. The app comprises a client that runs on the phone and a backend cloud component using Google Firebase. So you get first hand knowledge on implementing a native app on the phone and a cloud component. That’s very cool.
This is a fun and challenging set of assignments that let’s you get experience with many of the common programming challenges when building an Android app. You can use this experience as a foundation for creating and programming your own ideas as part of the group project at the end of the class. We will also publish the apps from the project phase of the class on Google Play. We will therefore experience design, programming, testing and publishing an app – the complete lifecycle.
What is the idea of this document? It presents a lot of details on the MyRuns app. Think of it as a high-level specification and some pointers on design. BTW, you are free to design your app as you like or follow the guidelines layout here.
##The Small Picture
The class assignments break down into 6 thematic labs that allow you to incrementally and systematically build a complex app. While this document does not attempt to answer all the questions – and in places it gives quite a lot of detail and is vague in other parts – you can download the following APKs for each lab assignment to your phone and play with it. By doing this you will best understand what the app is doing. Your job is to replicate the UI (or modify it) and functionality. The labs area as follows:
Note, as part of the development of the app you will need a set of icons; all of them are provided by Android Studio Image Asset. You can see where to use them by demoing the apks (above). Feel free to use your own icons specially for the app icon if you wish.
Checkout the grading rubric for the programming assignments plus design and coding advise.
There is a lot of information in this document – do not panic.
The document contains information for the complete MyRuns app. Again, do not feel overwhelmed by all the details. What you should do is read the complete document and then focus on what needs to be design and programmed for each lab. You start with lab 1 and finish with lab 6. So this week focus on lab 1 not the others labs. The reason we give out the complete design is so you can keep the complete project in mind as you develop each lab each week. The idea is that you can reuse common components as you move through the piecemeal programming of each successive lab.
Once more: this document contains a lot of material, but we need to know what the app does in the first place to help you digest it while keeping the big picture in mind. Each lab includes an example APK for you to download and run on your phone. This gives you a good idea of what is expect at the UI level and in terms of the new features each lab adds to the cumulative app.
In summary, read the spec. Each week read the lab description (which is more pointers into the document than a detailed lab description) and run the APK. Then start designing and programming the lab. Once you are done testing your code works you should submit the lab to the Git repository following the instructions.
Tip: once you have read the complete document then read it again.
The document has the following sections:
Labs section describes what you need to accomplish for each lab. There are 6 labs
Submission of labs explains what to submit and how.
User Interface section discusses the user interfaces and functionality.
User Interface Implementation section specifies how to implement the user interface.
Database Implementation section describes how to design and implement the database.
Service Implementation discusses the design considerations for service implementation.
The following labs make up the MyRuns app – we just list the labs here and discuss them in more detail later.
We are using Git for the submission of programming assignments. You should read those note before proceeding.
Your solutions must be implemented in java on the Android SDK.
We strongly recommend that you use a version control system to keep your work organized. It means that, SVN should not be used only when you submit project lab assignments.
For each project lab assignment, you should include one README file to include project description.
Note: The instruction of submission and git repository for everyone will be annonced as soon as we set them up. Check here again before the first assignment submission.
This section discusses the user interface (UI) and main functionality.
When you launch the app (i.e., MyRuns1) you are bought to the signin view, as shown in the figures below. After registering an account (i.e., creating a profile as described here you can actually signin to the app with the account information you have created – using the email and password used during the registration phase. When you sigin you are brought to the main activity that renders a blank view. You are free to implement MyRuns1 as you wish but since you are getting started with the frist assignment here is one approach you could follow:
The launched activity (let’s call it SignInActivity) renders the view that you see when you bring up the apk. First time through you can try and sign in (by clicking the sign in button) but you will (not surprisingly) fail because like most apps on the net you have to register first using an email (as the username) and password – the app gives you a toast saying the ‘’email or password is incorrect’’ if you try and sign in before registering. Note, that the widget (almost all widgets) understands that an email widget wants an email address and makes suggestions when you are registering or signing in. Similarly the password widget allows you to view or hide the password while typing. Smart hey?
If you click the register button then the SignInActiviy will start another activity (let’s call it the ProfileActivity) to complete registration process by registering the users profile. You get a chance to take a photo using the camera or from the galley (BTW, that’s why you need to code up the app permissions to ask the user if its OK to use the camera – you will see the permission dialogs during the registration) and fill in a number of other fields; again, the app is smart because when filling in the phone widget for example it makes telephone number suggestions. Once you have saved your registration profile (by clicking the register item in the app bar) you are returned to the SignActivity view.
Now you can sigin by entering your email and password. The next bit is boring – the SignInActivity starts the MainActivity and renders an empty view. We will use this MainActivity to build out our app in Lab2. There are a number of small parts to the UI that you can find by playing withe the views. Cover all cases you discover. You can also look at the rubric to see what we grade against. OK, this has been a bit long winded and we are unlikely to be so prescriptive for future labs, but you are just getting going so this level of specificity may be helpful.
The main UI consists of two tabs: start & history. When the app starts (if account is signed in), you are brought to the main activity and it focusses on the start tab. The app offers a number of modes to record workouts – for example, manual input which is a bit tedious and the GPS mode as shown in the figures. You can view your workout history by tapping on the history tab. In the “Settings” menu, you can set/change some preferences of the app. You can also view/edit the profile information through “Edit Profile” menu. We are going to introduce each of these components in the following section.
You can record your workout in three ways: manual input, GPS or automatic modes. This section describes how each method works.
Manual entry is activated when you select “Manual Entry” for the “Input Type” using the start tab. You can specify the type of activity in “Activity Type” spinner. When you click “Start” floating button, you will be brought to the manual input interface, where you can input the details of your workout. The table below shows the information type (e.g., heart rate), the type of widget used for data entry (e.g., TimePickerDialog) and short note on how the data could be stored (e.g., Store the timestamp in long) for data associated with a workout that the use can manually enter.
The corresponding details of exercise are listed in the following table.
When the user clicks on any exercise/workout entry, the app should show a data entry dialog. When user taps save, the entry should be saved to the database. More on the database later.
The GPS entry mode is when the user selects “GPS” for the “Input Type” on the start tab. You can specify the type of activity using the “Activity Type” spinner. When you click (it’s more tapping and not clicking on a smartphone but you get my drift) “Start”, you will be brought to a map interface, where you can see your location trace and some information about your current activity.
The automatic entry mode is when the user selects “Automatic” for the “Input Type” using the start tab. It is similar to the “GPS” mode except the activity type (i.e., walking, running) is automatically inferred using a classifier via Google Activity Recognition API. The real time activity inference will be shown in the status area. The final activity type is determined as the activity label (e.g., walking) that has been inferred more times than other activity label over a defined period of time. Also, a notification icon is shown in the notification area, indicating the background service is recognizing your activity.
The workouts/exercise records should be send to remote server and stored in the Firebase cloud. see lab 5!
The History tab shows the list of all recorded workout entries. Each entry consists two lines: title and text. The title includes the activity type and time of the activity while text captures distance and activity duration.
When the user clicks on an entry, assuming the entry was input manually, then the app shows an interface containing the status, as shown in the screenshot below. Otherwise the app opens the map interface to show the trace. When user clicks “DELETE” in the action bar, the entry should be removed from the history tab.
As discussed in the User Interface Walk-through section, you can set up the user profile (open a new activity), privacy setting (toggle button), unit preference (pop-up dialog with radio boxes) and sign out in the “Menu”. When clicking sign out, you should finish all open activities and get back to the sign in activity. Other setting modifications should be saved automatically.
When the user clicks the “Edit Profile” a new view is presented, as shown in the screenshots above. The user should be able to input their photo, name, email, phone number, etc. When the user click the “Change” button, they can choose to take a picture using the camera or select one from the gallery (as shown in the middle figure). The interface should allow the user to take or get a picture and crop it, as shown in the picture on the right). The following table shows detailed definitions of the user profile.
This section provides an overview of MyRuns’ system design principles. We use MVC as the architectural design pattern. Model–view–controller (MVC) is one of many software design patterns, which has been widely adopted to develop software architecture for user interfaces. The design consideration of MVC is to separate business logic from user interfaces. In what follows, the design of profile management and tracking are illustrated.
The design of the profile management is shown in the diagram below. As you can see from the previous section, the user can input, view, and change their profiles. The user profile contains items such as name, gender, photo, phone, gender, and class. Therefore,we can define a class to represent this data structure. This profile class also handles where to save the profile data and how to retrieve the profile data, which is transparent to other module. The profile definition is the model component in MVC.
We design the user interface in the profile activity layout. It defines how the profile data will be presented to the user and how the user update the profile in term of where they input the information and how they signal the system to save the profile. The profile activity layout is the view component in MVC.
The profile activity glues the profile definition and the user interface together: it gets data from the profile definition then pushes the data to the profile activity layout to show the profile to the user; when the user click the Save button, the activity is signaled. It retrieve the updated data from the view and push it to the profile definition to save.
Tracking is the most complex module on MyRuns, which is shown below.
As you can see from the diagram, the model has three sub-modules: tracking service, trace data structure, and trace database. The trace data structure defines what data is in a trace (e.g., GPS coordinates, time, duration, etc). All traces are saved in the trace database. The tracking service collect GPS coordinates as well as other trace related information.
The MapView layout defines how the trace data will be presented to the user. It shows the live location trace, as well as location traces from the history.
The MapView activity glues the user interface and the underline data structure, database, and tracking service together. If the user is tracking, the activity starts the tracking service, which creates an instance of the trace data structure. The service insert GPS coordinates to the trace instance, and notifies the activity, which, in turn, updates the MapView layout to show the latest location trace. After the user finished tracking, the activity stops the service, then save the trace instance to the database.
If the user want to view a trace from history, the MapView activity retrieve the record from the database, then updates the MapView layout to show the trace.
The implementation detail is described in later sections.
This section provides an overview (read hints without details) of the design and implementation of the user interfaces.
The main activity is a navigation interface to access different interfaces by clicking different tabs on the bottom of the screen. The tabs use a bottom navigation view to add multiple Tabs to one activity. You need to create a view in the activity’s layout and menu items:
<android.support.design.widget.BottomNavigationView
android:id="@+id/navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="0dp"
android:layout_alignParentBottom="true"
android:layout_marginStart="0dp"
android:background="?android:attr/windowBackground"
app:menu="@menu/navigation" />
After creating the navigation bar, you can add tabs through menu items:
// res/menu/navigation.xml:
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_start"
android:icon="@drawable/ic_play_navigation_black_24dp"
android:title="@string/title_start" />
<item
android:id="@+id/navigation_history"
android:icon="@drawable/ic_history_black_24dp"
android:title="@string/title_history" />
</menu>
Each Tab is associated with a class inherited from the Fragment class. When a tab is clicked, the corresponding interface will be rendered above the navigation bar.
You need to implement the BottomNavigationView.OnNavigationItemSelectedListener interface to select, and BottomNavigationView.OnNavigationItemReselectedListener to reselect different Tabs (and their respective fragments).
An activity layout xml must be included two components: ViewPager which is a container for inflating different fragment views, and a BottomNavigation view that explained above. Please refer to Android API documents to learn more about using ViewPager.
Note that the action bar options menu allows the user to navigate to the Profile and Settings activities. Consider the MainActivity as the “home screen” or parent activity and the ProfileActivity and SettingsActivity as children. Then the user can use the up button in the action bar in the child views to navigate back to the parent. See Providing Up Navigation to see how to do this.
The start tab allows the user to enter exercise information manually as discussed earlier. The root layout could be LinearLayout. You can also use other layout types if you wish (see ConstraintLayout). You will need spinner widgets for the layout to create a drop-down list for both the input type and activity type options.
You also need to implement the setOnClickListener for the “Start” FloatingActionButton. Once the “Start” button is clicked, the app should fire the different activities according to the “Input Type”. If the input type is “Manual Entry”, then the ManualInputActivity allows the user to enter exercise stats; when “GPS” or “Automatic” is selected, the MapDisplayActivity should be shown to the user. The interfaces for both activities are discussed in Start Tab section. The “Activity Type” should be passed to the new activities by putting the value to the intent’s extras. Activity types may include Running, Walking, Standing, Cycling, Hiking, Downhill Skiing, Cross-Country Skiing, Snowboarding, Skating, Swimming, Mountain Biking, Wheelchair, Elliptical and Other.
The user enters exercise information using dialogs driven by the ManualInputActivity. Therefore, you need to implement all the related dialogs in a DialogFragment, say MyRunsDialogFragment, or implement a DialogFragment for each dialog. After the user is done with inputting information using dialogs, the ManualInputActivity will store the input temporarily in an ExerciseEntry object. When the user clicks the “Save” button, the app saves the temporary data associated with the exercise stats in the database – so an insert in the database is going to occur when an exercise object representing all the information associated with a manually input single exercise is inserted as a new row in to the database. That was a torturous sentence class ;-) In dialog fragment’s onCreateDialog(), you should set which activity’s method should be called when the user clicks the OK button.
Please read Android API documents for details on DialogFragment.
The MapDisplayActivity layout has three part: map view, status and action bar. In order to overlay the status on the map, you can use FrameLayout and put both mapfragment and a LinearLayout which contains the textviews needed to show the status. You need to add save button in action bar (see Adding and Handling Actions) and also setDisplayHomeAsUpEnabled to navigate to the home activity. The following layout skeleton shows each widget.
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MapsActivity">
<fragment
android:id="@+id/map"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MapsActivity" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:orientation="vertical" >
<TextView
android:id="@+id/text_activity_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="#000" />
<TextView
android:id="@+id/text_cur_speed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="#000" />
.
.
.
</LinearLayout>
</FrameLayout>
In order to show the map properly, you need to get a Google MAP API key for your app. You can check Android developers to find out how to do that.
The location trace needs to be updated once a location update is available and processed. You need to create a starting marker marking the start position, ending marker marking your current location and all lines showing your trace. The starting marker needs to be place at the first location coordinate. Draw a single polyline along all the collected location coordinate. Place the ending marker at the last location coordinate.
Exercise status include activity type, average speed, current speed, climb, calories and distance. Please refer to the demo apk for details.
MapDisplay has two modes: displaying history entry and showing the live location trace. To display a history entry, you need to figure out a way to retrieve the data from the database. To show the live location trace, you need to receive an update notification from the TrackingService and update the map accordingly. Please refer to History Fragment and Service Implementation for more details.
The History Fragment loads all exercise entries from the database then displays the entries as a ListView/RecyclerView. As mentioned in class, a ListView uses an adapter to show data. You can implement an custom adapter class, for example, ActivityEntriesAdapter, which extends ArrayAdapter<ExerciseEntry>/RecyclerView.Adapter<ExerciseEntryViewHolder> that exposes data from an array to the widget. You need to implement how the ListView/RecyclerView displays each record. This can be done by using the override of the getView()/onCreateViewHolder method in ActivityEntriesAdapter. There should be two rows for each record. The format of the first row is as follows: <Activity Type> <Date> The second row’s format is: <Distance><Duration>. One such example is showing in User Interface Walk-through (middle picture). You need to handle user’s unit preference as well. You need to show the distance in user’s selected unit (metric or imperial units, see Settings Activity)
ListView’s onListItemClick should be implemented when the user clicks on history entries. When the selected entry’s input type is manual entry, the app opens DisplayEntryActivity to show the details. Otherwise it opens MapDisplayActivity to show the trace along with the status. You need to pass exercise entry’s unique database id to next activity, so that they can retrieve the entry from the database.
History list also needs to respond to history entry updates. When the user delete an entry through the web interface (see later), the cloud will send a message to the app and the app needs to update the database. When the user is viewing a history fragment, the app should be able to update the view to reflect any changes.
The DisplayEntryActivity has two jobs: 1) retrieve and display all the columns of a specific exercise entry to the list of TextView; and 2) setup a click listener for the option menu to call the deleteEntryInDB function in the ExerciseEntryHelper (see database section).
The user can view a summarized list of all exercises using the tab history fragment. If the user clicks on one of the summaries on the list view then detailed information associated with the single exercise is displayed by DisplayEntryActivity.
Importantly, the user can delete the whole exercise entry by clicking on the “DELETE” button in the upper right hand corner of the UI layout – take a look at the app once you click on an summarized entry in the list view.
Note, you could if you wish use the same activity (i.e., ManualEntryActivity) to input a new exercise entry (navigated from the start tab) and to display an existing exercise entry (navigated from the history tab). In this case you would not need to code up a new DisplayEntryActivity. Rather, you could simply use ManualEntryActivity to support both operations. You would need to pass information in the intent that started the composite ManualEntryActivity to determine if it was started from the “start” or “history” tabs. You would also have to make sure the menu presented the user with the option to “SAVE” (started by start tab) or “DELETE” (started by history tab) the entry. This design would allow you to use a single activity to support the functionality of both ManualEntryActivity and DisplayEntryActivity. That’s nice. Less components to maintain. Supporting a pattern.
Settings Activty sets up various application preferences settings, it inherits from PreferenceActivity. We need to assign a PreferenceFragment to the activity using getFragmentManager() method and then easily call addPreferencesFromResource in the onCreate() method of the fragment to load the preferences UI from an XML resource. This XML file is called preference.xml in directory: res/xml. The preference UI is displayed when the “Settings” menu is clicked.
Most of the elements in the Settings Activity are common widgets, e.g., SwitchPreference, ListPreference. There is an element for class homepage. When clicking this element should invoke and open a browser and go to our class webpage. This is a nice example of one of your components using other apps on your phone; that is the browser. This is achieved using the PreferenceScreen XML tag – see PreferenceFragment for more details.
Hint: you can directly set android:action and android:data for an intent inside XML file.
Another element is for user’s profile signing out. Clicking on sign out should go to the sign in activity and request for a new login.
The UI in Android is controlled by an XML file in the res/layout folder; in this case you need to specify the layout, for example, in the activity_profile.xml. The activity_profile.xml layout should consist of a linear hierarchical of widgets and layouts. Android provides a “drag and drop” method as part of the graphical layout – so you can either directly edit the xml file or use the graphical tool to design your layout, or both: you can easily switch between both modes as shown in class. Please note we have not covered fancy UI design or specialized widgets. Therefore you should use the standard widgets in the design view of the activity_profile.xml to drop standard widgets into the view. For example, the SignInActivity’s view includes EditText for email and password entry. Similarly the activity_profile includes standard widgets for ImageView, RadioButton, etc. to implement the profile
For more advance UI feel free to coded up in the example apk we handed out. However, as mentioned above simple layouts and standard widgets are fine for submission of Lab1. Again, you are not expected to implement the exact UI details of the MyRuns1 apk handed out this. However, if you feel adventurous then read on: The layout of the apk handed out includes a ScrollView element. Inside the ScrollView use a vertical LinearLayout/ConstraintLayout. You need to program all elements contained in the layout. The titles for all elements (e.g. Name, Email, Phone etc.) are TextInputLayout widgets. The editable boxes are TextInputEditText widgets. And the buttons for gender element are RadioButtons grouped as a RadioGroup. You will assign IDs to your widgets in the XML code, which you will refer later in your Java code. The labels in each TextInputEditText are specified by property “android:hint”.
Please set different keyboard layout for numerical and text input box using android:inputType property of EditText. By doing this, when you first interact with the text box, the keyboard layout is optimized for your type of input: for phone number field, the keyboard will be all numbers with larger buttons, etc. For email, the keyboard will be a Qwerty keyboard. Note that all properties specified in the XML file can also be specified and modified in the Java code. But use XML as much as possible rather than programming the UI. If the appearance of the UI changes as the program executes, you will need to do some UI work in the Java code.
You should implement two private helper function methods in the activity to help load user data that has already been saved - called loadProfile() - and one to save the user data – called saveProfile(). Consider saveProfile(): this function saves the user input data using a SharedPreference object, as discussed in class. After calling the helper function the activity simply displays some toast to the screen letting the user know that the data is saved. Similarly, when the application is started (either for the first time or restarted) the activity needs to load the user data using the loadProfile() helper. Your helper function calls loadProfile() method in onCreate() and uses the same SharedPreference object to load the data and display it to the screen. Think about the edge case the first time that the app runs when no previous data is saved. You will need to make sure some default data (e.g., empty string elements) are displayed. You will need to add a ImageView for displaying the photo and a button to trigger a Dialog, which will ask the user to either use camera or existing photos as shown above (central image). The selected photo needs to be cropped to fit the size of the ImageView. Also, you need to handle screen rotations. Recall in the class, a screen rotation will trigger onCreate(), which will remove the unsaved pictures in this case. You can utilize onSaveInstanceState() to save the temporary profile picture and reload it in onCreate().
You will need to add a ImageView for displaying the photo and a button to trigger a Dialog, which will ask the user to either use camera or existing photos as shown above (central image). The selected photo needs to be cropped to fit the size of the ImageView. Also, you need to handle screen rotations. Recall in the class, a screen rotation will trigger onCreate(), which will remove the unsaved pictures in this case. You can utilize onSaveInstanceState() to save the temporary profile picture and reload it in onCreate().
Note, the triggered dialog invokes MyRunsDialogFragment that inherits DialogFragment. We will reuse the MyRunsDialogFragment class in future labs to handle all customized dialog boxes for the MyRuns app. In MyRunsDialogFragment, you can differentiate various dialog fragments by supplying distinctive dialog IDs in the onCreateDialog method.
Setting the profile image is challenging. When user clicks the “Change” change button, the activity shows a dialog asking if the user wants to take a picture or select one from the gallery as discussed above. Based on the users input the activity starts the camera or galley apps. You need to use startActivityForResult() to get results back from the camera or galley app, as discussed in class. Consider the following workflow: start an activity to get an image (either by camera or gallery), the activity returns the results to the profile activity, the profile activity then start another activity to crop the image. Finally, the profile activity gets the cropped image back from the cropping activity and displays it in the image view. You can use MediaStore.ACTION_IMAGE_CAPTURE to open the camera, Intent.ACTION_PICK to open the gallery.
This section discusses the design and implementation of the database.
ExerciseEntry is the core data structure of the app. It defines what information a workout entry should have – we use the term workout and exercise interchangeably in this document. It can be defined as below.
public class ExerciseEntry {
private Long id;
private int mInputType; // Manual, GPS or automatic
private int mActivityType; // Running, cycling etc.
private Calendar mDateTime; // When does this entry happen
private int mDuration; // Exercise duration in seconds
private double mDistance; // Distance traveled. Either in meters or feet.
private double mAvgPace; // Average pace
private double mAvgSpeed; // Average speed
private int mCalorie; // Calories burnt
private double mClimb; // Climb. Either in meters or feet.
private int mHeartRate; // Heart rate
private String mComment; // Comments
private ArrayList<LatLng> mLocationList; // Location list
}
You need to implement methods to set/get these attributes. For example, you need to implement methods to convert mLocationList (of LatLngs) to a JSON array to save in the database and then convert JSON array to array list of LatLng (mLocationList) when retrieving the JSON array of LatLngs.
There is only one table needed. It can be defined as follow:
CREATE TABLE IF NOT EXISTS ENTRIES (
_id INTEGER PRIMARY KEY AUTOINCREMENT,
input_type INTEGER NOT NULL,
activity_type INTEGER NOT NULL,
date_time DATETIME NOT NULL,
duration INTEGER NOT NULL,
distance FLOAT,
avg_pace FLOAT,
avg_speed FLOAT,
calories INTEGER,
climb FLOAT,
heartrate INTEGER,
comment TEXT,
privacy INTEGER,
gps_data TEXT );
The _id is the primary key. In database, the primary key uniquely identifies a record. “AUTOINCREMENT” indicates that the value will be set automatically and incrementally. The field gps_data stores all the GPS coordinates. We use TEXT to save all the coordinates. As mentioned in Data structure, you should store the location list in gps_data.
The design principle of the database operations is to hide database operation details from app’s other modules. That is, other modules, e.g. history tab, do not need to operate the database directly to get data entries from or save data entries to the database. We define a helper class to encapsulate all the database operations. You can use SQLiteOpenHelper to implement your helper class. All necessary methods are defined as follows (see below). Also, in class we mentioned that we do not want to block the UI with insert() and query() operations on the UI – the UI may freeze if inserting or querying large objects. We discussed some solutions where the insert() and query() are off loaded worker threads independent of the UI thread; the advantages with this approach is that the UI is never blocked.
// Constructor
public ExerciseEntryDbHelper(Context context) {
// DATABASE_NAME is, of course the name of the database, which is defined as a tring constant
// DATABASE_VERSION is the version of database, which is defined as an integer constant
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
// Create table schema if not exists
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_ENTRIES);
}
// Insert a item given each column value
public long insertEntry(ExerciseEntry entry) {
}
// Remove an entry by giving its index
public void removeEntry(long rowIndex) {
}
// Query a specific entry by its index.
public ExerciseEntry fetchEntryByIndex(long rowId) {
}
// Query the entire table, return all rows
public ArrayList<ExerciseEntry> fetchEntries() {
}
In each method defined above, you need to get a database object using getReadableDatabase() or getWritableDatabase(), then do the reads/writes. Remember to close database cursors and database object after you are done.
As you can see from the class interface definitions, the input or output of these methods are all ExerciseEntry objects. When the history tab is loaded, it uses fetchEntries() to get the list of all entries. Then the list can be bound to an adapter so that the list view can show the entries. When the user selects an entry in the history tab, the app can use the fetchEntryByIndex() to get the entry’s details and display them either in a display activity (if it is a manual entry) or in the map (if it is not a manual entry). If an entry is generated, you can use insertEntry() to insert it to the database.
Hint:
To implement the database in the activities, you should create a database helper object. For example, if you define your helper class as ExerciseEntryDbHelper, you should create the helper object like this:
ExerciseEntryDbHelper exerciseEntryDbHelper = new ExerciseEntryDbHelper(this);
After this, you can use this object to operate the database. For example, if you want to remove an entry from the database:
exerciseEntryDbHelper.removeEntry(entryID);
This section discusses the design considerations for service implementation.
There are two tracking mode. When the user clicks the start button in start tab, the onStartBtnClick handler passes the parameters (i.e., input type (GPS/automatic) and activity type) to MapDisplayActivity. MapDisplayActivity is also used to display the history entry, so it needs to determine if it should display a history entry or start a new entry. If it needs to display an entry, then StartTabFragment should pass the row id to it so that it can retrieve the entry from the database.
If MapDisplayActivity needs to record a new workout, it starts the tracking service, get the exercise entry from the service, then update the map when it receives an update from the tracking service. The following flow chart shows the work flow of tracking service.
You need to start the service explicitly instead of using the bind the service only (you can try to figure out why yourself). Also, you need to make sure the activity will not leak bound services. You can check this by inspecting logcat output. The key idea is that TrackingService creates an exercise entry and updates it when a new location update or activity inference update is available. The TrackingService sends a message to the MapDisplayActivity, so that the activity can update the map in a timely fashion. MapDisplayActivity gets the exercise entry when it is bound to the service, and saves the entry in the database once the tracking is completed.
Hints:
In the automatic mode, the app continually infers the activity type using accelerometer data. To implement this, you need to use Google ActivityRecognitionClient. You should implement the API methods in your tracking service in the background and broadcast the result of recognition to the MapDisplay activity. For more information about implementation, read google’s document for activity recognition.
This final lab has two phases:
the first one is all about Firebase, and;
the second part is about communicating with the MyRun5 server through REST API using Android Volley API.
The major goal of MyRuns5 is to extend MyRuns architecture so that you’re able to do the following:
Authenicate and log into the app and backup your activity data on a remote ``Firebase’’ server
Share your activity data by uploading it to a remote ``social’’ server using HTTP
Retrieve activity data from your peers by creating a leaderboard in your app and downloading all stored exercises on the ‘’social server’’ that have been boarded by all MyRuns5 uers.
In the first phase, you will store the data in the Firebased cloud server, and retrieve it back to the phone (maybe new one). Clouds are the best persistent way to store users’ data. Here you should use the Firebase Authentication API and Firebase Realtime Database. Both are awesome and easy to use once you understand the plumbing.
Get started by looking at the class slides, and read Firebase Auth and Firebase Database.
After setting up Firebase SDK, and adding your project to Firebase console, you will use Firebase Auth and Realtime database for your project. You can benefit from Firebase Auth to support multi users signup and signin for your app, and the realtime database to sync the local database with the cloud server’s
Let’s discuss how your app authenticates with Firebase..
There are several methods to authenticate a user provided by Firebase, but you’re only going to use the Email/Password method –dDon’t forget to enable this method under the Authentication section in your firebase web console. Once you have done that, you can call two simple method provided by the API for signing up and signing in. MyRuns already has places holders for this code in the LoginActivity and ProfileActivity to register and login an account, respectively. You just need to call the signup method from Firebase authentication object to register an account in ProfileActivity (Register), and then log it in by calling the signin method inside the LoginActivity. You can find Firebase sample code here as well as the samples given out in the class slides
After creating a new account through the app, you can see the created account in the firebase console under Authentication. Check it out. This means that this account is stored in the Firebase server and the user can now login with the account on different platforms and phones. Once authenticated you can sign in from any MyRuns5 running on any phone. Try it.
Here is some sample code for Authenticating using email and passwords. Make sure you understand this code – do not blindly copy it.
FirebaseAuth mAuth = FirebaseAuth.getInstance();
// Sign up
mAuth.createUserWithEmailAndPassword(email, password).addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
progressDialog.dismiss();
if(task.isSuccessful()){
// Signup is completed!
}else{
// Failed!!!
if(task.getException() != null){
Log.e(TAG, "createUserWithEmail:failure", task.getException());
}
}
}
});
// Sign in
mAuth.signInWithEmailAndPassword(email, password).addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
showProgress(false);
if(task.isSuccessful()){
// Signin is completed!
}else{
// Failed!!
if(task.getException() != null){
Log.e(TAG, "signInWithEmail:failure", task.getException());
}
}
}
});
Once you have authenticated with Firebase you are ready for action: reading and writing to the realtime database.
The purpose of using this Firebase database service is to sync local data on the phone with the Firebase backend server. You need to come up with a structure to store users’ data which are already saved on the phone’s database into the server.And also retrieve them for users even if they are using your application on their new phones. Again you can easily save data into the Firebase database by calling CRUD (create, read, update and delete) methods provided in the Firebase database API. See database sample codes on the Firebase website or class slides.
After saving the data in the cloud database, you can see the data on the Firebase console under Realtime Database section and even edit them. Data is stored as a JSON tree. You can click on the records in the store.
Note: you should not sync exercises that have aleady synced. You should sync only new exercises. So you need to keep a flag/variable to mark an execise as being synced or not. Think about it, we’ll check your code to make sure you are not blindly syncing all exercises everytime
OK. Here is some reference code for updating the Firebase database for exerciseEntry objects. Again, do not blindly copy this code – you can use it as a template but understand its operation first.
DatabaseReferenc mDatabase = FirebaseDatabase.getInstance().getReference();
// Simple snippet for inserting data
mDatabase.child("user_"+Constant.getEmailHash(preference.getProfileEmail())).child("exercise_entries").child(exerciseEntry.getKey()).setValue(exerciseEntry)
.addOnCompleteListener(getActivity(), new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
if(task.isSuccessful()){
// Insert is done!
}else{
// Failed
if(task.getException() != null)
Log.w(TAG, task.getException().getMessage());
}
}
});
// Listener for being informed of data changes
mDatabase.child("user_"+Constant.getEmailHash(preference.getProfileEmail())).addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
// This method is called once with the initial value and again
// whenever data at this location is updated.
}
@Override
public void onCancelled(DatabaseError databaseError) {
// Failed to read value
Log.w(TAG, "Failed to read exercise entries.", databaseError.toException());
}
});
In the second phase, you should send (post) your exercise data already stored on your phone and Firebase cloud database to the social server. This is different from the Firebase database server. We use a HTTP POST method. HTTP POST will upload all your exercises as JSON objects to the social server. Because all users upload their data you can download all MyRuns exercises from all users. You can get a listed of all boarded exercises from the social server using a HTPP GET method.
We have created a server (called social server) which hosts a database and exposes an APIs to upload and share (download) the complete collection of exercise data with you and your classmates. Fun!
Simple hey. You use GET and POST all the time based on links you click on everyday using your favorite web browser. But now you want to do the same thing but inside your MyRuns5 app. Android provides a set of APIs to do this. Let’s explain more.
When you want to push a new exercise to be boared on the social server you use POST. These are parameters which your POST request sends as a JSON object when uploading the exercise data to the server: (All are strings)
For example: { “email” : “shayan@dartmouth.edu” , “activity_type” : “Walking” , “activity_date” : “2018-01-20” , “input_type” : “GPS” , “duration” : “20” , “distance” : “1”}
The server response to the POST method is a JSON object: {“result” : “Success”} if it’s successful, and {“result” : “Failed”} if it’s failed. You need to check for success and failure. Sometimes servers die. Internet goes down (not often).
To send this POST method, you can use JsonObjectRequest method of Android Volley API, and pass a JSON object of parameters to the method.
JSONObject jsonObject = new JSONObject();
try {
if(preference.isAnonymousPost())
jsonObject.put("email", Constant.getEmailHash(exerciseEntity.getEmail()));
else
jsonObject.put("email", exerciseEntity.getEmail());
jsonObject.put("activity_type", exerciseEntity.getActivityType());
jsonObject.put("activity_date", exerciseEntity.getDate());
jsonObject.put("input_type", exerciseEntity.getEntryType());
jsonObject.put("duration", exerciseEntity.getDuration().split(" ")[0]);
jsonObject.put("distance", exerciseEntity.getDistance().split(" ")[0]);
} catch (JSONException e) {
e.printStackTrace();
}
JsonObjectRequest jsonObjectRequest = new JsonObjectRequest
(Request.Method.POST, Constant.BASED_URL+"/upload_exercise", jsonObject, new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
try {
if(!response.has("result") || !response.getString("result").equalsIgnoreCase("success")){
// Server operation is successful.
}
} catch (JSONException e) {
e.printStackTrace();
}
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
// TODO: Handle error
if(error.getMessage() != null)
Toast.makeText(getContext(), error.getMessage(), Toast.LENGTH_SHORT).show();
}
}){
};
RequestQueue queue = Volley.newRequestQueue(getContext());
queue.add(jsonObjectRequest);
Once you have posted your data you want to get all the data back from the social server including your posted data and all other users’ data. You should be anle to see your friends shared data in the response. Cool!
To get all exercises stored in the server you need to call the HTTP GET method on the server. The server is waiting for requests and will serve you. For this method, you don’t need to pass any parameter to the server. The server response to this method is a JSON array – each element is a JSON object for an exercise. Simple.
Exercise Sharing API Sample GET Response:
[{“distance”: “190”, “input_type”: “Automatic”, “activity_date”: “2018-01-20”, “duration”: “30”, “email”: “kmasaba21@gmail.com”, “activity_type”: “Running”}, {“distance”: “2000”, “input_type”: “Automatic”, “activity_date”: “2018-04-23”, “duration”: “0”, “email”: “shayan@gmail.com”, “activity_type”: “Sleeping”}]
To implement GET, you need to use JsonArrayRequest of Volley API that its functionality is as same as JsonObjectRequest with a difference in the type of response. See Volley API for code examples.
Please note the social server is a simple server. Please do not launch a DDoS attack on it by calling POST or GET requests in a loop. That will take the server down! Also, you should avoid redundant upload of exercises that are already uploaded and boarded. We’ll check your code to make sure you take care of this use case!
Here is a code example for GET.
JsonArrayRequest jsonArrayRequest = new JsonArrayRequest
(Request.Method.GET, Constant.BASED_URL+"/get_exercises", null, new Response.Listener<JSONArray>() {
@Override
public void onResponse(JSONArray response) {
// Parse the JSON array and each JSON objects inside it
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
// TODO: Handle error
if(error.getMessage() != null)
Toast.makeText(getContext(), error.getMessage(), Toast.LENGTH_SHORT).show();
}
});
RequestQueue queue = Volley.newRequestQueue(getContext());
queue.add(jsonArrayRequest);
The API for sharing exercise data returns a json array of all exercises (for all users) which are stored on the social server. Once you receive the response, you need to parse the json objects and load them into a list as shown in the demo apk (you need to add a Board Fragment to your project to do this).
Requirements:
Create a new Fragment called BoardFragment and add it to MainActivity as the third tab of bottom navigation (after StartFragment and HistoryFragment).
BoardFragment should have a menu item which when selected, it uploads your records and then gets the latest activity records from the social server (sharing and retrieving).
Create any extra models that you might need for the list of retrieved data.
If user checks Anonymous posting in the SettingActivity, you should send SHA1 or MD5 hash of the user’s email instead of the email to the social server to keep the user identity anonymous.
The 5 labs serve as checkpoints along the way to build the complete lab. Follow each lab after reading the complete documentation. We provide the APK for each lab and the pointers below as well as the detailed spec (above). In what follows, we discuss each lab.
Demo the lab: You can download MyRuns1.apk and run the app to see how it operates. Use that knowledge to fill in the gaps in the above document.
This is the first in a series of labs that allow you to develop the MyRuns App to capture and display your physical activities using your Android phone. This lab focuses on the user registering their profile and hence creating an account. Once the user has registered an account they can simply sign directly into the app using the email and password set up during the registration phase. The user enters profile: i.e. name, email, password, phone number, gender, major and Dartmouth class, and then sign in the app with the created account credential.
The UI design for the profile activity is specified in Edit Profile Menu section, and the implementation design is specified in Profile Activity section. The application is defined in the AndroidManifest.xml file. You specify all of these files in XML and Java code. Recall the manifest captures the key information about the application to the Android system, information the system needs before it can run any of the application’s code – for example, the activity name, etc. Typically you will update the manifest for example if you have an application with more than one activity.
Starting with Android API level 23, users need to be asked for permission to use phone resources – such as in the case of MyRuns1 external memory and the camera. The user is asked by the app if permission is granted to use, say, the camera – and the user can agree or not. This extends in MyRuns to other resources as we move through the sequence of labs to GPS and other phone resources.
You need to add boilerplate code in your activity to check for permissions (see app permissions for more details). For my Runs1 you need permission for using the camera (we generously give you an example of that code below) and to read and write to external storage. Note, you also have to add code to the manifest to make all persmission sing and dance.
// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(thisActivity,
Manifest.permission.Camera)
!= PackageManager.PERMISSION_GRANTED) {
// Permission is not granted
// Should we show an explanation to users the reason that we need the permission?
if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
Manifest.permission.Camera)) {
// Show an explanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
} else {
// No explanation needed; request the permission
ActivityCompat.requestPermissions(thisActivity,
new String[]{Manifest.permission.Camera},
MY_PERMISSIONS_REQUEST_CAMERA);
// MY_PERMISSIONS_REQUEST_CAMERA is an
// app-defined int constant. The callback method gets the
// result of the request.
}
} else {
// Permission has already been granted
}
Then you should handle the permission request response by overriding the onRequestPermissionsResult(…) method in the activity:
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case MY_PERMISSIONS_REQUEST_CAMERA: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// permission was granted, yay! Do the
// camera-related task you need to do.
} else {
// permission denied, boo! Disable the
// functionality that depends on this permission.
}
return;
}
// other 'case' lines to check for other
// permissions this app might request.
}
}
Note, you need to read the User Interface section – we’ve included some design notes on how you could design the views/ controllers for lab. You cannot use println or printf for logging purpose with Android – for control flow or looking at programming state such as variables. As discussed in the notes Android supports a logging capability that you can add to your code. Take a look at the class notes that uses Log.d(TAG, ..). Setup your on TAG and add Log.d()s to all your methods – also checkout Log. If your program crashed, don’t be panic. Look into the system log in the LogCat window, it will print out the function call stack upon crash. If you see logs with red font then that is associated with the exception. Most of the time you will find what causes your crash. We will discuss debugging techniques in more detail next week in class.
Demo the lab: You can download MyRuns2.apk and run the app to see how it operates. Use that knowledge to fill in the gaps in the above document.
In this lab, you need to complete all the user interfaces, including all the activities, main activity’s Action Tabs. You should be able to navigate between all of the activities.
You need to continue implementing the profile activity. In Lab 1, you have implemented setting profile picture by taking a photo. As described in Settings and Edit Profile Menu section, user should also be able to select a picture from the gallery as their profile image. To make your life easier, we provide you the code to convert image URI to real file path.
private String getRealPathFromURI(Uri contentUri) {
String[] proj = new String[] { android.provider.MediaStore.Images.ImageColumns.DATA };
Cursor cursor = getContentResolver().query(contentUri, proj, null,
null, null);
int column_index = cursor
.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
cursor.moveToFirst();
String filename = cursor.getString(column_index);
cursor.close();
return filename;
}
It is possible that the Android Gallery app would show pictures from your Picasa, but you can’t access these pictures directly. Again, to make your life easier, we do not require you to handle this situation.
You need to implement all the activities shown in User Interface. To be specific, you need to implement the main activity containing two fragments: start and history, and one menu containing two items: settings and edit profile. You should be able to switch to different tab. When you rotate the screen, the tab that is open should stay opened. Switching to default tab is not acceptable when the screen rotated. You need to figure out how to save selected tab as discussed in Main Activity section.
You don’t need to show the map in MapDisplayActivity.
Demo the lab: You can download MyRuns3.apk and run the app to see how it operates. Use that knowledge to fill in the gaps in the above document.
Complete the database design and implementation following the Database Implementation section. You should be able to add an exercise entry manually from the start tab at the main activity, view the entries in the history tab, and delete the entry from DisplayEntryActivity.
You need to show the data in correct format. For example, if the user set their unit preference to Metric (Kilometers), all distance related data should be shown in kilometers. If you save the data in miles, you need to convert it to kilometers before showing it. You need to think of a good way to convert the raw exercise entry data to human readable format. You also need to display some data in the map in Lab 4. Try to come up with a good design to avoid duplicate code.
Demo the lab: You can download MyRuns4.apk and run the app to see how it operates. Use that knowledge to fill in the gaps in the above document.
Design and implement the tracking services. Draw the real-time GPS trace on Google Maps using the continuous location updates. Save the traces in the database and visualize the GPS trace history on Google Maps. You should also be able to view the history trace from the history tab. Make sure that you need to implement automatic activity recognition as well in your app. You should use the Google Activity Recognition API to autommatically detect the user’s physically activity (e.g., still, walking, etc.) in your app. You need to come up with a way to broadcast the activity recognition result to the map activity.
To be specific, you need to finish implementing the MapViewActivity, tracking service and ActivityRecognition.
Demo the lab: You can download MyRuns-Android-chk5.apk and run the app to see how it operates. Use that knowledge to fill in the gaps in the above document.
This lab integrates the Firebase cloud into MyRuns. MyRuns4 saves all the data on phone memory using SQLite. This is great because you do not need an internet connection to save and retrieve the data. Local storage is also relatively more secure in comparison with remote storage. However, that’s all we’ve got. With only local storage, it means you can lose all your data if you lose your phone or if you inadvertantly uninstall the app. Furthermore, we’re unable to use the same login credentials when logging into multiple phones. It’s even very difficult for us to share data if the data is not in the cloud. For instance, you might want to share your latest activity milestones like the record time you took to run several kilometers, how much calories you burned in a day. You may also just want see your activity history using another phone. In this cae you need to be able to log into your central account before you can access all the historical data. We can do this and more if our data is in the cloud. All apps exploit cloud these days and now MyRuns will as well.
MyRuns5 has to do the following:
Create an account in the cloud: when you register with MyRuns5 it authenticates to Firebase and after that you simply sign into an account that has already been authenticated..
MyRuns4 allows us to create and save different exercise entries. But now we have to be able to store them to Firebase.
Click on ``sync’’ in the history fragment tool bar (make sure to be connected to the internet) to synchronise the data on the phone with the cloud.
You should be able to uninstall the MyRuns5 or clear its data in the phone’s settings. Then Install the app again open it again. Signin with your created account. Go to the history and see your data
To find more things about implementation, refer to Firebase cloud server, and Social component.