2013年7月2日 星期二

Android Service

For simplicity,
For those running in foreground, they are called 'Activity' (those applications we can see and use in our mobile)
For those running in background, they are called 'Service' (those we cannot see while running in background and keep consuming your mobile's RAM. Haha they are one of the evilest guys that make your mobile so slow)


First of all, take a look at Service's life cycle: onCreate()->onStartCommand(Intent, int, int)->onDestroy()
You can see that it is a bit different from that of Activity. Today we are going to work on onStartCommand(Intent, int, int) only.

Service is mostly used in combination with Notification, a typical one is like:
1. Service does something in background secretly
2. Service finished, it triggers a Notification to notify user
3. User clicks the Notification and open the Application (Activity) to view the result

So we are going to make use of the Notification Application and fine tune it into a Service Notification Application (For detail about the Notification Application, please refer to http://nevescheng.blogspot.hk/2013/05/notification-with-big-view.html)

Do some restructure, create one more NotificationUtil Class :
package com.example.sample_service_notification;

import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;

public class NotificationUtil {
 public final static int NOTI_MODE_BASIC = 0;
 public final static int NOTI_MODE_BIG_TEXT = 1;
 public final static int NOTI_MODE_BIG_PIC = 2;
 public final static int NOTI_MODE_INBOX = 3;
 
 public static void setNotification(int mode, Context mContext, Class mClass){
  int mId = 1;
  String contentTitle = "Service Notification Test:";
  String contentText = "Service Content";
  String contentInfo = "Service Info";
  String contentText2 = "Service Content2";
  // Using NotificationCompat to create the notification builder can use
  // the new features introduced after API level 4 without compatibility  
  // problem with lower API level
  NotificationCompat.Builder mBuilder =
    new NotificationCompat.Builder(mContext)
    .setSmallIcon(R.drawable.ic_launcher)
    .setContentTitle(contentTitle)
    .setContentText(contentText)
    .setContentInfo(contentInfo);
   
  // A notification's big view appears only when the notification is expanded, 
  // which happens when the notification is at the top of the notification 
  // drawer, or when the user expands the notification with a gesture. 
  // Expanded notifications are available starting with Android 4.1.
  switch (mode){
  case NOTI_MODE_BIG_TEXT:
   NotificationCompat.BigTextStyle bigTextStyle 
   = new NotificationCompat.BigTextStyle();
   bigTextStyle.bigText("Big Text Line 1\nBig Text Line 2\nBig Text Line 3");
   // summary is displayed replacing the position of contentText
   // if summary is not set, contentInfo will not be displayed too
   bigTextStyle.setSummaryText(contentText2);           
   // Moves the big view style object into the notification object.
   mBuilder.setStyle(bigTextStyle);
   break;
  case NOTI_MODE_BIG_PIC:
   NotificationCompat.BigPictureStyle bigPicStyle 
   = new NotificationCompat.BigPictureStyle();
   Bitmap bm = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.interact);
   bigPicStyle.bigPicture(bm); 
   bigPicStyle.setSummaryText(contentText2);      
   // Moves the big view style object into the notification object.
   mBuilder.setStyle(bigPicStyle);
   break;
  case NOTI_MODE_INBOX:
   NotificationCompat.InboxStyle inboxStyle 
   = new NotificationCompat.InboxStyle();
   String[] events = new String[4];
   events[0] = "Inbox Line 1";
   events[1] = "Inbox Line 2";
   events[2] = "Inbox Line 3";
   events[3] = "Inbox Line 4";
   // Moves events into the big view
   for (int i=0; i < events.length; i++) {
    inboxStyle.addLine(events[i]);
   }  
   inboxStyle.setSummaryText(contentText2);
   // Moves the big view style object into the notification object.
   mBuilder.setStyle(inboxStyle);      
   break;
  case NOTI_MODE_BASIC:
   break;
  }
     
  // Creates an explicit intent for an Activity in your app
  Intent resultIntent = new Intent(mContext, mClass);

  // The stack builder object will contain an artificial back stack
  // for the started Activity
  // This ensures that navigating backward from the Activity leads out of
  // your application to the Home screen.
  TaskStackBuilder stackBuilder = TaskStackBuilder.create(mContext);
  // Adds the back stack for the Intent (but not the Intent itself)
  stackBuilder.addParentStack(mClass);
  // Adds the Intent that starts the Activity to the top of the stack
  stackBuilder.addNextIntent(resultIntent);
  // A PendingIntent is used to specify the action which should be performed 
  // once the user select the notification.
  PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(
    0, PendingIntent.FLAG_UPDATE_CURRENT);
  mBuilder.setContentIntent(resultPendingIntent);
  // Just add a fake action here, max 3
  mBuilder.addAction(R.drawable.ic_action_search, "Search", resultPendingIntent);
  mBuilder.addAction(R.drawable.ic_action_search, "Add", resultPendingIntent);
  mBuilder.addAction(R.drawable.ic_action_search, "Save", resultPendingIntent);
  // Hide the notification after its selected
  mBuilder.setAutoCancel(true);
     
  NotificationManager mNotificationManager =
    (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
  // mId allows you to update the notification later on.
  mNotificationManager.notify(mId, mBuilder.build());
 }
}

The MainActivity Class after restructure:
package com.example.sample_service_notification;

import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

public class MainActivity extends Activity {
 private Button btn_basic;
 private Button btn_bigtext;
 private Button btn_bigpic;
 private Button btn_inbox;
 
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
     
  findViews();
  setListieners();
 }

 @Override
 public boolean onCreateOptionsMenu(Menu menu) {
  getMenuInflater().inflate(R.menu.activity_main, menu);
  return true;
 }
    
 private void findViews(){
  btn_basic = (Button) this.findViewById(R.id.btn_basic);
  btn_bigtext = (Button) this.findViewById(R.id.btn_bigtext);
  btn_bigpic = (Button) this.findViewById(R.id.btn_bigpic);
  btn_inbox = (Button) this.findViewById(R.id.btn_inbox);
 }
    
 private void setListieners(){
  btn_basic.setOnClickListener(btn_basic_click);
  btn_bigtext.setOnClickListener(btn_bigtext_click);
  btn_bigpic.setOnClickListener(btn_bigpic_click);
  btn_inbox.setOnClickListener(btn_inbox_click);
 }
  
 private OnClickListener btn_basic_click = new OnClickListener() {
  public void onClick(View v) {
   // TODO Auto-generated method stub
   NotificationUtil.setNotification(NotificationUtil.NOTI_MODE_BASIC, MainActivity.this, MainActivity.class);
  }
 };
 
 private OnClickListener btn_bigtext_click = new OnClickListener() {
  public void onClick(View v) {
   // TODO Auto-generated method stub
   NotificationUtil.setNotification(NotificationUtil.NOTI_MODE_BIG_TEXT, MainActivity.this, MainActivity.class);
  }
 };
 
 private OnClickListener btn_bigpic_click = new OnClickListener() {
  public void onClick(View v) {
   // TODO Auto-generated method stub
   NotificationUtil.setNotification(NotificationUtil.NOTI_MODE_BIG_PIC, MainActivity.this, MainActivity.class);
  }
 };
 
 private OnClickListener btn_inbox_click = new OnClickListener() {
  public void onClick(View v) {
   // TODO Auto-generated method stub
   NotificationUtil.setNotification(NotificationUtil.NOTI_MODE_INBOX, MainActivity.this, MainActivity.class);  
  }
 };  
}

We will then make use of Receiver and Service to create a "Periodic Service" which is bind with the Activity


Create a new MainService Class:
package com.example.sample_service_notification;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;

public class MainService extends Service{
 private String TAG = "MainService"; 
 public int repeatTimes = 0;
 
 // MyBinder is used to bind the Service to the Activity
 public class MyBinder extends Binder {
  MainService getService() {
   return MainService.this;
  }
 }
 private final IBinder mBinder = new MyBinder();
 @Override
 public IBinder onBind(Intent arg0) {
  //TODO for communication return IBinder implementation
  return mBinder;
 }
 
 public void onCreate(){
  super.onCreate();
  Log.d(TAG,"onCreate");
 }
 
 @Override
 public int onStartCommand(Intent intent, int flags, int startId){
  //TODO do something useful
  Log.d(TAG,"onStartCommand");
  repeatTimes = repeatTimes + 1;
  NotificationUtil.setNotification(NotificationUtil.NOTI_MODE_BASIC, this, MainActivity.class);
  return Service.START_STICKY;
  
 }
 
 public void onDestroy(){
  super.onDestroy();
  Log.d(TAG,"onDestroy");
 }
}
Add in AndroidManifest.xml
<service
 android:name=".MainService"
 android:icon="@drawable/ic_launcher"
 android:label="@string/app_name" >
</service>
So here in onStartCommand, we simply ask the service to pop a Notification and add the counter repeatTimes by 1. Note the return Service.START_STICKY here, It means that if the service is killed while it is started, system will re-create the service later. If you don't want this just choose Service.START_NOT_STICKY.

Create a new StartServiceReceiver Class:
package com.example.sample_service_notification;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

public class StartServiceReceiver extends BroadcastReceiver {
 @Override
 public void onReceive(Context context, Intent intent) {
  // Used to receive message from ScheduleReceiver and then call the MainService
  // Act as the bridge between ScheduleReceiver and MainService
  Intent service = new Intent(context, MainService.class);
  context.startService(service);
 }
}
Add in AndroidManifest.xml
<receiver android:name="StartServiceReceiver" ></receiver>
It act as a bridge between the ScheduleReceiver  and MainService, after receiving message from ScheduleReceiver, start MainService.

Create a new ScheduleReceiver Class:
package com.example.sample_service_notification;

import java.util.Calendar;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

public class ScheduleReceiver extends BroadcastReceiver {
 // Restart service every 60 seconds
 private static final long REPEAT_TIME = 1000 * 60;
 
 @Override
 public void onReceive(Context context, Intent intent) {
  AlarmManager service = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
  Intent i = new Intent(context, StartServiceReceiver.class);
  PendingIntent pending = PendingIntent.getBroadcast(context, 0, i,
    PendingIntent.FLAG_CANCEL_CURRENT);
  Calendar cal = Calendar.getInstance();
  // Start 30 seconds after boot completed
  cal.add(Calendar.SECOND, 30);
  //
  // Fetch every 60 seconds
  // Use InexactRepeating instead of setRepeating as
  // InexactRepeating allows Android to optimize the energy consumption
  service.setInexactRepeating(AlarmManager.RTC_WAKEUP,
    cal.getTimeInMillis(), REPEAT_TIME, pending);
  // service.setRepeating(AlarmManager.RTC_WAKEUP, cal.getTimeInMillis(),
  // REPEAT_TIME, pending);
 }
}
Add in AndroidManifest.xml
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
……
<receiver android:name="ScheduleReceiver" >
 <intent-filter>
  <action android:name="android.intent.action.BOOT_COMPLETED" />
 </intent-filter>
</receiver>


Finally modify the MainActivity  Class to bind with the MainService:
package com.example.sample_service_notification;

import android.os.Bundle;
import android.os.IBinder;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends Activity {
 private Button btn_basic;
 private Button btn_bigtext;
 private Button btn_bigpic;
 private Button btn_inbox;
 private Button btn_check_service_repeat;
 private MainService myMainService;
 
 @Override
 public void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
     setContentView(R.layout.activity_main);
     
     findViews();
     setListieners();
 }

 @Override
 public boolean onCreateOptionsMenu(Menu menu) {
     getMenuInflater().inflate(R.menu.activity_main, menu);
     return true;
 }
    
 private void findViews(){
  btn_basic = (Button) this.findViewById(R.id.btn_basic);
  btn_bigtext = (Button) this.findViewById(R.id.btn_bigtext);
  btn_bigpic = (Button) this.findViewById(R.id.btn_bigpic);
  btn_inbox = (Button) this.findViewById(R.id.btn_inbox);
  btn_check_service_repeat = (Button) this.findViewById(R.id.btn_check_service_repeat);
 }
    
 private void setListieners(){
  btn_basic.setOnClickListener(btn_basic_click);
  btn_bigtext.setOnClickListener(btn_bigtext_click);
  btn_bigpic.setOnClickListener(btn_bigpic_click);
  btn_inbox.setOnClickListener(btn_inbox_click);
  btn_check_service_repeat.setOnClickListener(btn_check_service_repeat_click);
 }
  
 private OnClickListener btn_basic_click = new OnClickListener() {
  public void onClick(View v) {
   // TODO Auto-generated method stub
   NotificationUtil.setNotification(NotificationUtil.NOTI_MODE_BASIC, MainActivity.this, MainActivity.class);
  }
 };
 
 private OnClickListener btn_bigtext_click = new OnClickListener() {
  public void onClick(View v) {
   // TODO Auto-generated method stub
   NotificationUtil.setNotification(NotificationUtil.NOTI_MODE_BIG_TEXT, MainActivity.this, MainActivity.class);
  }
 };
 
 private OnClickListener btn_bigpic_click = new OnClickListener() {
  public void onClick(View v) {
   // TODO Auto-generated method stub
   NotificationUtil.setNotification(NotificationUtil.NOTI_MODE_BIG_PIC, MainActivity.this, MainActivity.class);
  }
 };
 
 private OnClickListener btn_inbox_click = new OnClickListener() {
  public void onClick(View v) {
   // TODO Auto-generated method stub
   NotificationUtil.setNotification(NotificationUtil.NOTI_MODE_INBOX, MainActivity.this, MainActivity.class);  
  }
 };  
 
 private OnClickListener btn_check_service_repeat_click = new OnClickListener() {
  public void onClick(View v) {
   // TODO Auto-generated method stub
   Toast.makeText(MainActivity.this, String.valueOf(myMainService.repeatTimes),
     Toast.LENGTH_SHORT).show();
  }
 };
    
 @Override
 protected void onResume() {
  super.onResume();
  doBindService();
 }
 
 @Override
 protected void onPause() {
  super.onPause();
  unbindService(mConnection);
 }
    
 private ServiceConnection mConnection = new ServiceConnection() {
  public void onServiceConnected(ComponentName className, IBinder binder) {
   myMainService = ((MainService.MyBinder) binder).getService();
   Toast.makeText(MainActivity.this, "Connected",
     Toast.LENGTH_SHORT).show();
  }
  
  public void onServiceDisconnected(ComponentName className) {
   myMainService = null;
   Toast.makeText(MainActivity.this, "Disconnected",
     Toast.LENGTH_SHORT).show();
  }
 };
    
 private void doBindService() {
  //The activity binds itself to the service to access its data.
  bindService(new Intent(this, MainService.class), mConnection,
    Context.BIND_AUTO_CREATE);
 }
}

