Advertisement

Building Apps with Environment Sensors

by

Learn how to use the Android Environment Sensors to detect information about the user's environment, including ambient temperature, pressure, humidity, and light.

The Android system supports a range of device Sensors, some implemented in hardware and some in software. The Environment Sensors are all hardware features, providing access to information about ambient temperature, pressure, humidity, and light. These sensors return values as follows: temperature is measured in degrees Celsius, atmospheric pressure in hPa millibars, relative ambient air humidity as a percentage value, and ambient light in SI lux units. In this tutorial, we will run through the basic process for using these four main Environment Sensors. We will not be using the device temperature sensor, as it is now deprecated as of Android 4.0.

There are many possible applications for these sensors, such as barometers and thermometers. You may have come across such apps in Google Play already, but it is worth noting that they may not necessarily be implementing their functions using Environment Sensors. For example, weather apps often use location data fetched over the Web to determine environment information based on where you are.

Since these sensors are provided via users' hardware, support does vary between devices and manufacturers. At the time of writing, very few Android smartphones or tablets support all of the Environment Sensors, but many of the more recent models support one or more of them. It is vital to carry out checks on whether the user has particular sensors and to avoid using functionality that is totally reliant on them. The only exception to this is if you ensure only users with the required hardware can download your application - you can do this using the filters for an app as listed in the Google Play store.


Step 1: Create a New Android Project

Create a new Android project in Eclipse and give it a name of your choice. Let Eclipse create a main Activity, as this is the only class we will need. For the code used in this tutorial, we are targeting Android API level 14 (Android 4.0 - Ice Cream Sandwich), but you can target a more recent version if you wish. You do not need to make any alterations to the Manifest file. Your main Activity class should have the following initial structure, with your chosen class name:

public class EnvironmentCheckerActivity extends Activity {

	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
	}
}

We are going to implement a couple of Interfaces, so extend your opening class declaration line as follows:

