




















































In this article by Rick Boyer and Kyle Merrifield Mew, the author of Android Application Development Cookbook - Second Edition, we'll take a look at the following recipes:
(For more resources related to this topic, see here.)
Android 5.0—Lollipop (API 21)—introduced a new type of notification called the Heads-up notification. Many people do not care about this new notification as it can be extremely intrusive. This is because the notification forces its way on top of other apps. (Take a look at the following screenshot.) Keep this in mind when using this type of notification. We're going to demonstrate the Heads-up notification with a Flashlight as this demonstrates a good use case scenario.
Here's a screenshot showing the Heads-up notification that we'll create:
If you have a device running Android 6.0, you may have noticed the new Flashlight settings option. As a demonstration, we're going to create something similar in this recipe.
Create a new project in Android Studio and call it FlashlightWithHeadsUp. When prompted for the API level, we need API 23 (or higher) for this project. Select Empty Activity when prompted for Activity Type.
Our activity layout will consist of just ToggleButton to control the flashlight mode. We'll use the setTorchMode() code and add a Heads-up notification. We'll need permission to use the vibrate option; so, start by opening the Android Manifest and follow these steps:
<uses-permission android_name="android.permission.VIBRATE"/>
<activity android_name=".MainActivity"
android_launchMode="singleInstance">
<ToggleButton
android_id="@+id/buttonLight"
android_layout_width="wrap_content"
android_layout_height="wrap_content"
android_text="Flashlight"
android_layout_centerVertical="true"
android_layout_centerHorizontal="true"
android_onClick="clickLight"/>
private static final String ACTION_STOP="STOP";
private CameraManager mCameraManager;
private String mCameraId=null;
private ToggleButton mButtonLight;
mButtonLight = (ToggleButton)findViewById(R.id.buttonLight);
mCameraManager = (CameraManager)
this.getSystemService(Context.CAMERA_SERVICE);
mCameraId = getCameraId();
if (mCameraId==null) {
mButtonLight.setEnabled(false);
} else {
mButtonLight.setEnabled(true);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (ACTION_STOP.equals(intent.getAction())) {
setFlashlight(false);
}
}
private String getCameraId() {
try {
String[] ids = mCameraManager.getCameraIdList();
for (String id : ids) {
CameraCharacteristics c =
mCameraManager.getCameraCharacteristics(id);
Boolean flashAvailable =
c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
Integer facingDirection =
c.get(CameraCharacteristics.LENS_FACING);
if (flashAvailable != null &&
flashAvailable && facingDirection != null
&& facingDirection ==
CameraCharacteristics.LENS_FACING_BACK) {
return id;
}
}
} catch (CameraAccessException e) {
e.printStackTrace();
}
return null;
}
public void clickLight(View view) {
setFlashlight(mButtonLight.isChecked());
if (mButtonLight.isChecked()) {
showNotification();
}
}
private void setFlashlight(boolean enabled) {
mButtonLight.setChecked(enabled);
try {
mCameraManager.setTorchMode(mCameraId, enabled);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
private void showNotification() {
Intent activityIntent = new
Intent(this,MainActivity.class);
activityIntent.setAction(ACTION_STOP);
PendingIntent pendingIntent =
PendingIntent.getActivity(this,0,activityIntent,0);
final Builder notificationBuilder = new Builder(this)
.setContentTitle("Flashlight")
.setContentText("Press to turn off the
flashlight")
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(),
R.mipmap.ic_launcher))
.setContentIntent(pendingIntent)
.setVibrate(new long[]{DEFAULT_VIBRATE})
.setPriority(PRIORITY_MAX);
NotificationManager notificationManager =
(NotificationManager)
this.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(0,
notificationBuilder.build());
}
You're ready to run the application on a physical device. As seen in the preceding steps, you'll need an Android 6.0 (or higher) device, with an outward facing camera flash.
Since this recipe uses the same flashlight code, we'll jump into the showNotification() method. Most of the notification builder calls are the same as the ones seen in previous examples, but there are two significant differences:
.setVibrate()
.setPriority(PRIORITY_MAX)
Notifications will not be escalated to Heads-up notifications unless the priority is high (or above) and uses either vibrate or sound.
Take a look at this from the developer documentation (http://developer.android.com/reference/android/app/Notification.html#headsUpContentView):
"At its discretion, the system UI may choose to show this as a heads-up notification".
We create the PendingIntent method as we've done previously, but, here, we set the action using this code:
activityIntent.setAction(ACTION_STOP);
We set the app to only allow a single instance in the AndroidManifest method as we don't want to start a new instance of the app when the user presses the notification. The PendingIntent method we created sets the action, which we can check out in the onNewIntent() callback. If the user opens the app without pressing the notification, they can still disable the flashlight using the ToggleButton.
We can use a custom layout with notifications. Use the following method on the builder to specify its layout:
headsupContentView()
Working with images can be very memory-intensive, often resulting in your application crashing due to an out-of-memory exception. This is especially true for pictures taken with the device camera as they often have a much higher resolution than the device itself.
Since loading a higher-resolution image than the UI supports doesn't provide any visual benefit, this recipe will demonstrate how to take smaller samples of the image for display. We'll use BitmapFactory to first check the image size, and we'll then load a scaled down image.
Here's a screenshot from this recipe, showing a thumbnail of a very large image:
Create a new project in Android Studio and call it LoadLargeImage. Use the default Phone & Tablet options, and select Empty Activity when prompted for the Activity Type.
We'll need a large image for this recipe, so we've referred to https://pixabay.com/ for an image. Since the image itself doesn't matter, we downloaded the first image that shown at the time. (The full size of image is 6000 x 4000 and 3.4 MB.)
As stated previously, we need a large image to demonstrate the scaling. Once you have the image, follow these steps:
<ImageView
android_id="@+id/imageViewThumbnail"
android_layout_width="100dp"
android_layout_height="100dp"
android_layout_centerInParent="true" />
public Bitmap loadSampledResource(int imageID, int
targetHeight, int targetWidth) {
final BitmapFactory.Options options = new
BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), imageID,
options);
final int originalHeight = options.outHeight;
final int originalWidth = options.outWidth;
int inSampleSize = 1;
while ((originalHeight / (inSampleSize *2)) >
targetHeight && (originalWidth / (inSampleSize *2)) >
targetWidth) {
inSampleSize *= 2;
}
options.inSampleSize=inSampleSize;
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(getResources(),
imageID, options);
}
ImageView imageView =
(ImageView)findViewById(R.id.imageViewThumbnail);
imageView.setImageBitmap(loadSampledResource(R.drawable.image_large, 100, 100));
The purpose of the loadSampledResource() method is to load a smaller image to reduce the memory consumption of the image. If we attempted to load the full image chosen from https://pixabay.com/, the app would require over 3 MB of RAM to load. That's more memory than most devices can handle (at the moment, anyway), and even if it could be loaded completely, it would provide no visual benefit to our thumbnail view.
To avoid an out-of-memory situation, we use the inSampleSize property of BitmapFactory. You will find options to reduce or subsample the image. (If we set inSampleSize=2, it will reduce the image in half. If we use inSampleSize=4, it will reduce the image by ¼.) To calculate inSampleSize, we first need to know the image size. We can use the inJustDecodeBounds property, as follows:
options.inJustDecodeBounds = true;
This tells BitmapFactory to get the image dimensions without actually storing the contents of the image. Once we know the image size, we calculate the sample using this code:
while ((originalHeight / (inSampleSize *2)) > targetHeight &&
(originalWidth / (inSampleSize *2)) > targetWidth) {
inSampleSize *= 2;
}
The purpose of this code is to determine the largest sample size that does not reduce the image below the target dimensions. To do this, we double the sample size, and check whether the size exceeds the target size dimensions. If it doesn't, we save the doubled sample size and repeat the process. Once the reduced size falls below the target dimensions, we use the last saved inSampleSize.
From the inSampleSize documentation:
Note that the decoder uses a final value that's based on powers of 2; any other value will be rounded down to the nearest power of 2.
Once we have the sample size, we set the inSampleSize property. We also set inJustDecodeBounds to false in order to make it load normally. Here is the code to do this:
options.inSampleSize = inSampleSize;
options.inJustDecodeBounds = false;
It's important to note that this recipe illustrates the concept of applying a task in your own application. Loading and processing images can be a long operation, which could cause your application to stop responding. This is not a good thing and could cause Android to show the Application Not Responding (ANR) dialog. It is recommended that you perform long tasks on a background thread to keep your UI thread responsive.
It's important to note that the targetHeight and targetWidth parameters we pass to the loadSampledResource()method do not actually set the size of the image. If you run the application using the same sized image we used earlier, the sample size will be 32, resulting in a loaded image that is 187 x 125 in size.
If your layout needs an image of a specific size, either set the size in the layout file; otherwise, you can modify the size directly using the Bitmap class.
We'll start this with a simple recipe that is commonly needed: how to get the last known location. This is an easy-to-use API with very little overhead resource drain (which means that your app won't be responsible for killing the battery life).
This recipe also provides a good introduction to setting up the Google Location APIs.
Create a new project in Android Studio and call it GetLastLocation. Use the default Phone & Tablet options, and select Empty Activity when prompted for the Activity Type.
First, we'll add the necessary permissions to the Android Manifest. We'll then create a layout with a Button and TextView. Finally, we'll create GoogleAPIClient to access the previous location. Open the Android Manifest and follow these steps:
<uses-permission android_name="android.permission.ACCESS_COARSE_LOCATION"/>
compile 'com.google.android.gms:play-services:8.4.0'
<TextView
android_id="@+id/textView"
android_layout_width="wrap_content"
android_layout_height="wrap_content" />
<Button
android_id="@+id/button"
android_layout_width="wrap_content"
android_layout_height="wrap_content"
android_text="Get Location"
android_layout_centerInParent="true"
android_onClick="getLocation"/>
GoogleApiClient mGoogleApiClient;
TextView mTextView;
Button mButton;
GoogleApiClient.ConnectionCallbacks mConnectionCallbacks = new GoogleApiClient.ConnectionCallbacks() {
@Override
public void onConnected(Bundle bundle) {
mButton.setEnabled(true);
}
@Override
public void onConnectionSuspended(int i) {}
};
GoogleApiClient.OnConnectionFailedListener mOnConnectionFailedListener = new GoogleApiClient.OnConnectionFailedListener() {
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
Toast.makeText(MainActivity.this, connectionResult.toString(), Toast.LENGTH_LONG).show();
}
};
mTextView = (TextView) findViewById(R.id.textView);
mButton = (Button) findViewById(R.id.button);
mButton.setEnabled(false);
setupGoogleApiClient();
protected synchronized void setupGoogleApiClient() {
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addConnectionCallbacks(mConnectionCallbacks)
.addOnConnectionFailedListener(mOnConnectionFailedListener)
.addApi(LocationServices.API)
.build();
mGoogleApiClient.connect();
}
public void getLocation(View view) {
try {
Location lastLocation = LocationServices.FusedLocationApi.getLastLocation(
mGoogleApiClient);
if (lastLocation != null) {
mTextView.setText(
DateFormat.getTimeInstance().format(lastLocation.getTime()) + "n" +
"Latitude="+lastLocation.getLatitude()+"n"+
"Longitude="+lastLocation.getLongitude());
} else {
Toast.makeText(MainActivity.this, "null", Toast.LENGTH_LONG).show();
}
}
catch (SecurityException e) {}
}
Before we can call the getLastLocation() method, we need to set up GoogleApiClient. We call GoogleApiClient.Builder in our setupGoogleApiClient() method, and then connect to the library. When the library is ready, it calls our ConnectionCallbacks.onConnected() method. For demonstration purposes, this is where we enable the button.
We used a button to show that we can call getLastLocation() on demand; it's not a one-time call. The system is responsible for updating the location and may return the same previous location on repeated calls. (This can be seen in the timestamp—it's the location timestamp, not the timestamp that appears when the button is pressed.)
This approach of calling the location non-demand can be useful in situations where you only need the location when something happens in your app (such as geocoding an object). Since the system is responsible for the updates of the location, your app will not be responsible for draining your battery due to location updates.
The accuracy of the Location object we receive is based on our permission setting. We used ACCESS_COARSE_LOCATION, but if we want higher accuracy, we can request ACCESS_FINE_LOCATION instead using the following permission:
<uses-permission android_name="android.permission.ACCESS_FINE_LOCATION"/>
Lastly, to keep the code focused on GoogleApiClient, we just wrap getLastLocation() with SecurityException. In a production application, you should check and request the permission.
If a problem occurs when establishing a connection with GoogleApiClient, OnConnectionFailedListener is called. In this example, we will display a toast.
Testing the location can be a challenge since it's difficult to actually move the device when testing and debugging it. Fortunately, we have the ability to simulate GPS data with the emulator. (It is possible to create mock locations on a physical device as well, but it's not as easy.)
There are three ways to simulate locations using the emulator:
To set a mock location in Android Studio, follow these steps:
Here's a screenshot showing Location Controls:
Important: Simulating the location works by sending GPS data. Therefore, for your app to receive the mock location, it will need to receive GPS data. Testing lastLocation() may not send the mock GPS data since it doesn't rely solely on GPS to determine the location of the device. (We can't force the system to use any specific location sensor; we can only make a request. The system will choose the optimum solution to deliver results.)
Google Cloud Messaging (GCM), Google's version of a push notification, allows your application to receive messages. The idea is similar to SMS messages but much more flexible. There are three components of GCM:
When the user starts the application, your code needs to connect to the GCM server and obtain a device token, and then send this token to your server. Your server is responsible for initiating the message and passing it to the GCM server. Your server needs to track the device tokens to be sent when initiating the message (your server tells the GCM server which device tokens to send).
You can implement your own server or chose to use one of many services available (the Simple Testing Option section offers an option to verify whether your code works).
This recipe will walk you through the steps needed to add GCM using the current (version 8.3) Google Services library. Before getting to the steps, it's worth noting that GCM is supported all the way back to API 8 as long as the user has a Google account. A Google account is not required after installing Android 4.0.4.
Create a new project in Android Studio and call it GCM. Use the default Phone & Tablet options, and select Empty Activity when prompted for the Activity Type.
Google Cloud Messaging uses the Google Services Plugin, which requires a Google Services Configuration File, available from the Google Developer Console. To create the configuration file, you will need the following information:
Note that if you download the source files, you will need to create a new package name when following the steps as the existing package name has already been registered.
After completing the preceding section, follow these steps:
classpath 'com.google.gms:google-services:1.5.0-beta2'
apply plugin: 'com.google.gms.google-services'
compile 'com.google.android.gms:play-services-auth:8.3.0'
<uses-permission android_name="android.permission.WAKE_LOCK" />
<permission android_name="<packageName >.permission.C2D_MESSAGE"
android_protectionLevel="signature" />
<uses-permission android_name="<packageName >.permission.C2D_MESSAGE" />
<receiver
android_name="com.google.android.gms.gcm.GcmReceiver"
android_exported="true"
android_permission="com.google.android.c2dm.permission.SEND" >
<intent-filter>
<action android_name="com.google.android.c2dm.intent.RECEIVE" />
<category android_name="<packageName>" />
<action android_name="com.google.android.c2dm.intent.REGISTRATION" />
</intent-filter>
</receiver>
<service
android_name=".GCMService"
android_exported="false" >
<intent-filter>
<action android_name="com.google.android.c2dm.intent.GCM_RECEIVED_ACTION"/>
<action android_name="com.google.android.c2dm.intent.RECEIVE" />
</intent-filter>
</service>
<service
android_name=".GCMInstanceService"
android_exported="false">
<intent-filter>
<action android_name="com.google.android.gms.iid.InstanceID" />
</intent-filter>
</service>
<service
android_name=".GCMRegistrationService"
android_exported="false">
</service>
public class GCMRegistrationService extends IntentService {
private final String SENT_TOKEN="SENT_TOKEN";
public GCMRegistrationService() {
super("GCMRegistrationService");
}
@Override
protected void onHandleIntent(Intent intent) {
super.onCreate();
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
try {
InstanceID instanceID = InstanceID.getInstance(this);
String token = instanceID.getToken(getString(R.string.gcm_defaultSenderId),
GoogleCloudMessaging.INSTANCE_ID_SCOPE, null);
Log.i("GCMRegistrationService", "GCM Registration Token: " + token);
//sendTokenToServer(token);
sharedPreferences.edit().putBoolean(SENT_TOKEN, true).apply();
} catch (Exception e) {
sharedPreferences.edit().putBoolean(SENT_TOKEN, false).apply();
}
}
}
public class GCMInstanceService extends InstanceIDListenerService {
@Override
public void onTokenRefresh() {
Intent intent = new Intent(this, GCMRegistrationService.class);
startService(intent);
}
}
public class GCMService extends GcmListenerService {
@Override
public void onMessageReceived(String from, Bundle data) {
super.onMessageReceived(from, data);
Log.i("GCMService", "onMessageReceived(): " + data.toString());
}
}
Intent intent = new Intent(this, GCMRegistrationService.class);
startService(intent);
Most of the actual GCM code is encapsulated within the Google APIs, simplifying their implementation. We just have to set up the project to include the Google Services and give our app the required permissions.
Important: When adding the permissions in steps 5 and 6, replace the <packageName> placeholder with your application's package name.
The most complicated aspect of GCM is probably the multiple services that are required. Even though the code in each service is minimal, each service has a specific task. There are two main aspects of GCM:
This is the code to register with the GCM server:
String token = instanceID.getToken(getString(R.string.gcm_defaultSenderId),
GoogleCloudMessaging.INSTANCE_ID_SCOPE, null);
We don't call getToken() in the Activity because it could block the UI thread. Instead, we call GCMRegistrationService, which handles the call in a background thread. After you receive the device token, you need to send it to your server as it is needed when initiating a message.
Receiving a GCM message is handled in GCMService, which extends GcmListenerService. Since the Google API already handles most of the work, all we have to do respond to the onMessageReceived() callback.
To make it easier to type, we left out an important Google Services API verification, which should be included in any production application. Instead of calling GCMRegistrationService directly, as we did in onCreate() previously, first check whether the Google API Service is available. Here's an example that shows how to call the isGooglePlayServicesAvailable() method:
private boolean isGooglePlayServicesAvailable() {
GoogleApiAvailability googleApiAvailability = GoogleApiAvailability.getInstance();
int resultCode = googleApiAvailability.isGooglePlayServicesAvailable(this);
if (resultCode != ConnectionResult.SUCCESS) {
if (googleApiAvailability.isUserResolvableError(resultCode)) {
googleApiAvailability.getErrorDialog(this, resultCode, PLAY_SERVICES_RESOLUTION_REQUEST)
.show();
} else {
Toast.makeText(MainActivity.this, "Unsupported Device", Toast.LENGTH_SHORT).show();
finish();
}
return false;
}
return true;
}
Then, change the onCreate() code to call this method first:
if (isGooglePlayServicesAvailable()) {
Intent intent = new Intent(this, GCMRegistrationService.class);
startService(intent);
}
To verify whether your code is working correctly, a testing application was created and posted on Google Play. This app will run on both a physical device and an emulator. The Google Play listing also includes a link to download the source code to run the project directly, making it easier to enter the required fields.
Take a look at GCM (Push Notification) Tester at https://play.google.com/store/apps/details?id=com.eboyer.gcmtester.
In this article, we learned how to make a Flashlight with a Heads-up notification, scaling down large images to avoid out-of-memory exceptions, how to get last location and using push notification with GCM.