Resulting Manifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
 package="com.example.sample_service_notification"
 android:versionCode="1"
 android:versionName="1.0" >

 <uses-sdk
  android:minSdkVersion="8"
  android:targetSdkVersion="15" />
 <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
 
 <application
  android:icon="@drawable/ic_launcher"
  android:label="@string/app_name"
  android:theme="@style/AppTheme" >
  <activity
   android:name=".MainActivity"
   android:label="@string/title_activity_main" >
   <intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
   </intent-filter>
  </activity>
        
  <service
   android:name=".MainService"
   android:icon="@drawable/ic_launcher"
   android:label="@string/app_name" >
  </service>
  <receiver android:name="ScheduleReceiver" >
   <intent-filter>
    <action android:name="android.intent.action.BOOT_COMPLETED" />
   </intent-filter>
  </receiver>
  <receiver android:name="StartServiceReceiver" ></receiver>        
 </application>

</manifest>

Resulting activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/LinearLayout1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <Button
        android:id="@+id/btn_basic"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Basic" />

    <Button
        android:id="@+id/btn_bigtext"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Big Text" />

    <Button
        android:id="@+id/btn_bigpic"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Big Pic" />

    <Button
        android:id="@+id/btn_inbox"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Inbox" />
    
    <Button
        android:id="@+id/btn_check_service_repeat"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Check Service Repeat" />    

</LinearLayout>

Last but not least, after installing the app (boot-up service), it will only be active after the device/emulator is rebooted

Result:

Wait for a while and you will see there is a notification icon appears in the notification bar

Click the "Check Service Repeat" button to see how many times the Service has run

Drag down the notification bar, notice that the time captured is 3:41 AM

Close the application, wait for a while and the notification icon appears again, the time captured now is 3:43 AM

沒有留言:

張貼留言