public class EnvironmentCheckerActivity extends Activity implements OnClickListener, SensorEventListener {

The click listener is for user interaction and the Sensor Event Listener is for receiving data from the device sensors. Eclipse should have provided import statements for the Activity and Bundle classes, but you also need to add the following to the list:

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

We will be adding code to the class later.


Step 2: Design User Interaction

In order to demonstrate the basic process for using the Environment Sensors, we are going to build a simple user interface. The app will display a list of four buttons, one for each of the sensors we will be using. When the user selects a button, the app will attempt to retrieve the appropriate information and present it within a Text View. First, let's define some text Strings we will use within the interface. Open your "res/values/strings.xml" file and edit it to contain the following:

<resources>
<string name="hello">Choose from the options to check your environment</string>
<string name="app_name">Environment Checker</string>
<string name="ambient_label">Ambient Temperature</string>
<string name="light_label">Light</string>
<string name="pressure_label">Pressure</string>
<string name="humidity_label">Relative Humidity</string>
<string name="text_placeholder">-</string>
</resources>

These represent the title, introductory text, button labels, and a placeholder for the Text Views. We are going to use a couple of drawable resources for the design, but you can omit them if you wish. To use them, in each of your app's drawables folders, you will need to create two additional files, "back.xml" and "btn.xml" (select each drawable folder in turn and choose "File" > "New" > "File", then enter the file name). For the "back.xml" file, enter the following code:

<shape xmlns:android="http://schemas.android.com/apk/res/android"
	android:dither="true">
	<gradient
		android:startColor="#FF000033"
		android:endColor="#FF000033"
		android:centerColor="#FF000066"
		android:angle="180" />
</shape>

For the "btn.xml" file, enter the following:

<shape xmlns:android="http://schemas.android.com/apk/res/android"
	android:dither="true">
	<gradient
		android:startColor="#FF00CC00"
		android:endColor="#FF66CC66"
		android:angle="90" />
	<padding android:left="10dp" android:top="10dp"
		android:right="10dp" android:bottom="10dp" />
	<corners android:radius="2dp" />
</shape>

The back drawable is for the Activity background and the "btn" drawable is for the button backgrounds. Feel free to alter these designs in any way you wish - make sure you copy them to each drawable folder in your app.

Now open your app's "main.xml" layout file (res/layout/main.xml). Enter a Scroll View and Linear Layout as follows:

<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent"
	android:background="@drawable/back">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="fill_parent"
	android:layout_height="wrap_content"
	android:padding="10dp"
	android:orientation="vertical" >
    
</LinearLayout>
</ScrollView>

Inside the Linear Layout, add the introduction, then a Button and Text View for each of the four sensors:

<TextView android:layout_width="fill_parent"
	android:layout_height="wrap_content"
	android:paddingBottom="10dp"
	android:textColor="#FFFFFF00"
	android:text="@string/hello" />
    
<Button android:id="@+id/ambient_btn"
	android:layout_height="wrap_content"
	android:layout_width="wrap_content"
	android:text="@string/ambient_label"
	android:background="@drawable/btn" />
<TextView android:id="@+id/ambient_text"
	android:paddingBottom="20dp"
	android:layout_width="fill_parent"
	android:layout_height="wrap_content"
	android:text="@string/text_placeholder" 
	android:textColor="#FFFF66FF"
	android:textStyle="bold" />
    
<Button android:id="@+id/light_btn"
	android:layout_height="wrap_content"
	android:layout_width="wrap_content"
	android:text="@string/light_label"
	android:background="@drawable/btn" />
<TextView android:id="@+id/light_text"
	android:paddingBottom="20dp"
	android:layout_width="fill_parent"
	android:layout_height="wrap_content"
	android:text="@string/text_placeholder" 
	android:textColor="#FFFF66FF"
	android:textStyle="bold" />
    
<Button android:id="@+id/pressure_btn"
	android:layout_height="wrap_content"
	android:layout_width="wrap_content"
	android:text="@string/pressure_label"
	android:background="@drawable/btn" />
<TextView android:id="@+id/pressure_text"
	android:paddingBottom="20dp"
	android:layout_width="fill_parent"
	android:layout_height="wrap_content"
	android:text="@string/text_placeholder" 
	android:textColor="#FFFF66FF"
	android:textStyle="bold" />
    
<Button android:id="@+id/humidity_btn"
	android:layout_height="wrap_content"
	android:layout_width="wrap_content"
	android:text="@string/humidity_label"
	android:background="@drawable/btn" />
<TextView android:id="@+id/humidity_text"
	android:paddingBottom="20dp"
	android:layout_width="fill_parent"
	android:layout_height="wrap_content"
	android:text="@string/text_placeholder" 
	android:textColor="#FFFF66FF"
	android:textStyle="bold" />

Each Button and Text View pair is virtually identical, with ID attributes to identify them in the Java code. Of course, you can alter any of the design elements if you wish. The layout refers to the drawable resources and Strings.


Step 3: Handle User Interaction

Open your app's main Activity class. At the top of the class declaration, before the "onCreate" method, add the following instance variables:

private Button ambientBtn, lightBtn, pressureBtn, humidityBtn;
private TextView ambientValue, lightValue, pressureValue, humidityValue;

These represent the Buttons and Text Views we created in the layout. We are going to use an array to keep track of the Text View items, as these will update with information when the sensors return it, so add the following array variable next:

private TextView[] valueFields = new TextView[4];

Now add these constants to refer to each sensor type:

private final int AMBIENT=0;
private final int LIGHT=1;
private final int PRESSURE=2;
private final int HUMIDITY=3;

Inside the "onCreate" method, after the line in which the content view is set, retrieve a reference to each button using the ID attributes we included in the layout, as follows:

ambientBtn = (Button)findViewById(R.id.ambient_btn);
lightBtn = (Button)findViewById(R.id.light_btn);
pressureBtn = (Button)findViewById(R.id.pressure_btn);
humidityBtn = (Button)findViewById(R.id.humidity_btn);

Now set each of these to use the Activity class as click listener:

ambientBtn.setOnClickListener(this);
lightBtn.setOnClickListener(this);
pressureBtn.setOnClickListener(this);
humidityBtn.setOnClickListener(this);

When these buttons are pressed, the Activity "onClick" method will execute. Next, retrieve the Text View items and add a reference to each in the array we declared, using the constants to specify each index:

ambientValue = (TextView)findViewById(R.id.ambient_text);
valueFields[AMBIENT]=ambientValue;
lightValue = (TextView)findViewById(R.id.light_text);
valueFields[LIGHT]=lightValue;
pressureValue = (TextView)findViewById(R.id.pressure_text);
valueFields[PRESSURE]=pressureValue;
humidityValue = (TextView)findViewById(R.id.humidity_text);
valueFields[HUMIDITY]=humidityValue;

Now we need to provide the "onClick" method, adding it to the class after the "onCreate" method:

public void onClick(View v) 
{
    	
}

Inside the method, we need to determine which button was clicked using a conditional:

if (v.getId()==R.id.ambient_btn) 
{     
//ambient temperature

}
else if(v.getId()==R.id.light_btn) 
{
//light

}
else if(v.getId()==R.id.pressure_btn) 
{
//pressure

}
else if(v.getId()==R.id.humidity_btn) 
{
//humidity

}

Inside each of these we will attempt to retrieve the relevant environment data.


Step 4: Setup Environment Sensor

At the top of the class, add a couple of variables for the environment sensing process:

private SensorManager senseManage;
private Sensor envSense;

Back in the "onCreate" method, after the existing code, add the following to create an instance of the Sensor Manager class:

senseManage = (SensorManager) getSystemService(Context.SENSOR_SERVICE);

We need the Sensor Manager for all environment sensing processes. We use it to retrieve specific sensors. Inside the "onClick" method "if" statement for ambient temperature, attempt to retrieve the ambient temperature sensor as follows:

envSense = senseManage.getDefaultSensor(Sensor.TYPE_AMBIENT_TEMPERATURE);

Now we need to take care of cases in which the sensor is not provided by the user device, so add the following test:

if(envSense==null)
	Toast.makeText(this.getApplicationContext(), 
	"Sorry - your device doesn't have an ambient temperature sensor!", 
	Toast.LENGTH_SHORT).show();

We simply output an error message. If the sensor is present, we need to register to receive the data it returns, so add the following after this "if" statement:

else
	senseManage.registerListener(this, envSense, SensorManager.SENSOR_DELAY_NORMAL);

Now carry out the same process for each of the buttons in the "onClick" method. For the light button (in its "if" statement inside "onClick"):

envSense = senseManage.getDefaultSensor(Sensor.TYPE_LIGHT);
if(envSense==null)
	Toast.makeText(this.getApplicationContext(), 
	"Sorry - your device doesn't have a light sensor!", 
	Toast.LENGTH_SHORT).show();
else
	senseManage.registerListener(this, envSense, SensorManager.SENSOR_DELAY_NORMAL);

Notice that here we are requesting the "TYPE_LIGHT" sensor and are tailoring the error message to the sensor type. For the pressure button:

envSense = senseManage.getDefaultSensor(Sensor.TYPE_PRESSURE);
if(envSense==null)
	Toast.makeText(this.getApplicationContext(), 
	"Sorry - your device doesn't have a pressure sensor!", 
	Toast.LENGTH_SHORT).show();
else
	senseManage.registerListener(this, envSense, SensorManager.SENSOR_DELAY_NORMAL);

Finally, in the "if" statement for the humidity button:

envSense = senseManage.getDefaultSensor(Sensor.TYPE_RELATIVE_HUMIDITY);
if(envSense==null)
	Toast.makeText(this.getApplicationContext(), 
	"Sorry - your device doesn't have a humidity sensor!", 
	Toast.LENGTH_SHORT).show();
else
	senseManage.registerListener(this, envSense, SensorManager.SENSOR_DELAY_NORMAL);

Step 5: Retrieve Accuracy Data

In addition to returning the requested environment data, the sensor will also return accuracy data. Add the following method to your class, which is required when implementing the Sensor Event Listener Interface:

@Override
public final void onAccuracyChanged(Sensor sensor, int accuracy) {

}

Inside the method, start building a message regarding the accuracy:

String accuracyMsg = "";

We will use a switch statement on the passed accuracy integer parameter:

switch(accuracy){
case SensorManager.SENSOR_STATUS_ACCURACY_HIGH:
	accuracyMsg="Sensor has high accuracy";
	break;
case SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM:
	accuracyMsg="Sensor has medium accuracy";
	break;
case SensorManager.SENSOR_STATUS_ACCURACY_LOW:
	accuracyMsg="Sensor has low accuracy";
	break;
case SensorManager.SENSOR_STATUS_UNRELIABLE:
	accuracyMsg="Sensor has unreliable accuracy";
	break;
default:
	break;
}

We tailor the message to the sensor's accuracy level, using the Sensor Manager class. Output the accuracy when it is received as follows, but after the switch statement:

Toast accuracyToast = Toast.makeText(this.getApplicationContext(), accuracyMsg, Toast.LENGTH_SHORT);
accuracyToast.show();

Inside the "onAccuracyChanged" method you can also determine the sensor type from the passed parameter if you need to.


Step 6: Retrieve Sensor Data

Now we can finally retrieve the returned data from the sensors using a Sensor Event - we do this in the "onSensorChanged" method, which is also required in order to implement the Interface:

@Override
public final void onSensorChanged(SensorEvent event) {
    	
}

The Sensor Event returns its data in different ways depending on the sensor type. For all four types we are using, it is retrieved in the same way, from the first item in an array of floating point values. Add the following inside the method:

float sensorValue = event.values[0];

We are now going to build a text String including this data, and write it to the relevant Text View for the user to see. First, we create a Text View variable and give it a default value (this value will be overwritten - we include it to keep Eclipse happy):

TextView currValue = ambientValue;

Next, we declare the String:

String envInfo="";

The content of the String is going to depend on the type of sensor, so let's find out which one it is:

int currType=event.sensor.getType();

Now we can use a switch statement on this value:

switch(currType){
case Sensor.TYPE_AMBIENT_TEMPERATURE:
	envInfo=sensorValue+" degrees Celsius";
	currValue=valueFields[AMBIENT];
	break;
case Sensor.TYPE_LIGHT:
	envInfo=sensorValue+" SI lux units";
	currValue=valueFields[LIGHT];
	break;
case Sensor.TYPE_PRESSURE:
	envInfo=sensorValue+" hPa (millibars)";
	currValue=valueFields[PRESSURE];
	break;
case Sensor.TYPE_RELATIVE_HUMIDITY:
	envInfo=sensorValue+" percent humidity";
	currValue=valueFields[HUMIDITY];
	break;
default: break;
}

In each case, we build the informative String using the sensor value retrieved and a text excerpt relevant to the type. We also set the Text View to the relevant user interface item using the array and constants. After the switch statement, output the information:

currValue.setText(envInfo);

Now reset the sensor variable and stop listening for updates to prevent unnecessary battery usage:

envSense=null;
senseManage.unregisterListener(this);

Finally, we don't want the app to use unnecessary resources when it is paused, so add this method to the class:

@Override
protected void onPause() {
	super.onPause();
	senseManage.unregisterListener(this);
}

Step 7: Try it Out

That's the complete demonstration app! There is no point running this app on the default Android emulator, because it does not provide Environment Sensors. However, you can either run it on an actual device or use the Sensor Simulator tool, which allows you to simulate certain aspects of the environment. The following is a screenshot of the app running on the Samsung Galaxy S III just after retrieving the light and pressure data:

Here it is for the other two sensors, which are not supported:


Conclusion

The Environment Sensors are an exciting but still developing feature of the Android platform. However, it is a little early to focus on them. If you want to explore using these sensors in more advanced ways, check out the Developer Guide section on calculating dew point and absolute humidity levels based on relative humidity and ambient temperature. Other than that - try to be patient!

Advertisement