Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Android

AutoRing Android Service Creation Walkthrough

4.89/5 (23 votes)
30 Apr 2014CPOL19 min read 62.9K   2.8K  
Create a simple service which listens for connection changes

Introduction

This walkthrough will cover the creation of a service application and the following topics:

  • Service creation
  • Using notifications
  • Context menus
  • Battery status checks
  • Persisting user settings
  • Creating icons

The project is built using Eclipse and the Android SDK.

Background

I created this app to simulate the ring behavior of the Motorola RAZR which would switch to loud when charging and return to vibrate-only when disconnected (no manual adjustment needed). This app also allows the reverse scenario (silent when connected otherwise ring).

This tutorial assumes you already have the Eclipse environment up and running. If you are new to Eclipse and Android development, I recommend going through the temperature converter tutorial which can be found here.

Using the Code

You can create the project by going through the steps listed below. If you prefer to load the entire project, download\unzip the project file, then open Eclipse and choose File->Import..->General->Existing Projects and choose the root folder of the AutoRing project.

Let's begin:

Start Eclipse (I'm using Eclipse Classic version 3.6.2).

Choose File -> New -> Project -> Android -> Android Project

Image 1

Click Next.

Fill in the fields as shown below. You can use any version of Android 1.5 or later.

Image 2

Click Finish.

Once the project is created, add these icons to the AutoRing\res\drawable folder. You can drag them directly to the folder in Eclipse or you can use Windows Explorer. If you use Explorer, right-click the folder in Eclipse and choose Refresh to see the new files. Be sure to name the files as listed below as the names will be referenced in our project. We will discuss creating your own icons at the end of this walkthrough.

Image 3 circlefill.png Image 4 circleopen.png Image 5 downarrow.png Image 6 mainicon.png

Once the icons are added, the expanded res\drawable folder should look like this:

Image 7

Open AndroidManifest.xml.

Image 8

Click on the last tab in the source editor to view the actual XML.

Image 9

If you are using Android 1.6 or later, update the sdk version to 4. At version 3, phone and storage permissions will be required to install this application. Android 1.5 requires sdk version 3 (the application won't start otherwise). This value could also have been set in the "New Android Project" dialog but either way works.

XML
<uses-sdk android:minSdkVersion="4" />

Update the application tag to set the application icon and title.

XML
<application android:icon="@drawable/mainicon" android:label="AutoRing">

Add the launchMode attribute to the activity tag. Set it to singleInstance. This will prevent multiple instance of our activity from running. Without this, the user can launch our app from the home screen then another instance from the notification screen (several actually).

XML
<activity android:name=".AutoRingActivity"
          android:label="@string/app_name"
          android:launchMode="singleInstance">

Add the service tag for the service we will create. Setting exported to false ensures that no other apps can talk to our service. Also add the receiver tag which is used with our RebootReceiver class to restart our service when the phone is restarted. If you prefer to restart the service manually, you can leave this part out.

XML
.........
   </activity>
   <service android:name=".AutoRingSvc" android:exported="false" />
   <receiver android:name="RebootReceiver">
     <intent-filter>
       <action android:name="android.intent.action.BOOT_COMPLETED" />
     </intent-filter>
   </receiver>
</application>
........

Open main.xml and click on the second tab to see the actual XML.

Image 10 Image 11

Remove the existing XML in main.xml.

Add the tag for our main activity.

XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:id="@+id/main_view"
    android:gravity="center_horizontal"
    xmlns:android="http://schemas.android.com/apk/res/android">

Add the tags for the 2 status text boxes.

XML
<TextView
    android:id="@+id/txtStatus"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="AutoRing is stopped"
    android:textAppearance="?android:attr/textAppearanceMedium"/>
<TextView
    android:id="@+id/txtBattStatus"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text=""
    android:textAppearance="?android:attr/textAppearanceMedium"/>

Add the tags for the option buttons. The marginTop attribute will put some space above the button.

XML
<Button
    android:layout_marginTop="25sp"
    android:id="@+id/btnConnectedOption"
    android:text="When Connected:"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceMedium"
    android:layout_width="fill_parent"
    android:gravity="center_vertical|left"/>
<Button
    android:id="@+id/btnDisconnectedOption"
    android:text="When Disconnected:"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceMedium"
    android:layout_width="fill_parent"
    android:gravity="center_vertical|left"/>

Finally, add the tag for the service Start\Stop button.

XML
<Button
    android:layout_marginTop="40sp"
    android:id="@+id/btnStartStop"
    android:text="Start AutoRing"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceMedium"
    android:layout_width="wrap_content"/>
</LinearLayout>

This layout should create the following GUI once our application is running (the arrows are added in code):

Image 12

Next, we will add the service class.

Right Click on the AutoRing project and choose New->Class.

Image 13

Enter the Name, Package and Superclass as shown below:

Image 14

Click Finish.

We need one more class. This will allow our service to restart automatically when the phone is restarted. If you prefer to restart the service manually after a phone restart, you can skip this step and the next coding step.

Image 15

Coding the RebootReceiver

There are only a few lines of code we need to add to the RebootReceiver class to restart our service. We read the StartOnReboot preference (set in AutoRingActivity.onClick) to determine if the service should be started. There may be several services trying to start at boot time, so we'll wait 30 seconds before starting our service. We'll use a Timer to delay the service start. We check the preferences again in (the unlikely) case the user started\stopped the service within the 30 second delay. Update the onReceive method as follows:

Java
@Override //this gets called after a phone reboot
public void onReceive(Context context, Intent intent) {

    //component context not enough, get app context for preferences
    final Context ctxt = context.getApplicationContext();

    //conditional service restart, only restart if it was running before reboot
    if (!ctxt.getSharedPreferences("AutoRing", 0).getBoolean("StartOnReboot", false))
        return; //don't start service

    //use timer to delay service start so phone remains responsive at boot
    (new java.util.Timer()).schedule(
        new java.util.TimerTask() {
            @Override
            public void run() //restart the service
            {     //recheck in case user started and 
                //stopped service within 30 seconds
                if (ctxt.getSharedPreferences
                ("AutoRing", 0).getBoolean("StartOnReboot", false))
                    ctxt.startService(
                    new Intent(ctxt, AutoRingSvc.class));
            }
        }, 30000); //wait 30 seconds
} //onReceive
  • getApplicationContext() is required to get the global context for our app. The context passed in to onReceive is the component context which does not contain the correct preferences.
  • The sleep() methods do not work here. I think the process gets dumped while sleeping.
  • Adding the BOOT_COMPLETED action to AndroidManifest.xml will trigger the RebootReceiver at bootup even if the app\service was never run (installed only).

Coding the AutoRingActivity Class

Open AutoRingActivity.java.

Image 16

This is the class that will allow the user to set options for our service class and also to start and stop the service. Note that we'll be coding some references to the service class even though it's not done yet. This will produce some compile errors until we code the service.

Remove all the existing code from AutoRingActivity.java.

Add the package name and imports needed for our application.

Java
package droid.ar;

import droid.ar.R;
import android.app.Activity;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.MenuItem;
import android.view.View.OnCreateContextMenuListener;
import android.widget.TextView;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Toast;
import android.content.Intent;
import android.view.View;

Create the AutoRingActivity class. Our class will implement 2 click listener interfaces so we can use our activity to process menu and button clicks (instead of creating a separate listener class).

Java
public class AutoRingActivity extends Activity
    //implement click listeners so this class can process menu & button clicks
    implements MenuItem.OnMenuItemClickListener, View.OnClickListener {

Add the class level variables for our activity pointers and button objects.

Java
//class level variables
AutoRingActivity mMainActivity;
LinearLayout mMainView;
Button btnConnectedOption, btnDisconnectedOption, btnStartStop;
private int mConnectAction = 2; //default is Ring
private int mDisconnectAction = 1; //default is Vibrate

Begin the onCreate handler. Do some housekeeping, then create the button objects and set their listeners. For Android 1.5, setting listeners must be done in code. After 1.5, the listeners can be declared in main.xml. Notice that we assigned our activity instance to the static AutoRingSvc.mMainActivity. We haven't coded the service yet, so Eclipse will give a compiler error.

Java
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    //housekeeping
    setContentView(R.layout.main);
    mMainView = (android.widget.LinearLayout) findViewById(R.id.main_view);
    AutoRingSvc.mMainActivity = this; //so service can update GUI
    mMainActivity = this; //for use in anonymous classes

    //get button objects
    btnConnectedOption = ((Button) findViewById(R.id.btnConnectedOption));
    btnDisconnectedOption = ((Button) findViewById(R.id.btnDisconnectedOption));
    btnStartStop = ((Button) findViewById(R.id.btnStartStop));

    //for Android 1.5 and less, we need to add the click listeners in code
    //after Android 1.5, these can be declared in main.xml 
         //(android:onClick="onClick")
    btnConnectedOption.setOnClickListener(this);
    btnDisconnectedOption.setOnClickListener(this);
    btnStartStop.setOnClickListener(this);       

Add code to load the user preferences. The variables will remain unchanged if the preferences are not set. The 0 (Context.MODE_PRIVATE) indicates we want to keep these preferences private to the AutoRing app.

Java
//get user preferences - use defaults if not set
mConnectAction = getSharedPreferences("AutoRing", 0)
        .getInt("ConnectAction", mConnectAction);
mDisconnectAction = getSharedPreferences("AutoRing", 0)
        .getInt("DisconnectAction", mDisconnectAction);

Add the call to the GUI update method.

Java
UpdateSvcStatus(); //update text boxes and buttons

Add the handler for the context menu. This will show the options menu when the user clicks an option button. We pass in the calling button instance as a tag to the view parameter. This will let us determine the correct heading for the menu. Each menu item is assigned the button id as the groupid so we'll know the calling button when we process the menu item click. The checked item is determined by the current action value. The service has a string static string array (mActionList) which lists the option choices. We use that to populate the context menu.

Java
    //this is called when we pop up the context menu (openContextMenu)
    mMainView.setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
        @Override
        public void onCreateContextMenu(
            ContextMenu menu, View v, ContextMenuInfo menuInfo) {
            //we're using the same context menu for the 
            //Connect and Disconnect buttons
            Button btnCaller = (Button)v.getTag(); //we passed the 
                        //calling button in the view tag
            //set menu heading according to calling button
            menu.setHeaderTitle(btnCaller == btnConnectedOption ?
                "When Connected" : "When Disconnected");
            //get action based on calling button and current setting
            int action = (v.getTag() == btnConnectedOption) ? 
                    mConnectAction : mDisconnectAction;
            //create the menu items according the mActionList string array
            //set the group id to the calling button id for later reference
            //all the menu items have the same listener
            for (int i=0; i<4; i++)
                menu.add(
                 btnCaller.getId(),i,0, AutoRingSvc.mActionList[i])
                 //set checked if this item matches current setting
                 .setChecked(action == i)  
                 //we implemented OnMenuItemClickListener
                 .setOnMenuItemClickListener(mMainActivity);  
            //set the menu items to checkable and exclusive 
            // so they appear as radio buttons
            menu.setGroupCheckable(btnCaller.getId(), true, true);
        } //onCreateContextMenu
    });
} //onCreate

