Create a Wear OS Medication Reminder App in Just 8 Steps

Work has changed forever, because by mixing it with OpenAI’s ChatGPT it becomes as easy as it gets, to the point that it’s even a bit scary – especially if you are a developer. Honestly, it has been more difficult to structure this article than actually creating the app.

Summary

Today, we discuss how to create a Wear OS app that reminds the user to take their medication. The app would display a notification at a specified interval, and the user should be able to configure the time to take the medication, the interval, and number of times to take it through a settings screen in the app.

To create the app, we followed the following steps:

  1. Set up the development environment by installing Android Studio and the Android SDK, and installing the Android Wear 2.0 Preview and the Android Emulator.
  2. Create a new project in Android Studio, choosing “Phone and Tablet” as the form factor and “Empty Activity” as the activity type.
  3. Add the Wearable Support library to the project by adding the following line to the dependencies block in the build.gradle file: implementation ‘com.google.android.support:wearable:2.3.0’
  4. Set up the app to run on a Wear OS device by adding the following uses-feature element to the AndroidManifest.xml file: <uses-feature android:name=”android.hardware.type.watch” />
  5. Add a notification to the app to remind the user to take their medication, using the NotificationCompat.Builder class and the NotificationManagerCompat class.
  6. Set up a repeating alarm to trigger the notification at the specified interval, using the AlarmManager class and the PendingIntent class.
  7. Create an AlarmReceiver class to receive the alarm and trigger the notification.
  8. Run the app on a Wear OS device, such as a Google Pixel Watch, by connecting the device to the computer and clicking the Run button in Android Studio.

We also discussed how to allow the user to configure the time to take the medication, the interval, and the medication name through a settings screen in the app. To do this, we followed these steps:

  1. Created a layout for the settings screen using views such as TimePicker, EditText, and TextView.
  2. Created a SettingsActivity class to display the settings screen, inflating the layout file and displaying it to the user.
  3. Added a menu item to the main activity to launch the settings screen, using the onCreateOptionsMenu() and onOptionsItemSelected() methods.
  4. Saved the user’s settings when the SettingsActivity is closed, using the SharedPreferences class.
  5. Loaded the user’s settings when the app starts, using the SharedPreferences class.
  6. Used the user’s settings to set the alarm, modifying the code for setting the alarm to use the user’s settings for the time, interval, and medication name.
  7. Modified the AlarmReceiver class to use the user’s settings to trigger the notification.

Overall, this process allows the user to customize the app to fit their specific needs, and ensures that they are reminded to take their medication at the desired intervals.

Now, let’s get into the details…

Step by step guide

1. Set up your development environment

First, install Android Studio (download it from here).

Open Android studio and install the Android SDK.

Click on “More Actions” and Select SDK Manager
Select the latest version of API Level and hit apply

Create a Virtual Device for testing:

  • Open the Virtual Device Manager
  • Click the “Create Virtual Device” button
  • In the “Category” list, select “Wear OS” and in the “Device” list, select the desired device and click “Next”.
  • In the “System Image” screen, select the latest image and click “Next”. If the image is not installed, click on the download Button to install it from here. The latest at the time of this writing is “R” (API Level 30)
  • Enter a name for the emulator and click “Finish” to create it.

You will be able to select the emulator from the device drop-down menu in Android Studio and click the “Run” button to launch it.


2. Project Setup

  • From the startup screen, select “New Project”, then select Wear OS from the templates list and choose “Blank Activity”. Hit Next

  • Choose your app name, package name, select Java for the language and make sure you use an API Level that supports Wear OS (like 30) and click finish.
  • Once Android Studio finishes setting up the project, if this is a fresh Android Studio install, close the Assistant by clicking on the tab on the right, so you will have more room to work with.
  • In the Project pane on the left, open the Gradle Scripts folder and open the build.gradle file (the one that says Module)
  • Check under android and make sure compileSdk is set to the latest (33 in my case), minSdk is set to 30 and targetSdk is also the latest (33).
  • Replace the dependencies node entirely with this code:
dependencies {
    implementation "androidx.wear:wear:1.2.0"

    // Add support for wearable specific inputs
    implementation "androidx.wear:wear-input:1.1.0"
    implementation "androidx.wear:wear-input-testing:1.1.0"

    // Use to implement wear ongoing activities
    implementation "androidx.wear:wear-ongoing:1.0.0"

    // Use to implement support for interactions from the Wearables to Phones
    implementation "androidx.wear:wear-phone-interactions:1.0.1"
    // Use to implement support for interactions between the Wearables and Phones
    implementation "androidx.wear:wear-remote-interactions:1.0.0"
}

The final build.gradle file should look like this:

Now, let’s setup the app to run on a Wear OS device:

  • In the Project pane, expand the manifests folder, and then open the AndroidManifest.xml file.
  • Make sure that your manifest file has the following uses-feature element as a child of the manifest element:
<uses-feature android:name="android.hardware.type.watch" />

And that’s it for the project setup. Now let’s start building the app itself!


3. Creating a notification

As a basic feature, we need the app to notify the user when they need to take their medication. In order to do this, first

  • In the Project pane, expand the java folder, and then open the MainActivity.java file.
  • Add the following code to the onCreate method to create a notification that reminds you to take your medication:
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "medication_reminder")
                .setSmallIcon(R.drawable.notification_icon)
                .setContentTitle("Time to take your medication")
                .setContentText("Don't forget to take your medication")
                .setPriority(NotificationCompat.PRIORITY_HIGH)
                .setVibrate(new long[] { 1000, 1000, 1000, 1000, 1000 })
                .setAutoCancel(true);

NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);

// notificationId is a unique int for each notification that you must define
notificationManager.notify(notificationId, builder.build());

Adding this code will create problems. That is because Android Studio does not yet know what certain parts of the code are. These parts will be marked in red.

The solution is simple. Click on the red items and let Android Studio suggest the fix with the red lighbulb icon. Click that icon (or presse Alt+Enter on windows) and select “Import Class”.

Do this with each red item except for notification_icon and notificationId , and Android Studio will fix the code for you.

Notice that notification_icon and notificationId still create problems.

First the notification_icon, as the name implies, is the icon to display with the notification. To fix it, we need to create it.

1. Right-click on the mipmap folder in the Project Pane and select New->Vector Asset

2. Give it a name of notification_icon and click the small android logo to select a new icon from the clip art. I chose the one called “medical services”

Click Next and then Finish.

You should find a new folder called drawable inside the res folder and a notification_icon.xml file inside it. And the error should be gone:

Now to approach the last problem, notificationId…

To understand what it is, the notification id is unique for each notification that the app will display and should be different every time. But it should not only be unique, but this number cannot be as simple as a 0 and then increment it next time, because if the app is killed by any reason, the count would restart. However, these numbers do not need to be consecutive, only unique.

So to approach this, we will create a simple method that converts the current date and time to an integer number and use that number as the notificationId.

Add this code before the line with the notify() instruction:

Date now = new Date();
int notificationId = Integer.parseInt(new SimpleDateFormat("MMddHHmmss",  Locale.US).format(now));

Note that Date, Locale and SimpleDateFormat will need their classes imported too, just like we did with the first errors.

And for now, we are ready to test the fist part! Let’s try to get a notification! To do that, run your emulator if you haven’t already and once it starts, hit run (Shift+F10) to test your notification.

Success! … or not 🙁 There is a notification, but is rather an error message. This is because we have not created a notification channel for it to post the notifications to. To solve this, simply add this code before we create the notification:

//Create a High Importance notification Channel
NotificationChannel channel = new NotificationChannel(
        "medication_reminder",
        "medications_channel",
        NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription("Notifications to remind you to take your medication");
// Register the channel with the system; you can't change the importance
// or other notification behaviors after this
NotificationManager notificationManagerService = getSystemService(NotificationManager.class);
notificationManagerService.createNotificationChannel(channel);

This should create the notification channel we need and… voila! We have a fancy looking notification, that when clicked, it opens up with its own icon and the text we described:

Now that we have successfully created the basic notification functionality is time to move on and create a script that can fire these notifications based on certain parameters, because as of now, the notification fires, but it does so only when opening the app.

You can grab my code up to this point from here: https://github.com/javlae2/wearos_medication_reminder/tree/v0.01


4. Setup a notification trigger

As of now, we can open the app and immediately see a notification. This is fine for now, but the desired behavior is a bit different. We want the notification to be triggered by a specific interval set by the user. We will leave the “set by the user” thing alone for this step and we will focus on triggering the notification every x seconds. To accomplish this, we need to create an internal repeating alarm and trigger a notification whenever it ticks.

Add the following code to the onCreate method to set up a repeating alarm that triggers the notification at a specified interval:

Intent intent = new Intent(this, AlarmReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);

AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);