This code should produce the following context menu when our app is running.

Image 17

Add the handler for the context menu selection. Remember that we set the GroupId of the context menu items to the id of the calling button. We use that to determine which option to update. We then set the option according to the menu item selected (item id). We then update the preferences collection and the GUI fields.

Java
//this is called when the user clicks a context menu item
//this is the implementation of OnMenuItemClickListener
public boolean onMenuItemClick(MenuItem item) {
    //the calling button instance was passed to the context menu
    //the groupid of the menu item is the calling button id
    //update service option value with new setting
    if (item.getGroupId() == R.id.btnConnectedOption)
        mConnectAction = item.getItemId();
    if (item.getGroupId() == R.id.btnDisconnectedOption)
        mDisconnectAction = item.getItemId();

    //store updated settings, these are read in AutoRingSvc.onCreate()
    getSharedPreferences("AutoRing", 0).edit()
     .putInt("ConnectAction", mConnectAction)
     .putInt("DisconnectAction", mDisconnectAction)
     .commit();

    UpdateSvcStatus(); //update GUI
    return true;
} //onMenuItemClick

Add the handler for button clicks. There are 2 option buttons and a Start\Stop button on the activity screen. The start button will start the service if it's not running, else it will be stopped. For the option buttons, we attach the button instance to the main view instance (we could also use a class variable) then manually launch the context menu. we could also attach the context menu to an GUI object (registerForContextMenu), but that would require a long press to activate the menu. We also set the StartOnReboot preference which is read in the RebootReceiver class. If the user manually stopped the service here, we don't want to start the service at boot time.

Java
//this is called when the user clicks a button on our GUI
//OnClickListener implementation
public void onClick(View view) {
    Button btn = (Button)view;
    if (btn.getId()== R.id.btnStartStop)
    {
        AutoRingSvc.mMainActivity = this; //so service can update GUI
        //store service state, this is read in RebootReceiver class
        //if user started service, we will need to restart on reboot
        getSharedPreferences("AutoRing", 0).edit()
         .putBoolean("StartOnReboot", !AutoRingSvc.mSvcStarted).commit();
        //create service intent (or connect to running service)
        Intent svc = new Intent(mMainActivity, AutoRingSvc.class);
        if (!AutoRingSvc.mSvcStarted) //if not started
            getApplicationContext().startService(svc); //start service
        else //already started, so attempt stop
            //stopService returns true if stopped successfully
            AutoRingSvc.mSvcStarted = 
                !getApplicationContext().stopService(svc); 
        UpdateSvcStatus(); //update GUI
    }
    //if user clicked on an option button
    if (btn.getId()== R.id.btnConnectedOption || 
        btn.getId()== R.id.btnDisconnectedOption)
    {
        mMainView.setTag(btn);     //pass calling button to context menu 
                    //so we get button id back
        openContextMenu(mMainView); //start option menu
    }
} //onClick

Add the method to update the GUI objects. There are 2 text fields to show service and connection status. If the service is running, disable the option buttons. Set the option button text to the selected option value. For Android 1.6 and above, we'll add the arrow icon to the buttons so users will know they are option buttons. It seems that Android 1.5 and earlier don't support button icons. We remove the icon when the button is disabled because the icon will not turn light gray like the button, which looks odd.

Java
public void UpdateSvcStatus() {
    //update the GUI text fields with the battery and service status
    ((TextView) findViewById(R.id.txtBattStatus)).setText(AutoRingSvc.mBatteryStatus);
    ((TextView) findViewById(R.id.txtStatus)).setText
    ("AutoRing is "+(AutoRingSvc.mSvcStarted? "running": "stopped"));
    //set text of Start\Stop button
    btnStartStop.setText((AutoRingSvc.mSvcStarted? "Stop": "Start") + " AutoRing");

    //if service is running disable the option buttons, else enable them
    btnConnectedOption.setEnabled(!AutoRingSvc.mSvcStarted);
    btnDisconnectedOption.setEnabled(!AutoRingSvc.mSvcStarted);

    //update option button text with current settings
    btnConnectedOption.setText("When Connected: " + 
            AutoRingSvc.mActionList[mConnectAction]);
    btnDisconnectedOption.setText("When Disconnected: " + 
            AutoRingSvc.mActionList[mDisconnectAction]);

    //must be Android 1.6 or higher to add arrow icon to buttons
    if (Integer.parseInt(android.os.Build.VERSION.SDK) > 3)
    {
        //add arrow icons to option buttons when enabled
        btnConnectedOption.setCompoundDrawablesWithIntrinsicBounds
        (0,0, btnConnectedOption.isEnabled()? R.drawable.downarrow: 0, 0);
        btnDisconnectedOption.setCompoundDrawablesWithIntrinsicBounds
        (0,0, btnDisconnectedOption.isEnabled()? R.drawable.downarrow: 0, 0);
    }
} //UpdateSvcStatus

Add the following methods to finish off our activity class. onPause is called when our activity is moved to the background (user started another app) so we can disconnect from the service. onResume is called when the app is moved back to the foreground, so can reconnect to the service for updates. onDestroy is called when our app is stopped. The Toast call is just to prove that the GUI has ended but our service is still running. You should remove this for a production app.

Java
    @Override
    public void onPause() { //app moved to background, no need for updates
        super.onPause();
        AutoRingSvc.mMainActivity = null; //disconnect from service
    }

    @Override
    public void onResume() { //app moved to foreground (also occurs at app startup)
        super.onResume();
        AutoRingSvc.mMainActivity = this; //reconnect to service
        UpdateSvcStatus(); //update GUI
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        //show message so we know when the GUI app is gone and 
        //the service is still running.
        Toast.makeText( mMainView.getContext(), " Goodbye GUI ",
            Toast.LENGTH_SHORT).show();
    }
} //AutoRingActivity

Coding the Service Class

Open AutoRingSvc.java and delete the existing code.

Add the package name and imports needed for our service.

Java
package droid.ar;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.os.BatteryManager;
import android.os.IBinder;
import android.widget.Toast;

Add the static variables which will be used to share data between the service and activity class. Also add the default notification volume and status indicator. The 2 action variables are set to the defaults. You can change these values to your preferred settings.

Java
public class AutoRingSvc extends Service {

    //we use static variables to share data between the service and GUI app
    //we could use binding instead, but this is simpler
    public static AutoRingActivity mMainActivity;
    public static String mBatteryStatus = "";
    public static boolean mSvcStarted = false;

    private int mConnectAction = 2; //default is Ring
    private int mDisconnectAction = 1; //default is Vibrate
    private int mNotificationVolume = 5; //Default volume
    private int mLastStatus = -1;

Add the static action list array. This will be used to populate the option menu and also used as the ringer status text. The android constants for ringer mode is 0-2, which lets us use our array index instead of the constants when setting the ringer mode.

Java
//possible actions when connected\disconnected
//we sorted them according to the RINGER values just for shorter code
public static String[] mActionList = {
    "Silent",     //AudioManager.RINGER_MODE_SILENT = 0
    "Vibrate Only",    //AudioManager.RINGER_MODE_VIBRATE = 1
    "Ring",     //AudioManager.RINGER_MODE_NORMAL = 2
    "No Change"};    //No constant for this

Add the required onBind handler. This is another (more complicated) way to talk to a running service. We will stop our service to make setting changes, so onBind is not needed. Just ignore this event.

Java
@Override //required for Service class
public IBinder onBind(Intent arg0) { return null; }

Add the onCreate handler. This is called each time the service starts. If the OS gets low on memory, it may stop our service. When memory is freed up, our service will be restarted automatically and onCreate is called again.

We tell Android we want to receive battery status updates with the registerReceiver call.

SetForeground is used to tell Android we want to keep our service running even if memory gets low. SetForeground only works in Android 2.0 and less. Starting in 2.1, we would need to start the service using startForeground.

The user preferences are taken from the preferences collection (set in AutoRingActivity.onMenuItemClick).

We also log the service start here. If you're debugging using your phone (not a virtual device), you will need to get a (free) LogCat viewer from the Android market. Log messages will not show up in the Eclipse debugger unless you use a virtual device.

Java
@Override //Called on service start (by GUI or OS)
public void onCreate() {
    super.onCreate();
    android.util.Log.d("AutoRing","Service Starting");
    //popup a message from the service. This will appear even if no GUI.
    Toast.makeText(getApplicationContext(), 
        " Starting ", Toast.LENGTH_SHORT).show();
    //listen for connect\disconnect
    registerReceiver( batteryReceiver, 
        new IntentFilter(Intent.ACTION_BATTERY_CHANGED) );

    //retrieve stored settings that were set in GUI
    //SharedPreferences object is not shared across 
    //processes so we can use any name
    //keep default values if preference not set (should never happen)
    mConnectAction = getSharedPreferences("AutoRing", 0)
                .getInt("ConnectAction", mConnectAction);
    mDisconnectAction = getSharedPreferences("AutoRing", 0)
                .getInt("DisconnectAction", mDisconnectAction);
    mNotificationVolume = getSharedPreferences("AutoRing", 0)
                .getInt("NotVol", mNotificationVolume);
    mSvcStarted = true;
    setForeground(true); //only has affect before 2.0. In 2.0 use startForeground()
} //onCreate 

If you are using Android 2.0 or later, you can include the onStartCommand handler. This will allow you to use an Intent object to pass settings to the service at startup instead of using shared static values and the SharedPreferences class. This walkthrough is compatible to version 1.5, so I have this code commented.

Java
/*
@Override   //only exists in 2.0 and later
public int onStartCommand(Intent intent, int flags, int startId) {
    //add code here to get Intent data (user options)
    //if service is killed and restarted by the OS, resend the Intent info
    return START_REDELIVER_INTENT;
}
*/

Add the handler for the battery status changes. The main work of our service is done here. When the phone is disconnected, the battery status becomes BATTERY_STATUS_DISCHARGING. Any other status indicates that the phone is plugged in. When the handler is triggered, we change the ringer state (on\off\vibrate) and notification (like SMS) volume (zero\non-zero) based on the user settings. We then store the notification volume in the preferences in case of service restart. We then update the GUI app if it is running. Finally, we update the notification icon and text in the phone's status bar.

Java
//the main listener for connect\disconnect
//we need to turn the main ringer on\off
//we also need to mute\ unmute the notification volume (ie Text Msg)
public BroadcastReceiver batteryReceiver = new BroadcastReceiver() {
@Override
public void onReceive( Context context, Intent intent )
{
    int newStatus = intent.getIntExtra( "status", 0 );
    if (newStatus != mLastStatus) //status change
    {
        AudioManager am = (AudioManager)context.getSystemService
                    (Context.AUDIO_SERVICE);
        //store max vol for later
        int maxRingVol = am.getStreamMaxVolume(AudioManager.STREAM_RING);
        //get current notification volume
        int notVol = am.getStreamVolume(AudioManager.STREAM_NOTIFICATION);
        int curAction = 
           (newStatus == BatteryManager.BATTERY_STATUS_DISCHARGING || 
            newStatus == BatteryManager.BATTERY_STATUS_NOT_CHARGING) ? 
            mDisconnectAction : mConnectAction;
        mBatteryStatus =   //update text box
          (newStatus == BatteryManager.BATTERY_STATUS_DISCHARGING || 
           newStatus == BatteryManager.BATTERY_STATUS_NOT_CHARGING ?
            "Not ": "") +"Connected ("+mActionList[curAction]+")";
        if (curAction < 3) //skip if action=No Change
        {
            am.setRingerMode(curAction); //set main ringer
            //change notification volume according to action
            if (notVol > 0 && curAction < 2) //Silence or Vibrate
            {
                //store current volume for later
                mNotificationVolume = notVol; 
                am.setStreamVolume(AudioManager.STREAM_RING, 0, 0); //force 0 volume
                am.setStreamVolume( //set mute
                    AudioManager.STREAM_NOTIFICATION, 0, 0);
                getSharedPreferences("AutoRing", 0)
                 .edit() //store for svc restart
                 .putInt("NotVol", mNotificationVolume).commit();
            }
            else //set ringer loud 
            {
               if (curAction==2 && notVol==0) //if action=Ring 
                 //and currently muted, reset volume
               am.setStreamVolume(AudioManager.STREAM_NOTIFICATION,
                  mNotificationVolume, 0);
               am.setStreamVolume( //force max volume
                 AudioManager.STREAM_RING, maxRingVol, 0);
            }
        }
        //if the GUI app is connected, update app screen
        if (mMainActivity!=null) mMainActivity.UpdateSvcStatus();
        mLastStatus = newStatus;
        //update status text and icon
        DoNotify(!(newStatus == BatteryManager.BATTERY_STATUS_DISCHARGING || 
                   newStatus == BatteryManager.BATTERY_STATUS_NOT_CHARGING)); 
    }
}}; //batteryReceiver

Add the method to update the notification bar on the phone. We change the icon based on the connection status. We set the notification text based on the user settings and phone status. Use the FLAG_ONGOING_EVENT flag so our notification will be in the Ongoing list and the Clear button will not delete our message. The text is set in the status bar next to our icon and also the text which appears in the notification drop down.

Java
private void DoNotify(boolean connected) //update notification text and icon
{
    //set notification icon and text in status bar
    Notification notification = new Notification (
        connected? R.drawable.circlefill: R.drawable.circleopen,
        mBatteryStatus, System.currentTimeMillis());
    //set text in notification list
    Intent notificationIntent = new Intent(this, AutoRingActivity.class);
    notification.setLatestEventInfo(
        getApplicationContext(), "AutoRing", mBatteryStatus,
        PendingIntent.getActivity(this, 0, notificationIntent, 0));
    notification.flags |= Notification.FLAG_ONGOING_EVENT; //Cannot be cleared
    //trigger notification
    ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE))
        .notify(1, notification);
} //DoNotify

Once the app is up and running, the AutoRing notifications will look like this. We're using the mBatteryStatus variable for the text in the notification list. This is set in the BroadcastReceiver method. Note that the system icons are hidden while our status is showing the status bar so our text can fill the entire bar. If the message exceeds that length of the bar, the message will scroll line by line. You can also use "\n" in the message to explicitly cause scrolling.

Image 18 Image 19

Finish the service class by adding the onDestroy handler. We log the event and show a popup to confirm it happened. We then unregister the battery event listener then cancel the notification so the icon and text are removed from the status bar and notification list. There doesn't seem to be a built-in way to determine if the service is started\stopped, so we explicitly set the mSvcStarted variable. We set this variable to true in the onCreate method.

onDestroy is called when we use the stopService() call in AutoRingActivity and when the user uses the "Running services" app (Android 2.0 and later) in the Android Settings. onDestroy is not called when the user uses Force stop in the "Manage applications" app in the Android settings or when using adp kill (discussed later).

Java
    @Override //called when service is stopped
    public void onDestroy() {
        super.onDestroy();
        android.util.Log.d("AutoRing","Service Stopping");
        Toast.makeText(getApplicationContext(), 
            " Stopping ", Toast.LENGTH_SHORT).show();
        unregisterReceiver(batteryReceiver); //disconnect listener
        //remove notification icon and text
        ((NotificationManager) getSystemService
            (Context.NOTIFICATION_SERVICE)).cancelAll();
        mSvcStarted = false;
    }
} //AutoRingSvc

And that's it. We're done coding our application.

Build the project (Project->Build All). If you have Build Automatically set, the project will rebuild each time you save a source file.

Running the App

To see the connect\disconnect functionality actually work, you will need to connect your phone to the computer (virtual devices are always connected):

  • Connect your phone using a USB cable (you may need to install the USB drivers for your phone)
  • On your phone, in Settings->Application->Development, enable USB debugging
  • USB Storage should be disabled on your phone

In Eclipse, press F11 to start debugging.

After a few seconds (if everything goes right), the application should start on your phone.

If the Eclipse starts a virtual device instead:

  • Close the virtual device
  • Go to Run->Debug Configurations
  • In the configuration settings under Target, choose Manual. This will allow you to choose which device to debug on.

Image 20

Click Debug then select Choose a running Android device in the next screen and click OK to start debugging.

Image 21

If you don't see any running devices, confirm that you phone is attached to your PC and you have a working USB cable. You can also download USBDeview (freeware) which lists the attached USB devices on your PC.

To exit the app, use the back button on your phone or choose Run->Terminate in Eclipse.

To install the application to your phone, using an APK file

On your phone, in Settings->Applications, enable Unknown sources to allow non-market apps on your phone.

In Eclipse, choose File->Export..->Android-> Export Android Application.

Image 22

Click Next.

Enter AutoRing as the project name.

Image 23

Click Next.

If you already have a keystore, choose Use existing keystore. If not, here are the steps to create one.

Choose Create new keystore. Enter a file name (no extension is needed) and a password.

Image 24

Click Next.

For Alias and Password, you can use the same values you entered into the previous screen. Set validity to 100 years. Enter any name in the Name field. If you plan to publish any apps using this keystore, you should probably use your real information.

Image 25

Click Next.

Enter the file name for your apk file.

Image 26

Click Finish.

To install the apk file onto your phone, use the adb tool in the android-sdk\platform-tools folder. If you don't know the folder, just search your computer for adb.exe.

To install the apk file, use this command line:

adb install C:\AutoRing.apk

You can also use one of the (free) installer apps from the Android market which lets you install apk files from the phone's SD card.

Once the install is complete, AutoRing should be available in your phones application list.

Testing the Service Restart Logic

As mentioned previously, onDestroy is called when we use the stopService() call and when the user uses the "Running services" app (Android 2.0 and later) in the Android Settings. onDestroy is not called when the user uses Force stop in the "Manage applications" app in the Android settings.

If you want to simulate a memory-low process kill\restart, you can use the adb.exe from your computer:

  • Ensure you only have one emulator running and your phone is not attached
  • Start the AutoRing service on the emulator
  • From a cmd prompt, go to the android-sdk\platform-tools folder and run adb shell
  • Enter ps. This will give you a list of processes running on your device (or emulator). Search for the process where name is droid.ar. This is the AutoRing service. Note the PID (second column) for the process.
  • Enter kill pid where the pid is for the droid.ar process. This will cause the service to be killed immediately (onDestroy is not called). It will also trigger a service restart, which confirms the restart logic is working.
  • Enter exit to quit the shell.

adb would not let me kill a process on my actual phone (Operation not permitted) so I tested only on the emulator.

Creating your Own Icons

All the icons used for Android apps are transparent PNG files. Microsoft Paint (the default Windows image editor) does not support transparent PNGs, so you will need to use another application if you want transparent pixels. Here are instructions for Photoshop and Paint.Net (a free image editor).

Some basic rules for Android icons: Image 27 Image 28 Image 29
  • Icon names must be lower case or numbers. Underscores ( _ ) are also allowed. Periods are allowed, but only the first part of the file name will be used as the resource name (myicon.abc.png is the same as myicon.xyz.png).
  • The notification icon (in the status bar) is 25x25 pixels and should be grayscale (though color is supported as in the blue icons above).
  • The main application icon is 72x72 pixels and can be full color. I saved mine as 8 bit color to reduce file size.
  • The arrow icon is just a copy of the Android arrow used for preferences. I used the gray version, but color icons will also work (though may look odd on a gray button). I tested color using this green arrow shown above.
PhotoShop (I'm using version 7, which is somewhat old):
  • Select File->New
  • In the "New" dialog, choose the Transparent option under Contents to allow for transparency. Set the size you want then click OK. The checkered pattern is the transparent part of the image.
  • Draw what you want, then save as a PNG file
  • If you have an existing image and want to set the background to transparent
    • Open the existing image
    • Choose Select->All, then choose Edit->Copy
    • Open a new image with Transparent selected
    • Paste the old image (Edit->Paste)
    • Use the Magic Wand Tool to select the pixels you want to set transparent, then click delete
    • You can also use the Background Eraser Tool to clear specific pixels

Paint.Net (This is freeware which has some impressive features). You can download it here.

  • Select File->New
  • Set the size you want then click OK
  • Choose Edit->Select All then press delete. This will set all the pixels to transparent.
  • Draw the image you want then save as a PNG file. I used 8 bit color to reduce the file size.
  • If you have an existing image and you want to set some pixels to transparent
    • Open the existing image
    • Use the Magic Wand tool to select the pixels you want to remove then press delete
    • You can also use the Eraser tool to remove specific pixels

The full Android icon design guidelines can be found here.

Additional Thoughts

  • The service class is not needed if you just want to test a listener or notifications. The listener will still run when the activity is moved to the background. The issues with this are that the OS will remove background GUI apps before removing services when memory gets low and that GUI apps are not restarted automatically.
  • Dealing with the API changes between 1.5 and 2.0 can be tedious. Be prepared for this if trying to write backward compatible apps. Cryptic crashes may indicate a version problem.
  • According to the version distribution chart, 97% of Android users use Android 2.1 and above. You can probably ignore version 1.5\1.6 when creating Android apps.
  • The AutoRing app may be better without the "No Change" option. If the user chooses this, the status text will show No Change instead of the actual ring state. Of course, it we set the actual ring state, it may confuse the user. Hmmm. Fortunately, this option is impractical and will rarely be used.
  • I only tested the reboot logic on my Ideos 2.2. The virtual machines for version 1.5\1.6\2.2 didn't let me power down the device.
  • In this app, we used a context menu to set app options. If you have many options to set, consider using the PreferenceActivity class where a separate activity is used for setting user preferences.
  • I couldn't get the main app icon to appear in the 1.5 emulator. I tried several icon versions. I think this is an emulator issue. The 2.2 emulator showed the icon correctly.
  • The virtual machine is a good way to get screen captures from your app. Just capture\copy the emulator screen (Alt+PrtScr) then crop using an image editor.
  • The main icon is a quickie creation using 3D Studio (very not free) but there are many free 3D modeling tools available if you want to give 3D stuff a try.
  • If you want a full app that can detect battery conditions (among many others), check out the Llama app in the Android market. It's also free.
  • I recently found out that there is a similarly named app in the Android market. That app is not related to this walkthrough. The naming is just coincidence.

Resources

"Copy from one, it's plagiarism; copy from two, it's research." - Wilson Mizner

And I think we're done. If you're still reading this, you have a high tolerance for boredom. I hope you found this walkthrough useful. If you found any part confusing or if you think I missed something, please let me know so I can update this page.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)