// Set the alarm to start at the specified time and repeat at the specified interval
long interval = 60000; // 60 seconds
long triggerTime = SystemClock.elapsedRealtime() + interval;
alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerTime, interval, pendingIntent);

As usual after pasting code, Android Studio will complain and mark some elements in red. Do our usual, and import those classes, except for AlarmReceiver, as this is a class that we will create ourselves. That’s next.

Now, from the Project Panel create a new Java class at the same level as MainActivity and call it AlarmReceiver. Then replace all its code with this one:

//AlarmReceiver.java
package com.gptmade.pillpal;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.icu.text.SimpleDateFormat;

import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;

import java.util.Date;
import java.util.Locale;

public class AlarmReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {

        //CREATE THE NOTIFICATION
        NotificationCompat.Builder builder = new NotificationCompat.Builder(context, "medication_reminder")
                .setSmallIcon(R.drawable.notification_icon)
                .setContentTitle("Time to take your medication")
                .setContentText("Don't forget to take your medication")
                .setPriority(NotificationCompat.PRIORITY_HIGH)
                .setVibrate(new long[] { 1000, 1000, 1000, 1000, 1000 })
                .setAutoCancel(true);

        NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);

        Date now = new Date();
        int notificationId = Integer.parseInt(new SimpleDateFormat("MMddHHmmss",  Locale.US).format(now));
        notificationManager.notify(notificationId, builder.build());
    }
}

Notice something familiar? Yes, we have the same notification code that we had in the MainActivity, except that we are replacing the ‘this’ keyword with the ‘context’ keyword, as we will be passing that context from the main as an argument. Now we can create a new notification each time we want, by triggering the alarm.

Remove the code that creates the notification from MainActivity, but don’t remove the part that creates the channel, as this needs to be invoked only once when the app is started. Here’s how it should look now:

Then, we need to tell the app that we have made a custom receiver, so we must declare it in the AndroidManifest.xml file (this took me a long time to figure out, as chatGPT did not suggest it). Add it just before the closing </application> tag

<receiver android:name="AlarmReceiver" />

Run the app… Great! Now we have an app that notifies you to take your meds every x seconds, depending on the value of the “interval” variable.

Up next, we will control these notifications to fire according to the user’s choices. So we will add a new settings screen where the user can input the information and change the code a bit so this information can be saved and used for notifying actual notifications.

You can check my code up to this point here: https://github.com/javlae2/wearos_medication_reminder/tree/v0.02


5. Create user input layout

So far so good, but these notifications will trigger forever every x seconds. We don’t want to suggest the user to OD on their meds!

To allow the user to configure the time to take the medication, the interval, and which medication, we need to add a second screen to our app where the user can enter this information.

In the Project Pane, under res->layout, create a new layout file called “settings_activity.xml”

Leave it with the default options and after creating the file, switch to the code view and paste the following:

<?xml version="1.0" encoding="utf-8"?>
<androidx.wear.widget.BoxInsetLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="@dimen/box_inset_layout_padding"
    tools:context=".MainActivity"
    tools:deviceIds="wear">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/frameLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_boxedEdges="all">


        <LinearLayout
            android:id="@+id/linearLayout2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Every (hours):" />

            <EditText
                android:id="@+id/interval_edit_text"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:inputType="number"
                android:lines="1"/>
        </LinearLayout>

        <LinearLayout
            android:id="@+id/linearLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/linearLayout2"
            app:layout_constraintVertical_bias="0.0"
            tools:layout_editor_absoluteX="0dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Repeat (times):" />

            <EditText
                android:id="@+id/repetitions_edit_text"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:inputType="number"
                android:lines="1" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:orientation="horizontal"
            app:layout_constraintBottom_toBottomOf="parent">

            <ImageButton
                android:id="@+id/buttonDelete"
                android:layout_width="64dp"
                android:layout_height="52dp"
                android:backgroundTint="#E91E63"
                android:onClick="deleteAlarm"
                android:src="@drawable/delete_icon"
                tools:ignore="SpeakableTextPresentCheck" />

            <ImageButton
                android:id="@+id/buttonSave"
                android:layout_width="64dp"
                android:layout_height="52dp"
                android:backgroundTint="#4CAF50"
                android:onClick="saveSettings"
                android:src="@drawable/save_icon"
                tools:ignore="SpeakableTextPresentCheck" />
        </LinearLayout>
    </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.wear.widget.BoxInsetLayout>

You may have noticed that Android Studio is complaining (again). That’s because we are referencing a couple of icons that do not yet exist, so we need to create them. You already know how to do that! Call them delete_icon and save_icon respectively.

You may also have noticed that Android Studio is giving you warnings about hardcoded strings. This is because not every user will have their device in the same language, and the ideal is to set those string values in separate strings.xml files. You can do that later, it is a fairly simple process, but we will not focus on that this time.

Anyway, now that we have the layout, we need to display it.

Create a new Java class called SettingsActivity and paste the following code:

package com.gptmade.pillpal;

import android.app.Activity;
import android.os.Bundle;

public class SettingsActivity extends Activity {

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

And add the new Activity to the manifest file:

<activity android:name=".SettingsActivity"/>

We’ll now add a way to navigate to our new settings screen. To do this we will add a button from the MainActivity (home page).

First, let’s create an icon for our fancy button. Create a new Vector asset under the res folder and call it “edit_item”, choose an icon from the clip art and click OK.

Then, open your main layout file “activity_main.xml”, switch to code view and replace its code with this one:

<?xml version="1.0" encoding="utf-8"?>
<androidx.wear.widget.BoxInsetLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="@dimen/box_inset_layout_padding"
    tools:context=".MainActivity"
    tools:deviceIds="wear">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/frameLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_boxedEdges="all">

        <TextView
            android:id="@+id/text"
            android:layout_width="119dp"
            android:layout_height="19dp"
            android:text="@string/hello_world"
            android:textAlignment="center"
            android:textStyle="bold"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/summaryTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="No reminders set"
            android:textAlignment="center"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.415" />

        <ImageButton
            android:id="@+id/buttonEdit"
            android:layout_width="64dp"
            android:layout_height="52dp"
            android:backgroundTint="#4CA6E4"
            android:onClick="goToSettings"
            android:src="@drawable/edit_icon"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/summaryTextView"
            tools:ignore="SpeakableTextPresentCheck" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.wear.widget.BoxInsetLayout>

Now that our button is created, let’s program it to go to the Settings page.

Add the following method to the MainActivity file, just before the closing bracket:

public void goToSettings(View view) {
    // Start the SettingsActivity
    Intent intent = new Intent(this, SettingsActivity.class);
    startActivity(intent);
}

If you run the app at this point, it will look similar to this:

The “PillPals” text is different in your version? Check your res/values/strings folder 😉

And tapping the button will trigger the settings page, that should look like this:

Great! We can now accept user input. Next up, we need to store that input and apply it to the functionality.

If you want to compare your work with mine, check my code up to this point here: https://github.com/javlae2/wearos_medication_reminder/tree/v0.03

6. Apply the user input

Now that we have the (rather ugly) user interface, let’s take the user’s input and apply it to configure the app’s functionality.

First, we need to take the input and save it.

Go to SettingsActivity. This one has a lot of changes, so replace the ENTIRE code with the following. I´ve added comments inline to explain what the new code does.

Work in progress… please check in later to see the rest of this article

Further improvements

DISCLAMER:

Please note, I have a developer background but I am by no means an Android developer, so judging the quality of the code here provided is not in my capacity. Please feel free to suggest changes in the comments. I have adapted the code here published, but it has mostly been generated by chatGPT. The modifications were necessary for the code to work, as the AI has its limitations in terms of when its information was last updated. Its training data only goes up until 2021. For example, it was giving information on working on WearOS 2.0 Preview, which was launched on Feb 2017.


Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *