上方廣告

上方連結組

2013年9月5日 星期四

Service 進階應用

之前在這篇文章介紹了何謂 Service。但落落長一大篇的理論, 卻沒看到什麼很實務的應用, 所以這篇就決定以實務應用為切入點。

1. 如何使用 Service 下載檔案 ?

在這個例子裡具有兩個特性
    A. 檔案下載通常都是一次性的
    B. 不能在主執行緒使用 HTTP 傳輸, 所以必須要使用非同步的方法。

運氣很好, Android 官方有提供一個 IntentService 類別, 這個類別的特性完全符合我們的需求。使用上也非常簡單, 只需要把原本繼承 Service 的物件改成繼承 IntentService 類別, 接著覆寫其中的 onHandleIntent(Intent)方法即可 :



public class DownloadService extends IntentService {

 public static final String URL = "urlpath";
 public static final String FILENAME = "filename";
 public static final String FILEPATH = "filepath";

 public DownloadService() {
  super("DownloadService");
 }

 @Override
 protected void onHandleIntent(Intent intent) {
  String urlPath = intent.getStringExtra(URL);
  String fileName = intent.getStringExtra(FILENAME);
  File output = new File(Environment.getExternalStorageDirectory(),
    fileName);
  if (output.exists()) {
   output.delete();
  }

  InputStream stream = null;
  FileOutputStream fos = null;
  try {
   URL url = new URL(urlPath);
   URLConnection connection = url.openConnection();
   connection.connect();

   int fileLength = connection.getContentLength();
   System.out.println("Content Length : " + fileLength);

   fos = new FileOutputStream(output);
   stream = connection.getInputStream();
   InputStream reader = new BufferedInputStream(stream);

   byte[] data = new byte[1024];

   int next = -1;
   while ((next = reader.read(data)) != -1) {
    fos.write(data, 0, next);
   }

   reader.close();
  } catch (Exception e) {
  } finally {
   if (stream != null) {
    try {
     stream.close();
    } catch (IOException e) {
    }
   }
   if (fos != null) {
    try {
     fos.close();
    } catch (IOException e) {
    }
   }
  }
 }
}
由於 onHandleIntent 已經是非同步執行了, 所以我們不需要再將 HTTP 的方法另外再透過 AsyncTask 等類別進行非同步處理。

因為這個範例需要透過 HTTP 下載檔案, 而下載下來後會儲存在 SD 卡上, 所以在 Manifest 檔中除了要宣告這個 Service 外, 也記得要向使用者要求這兩個權限 。
 
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.intentservicesample"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="18" />

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.example.intentservicesample.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service android:name="com.example.intentservicesample.DownloadService"></service>
    </application>
</manifest>

接下來我們只要在 Activity 中 Button 按下去的事件中啟動它即可。
 
package com.example.intentservicesample;

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

public class MainActivity extends Activity {

 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  Button button = (Button) this.findViewById(R.id.button1);
  button.setOnClickListener(new OnClickListener() {

   @Override
   public void onClick(View arg0) {
    Intent intent = new Intent(MainActivity.this,
      DownloadService.class);
    intent.putExtra(DownloadService.FILENAME, "3.jpg");
    intent.putExtra(
      DownloadService.URL,
      "https://dl.dropboxusercontent.com/u/16912340/android/3.jpg");
    startService(intent);
   }
  });
 }
}

這個範例並沒有加入下載完成的通知機制, 是因為不想加入其它與 Service 無關的方法以免失焦。如果想要有下載完成的通知方法, 可以再搭配 Broadcast 與 BroadcastReceiver 來使用即可。

完整範例程式碼可以在這裡下載

2. 如何開啟一個啟動後持續運作的 Service ?

有時候我們希望一個 service 啟動後可以持續運作不中斷, 例如每隔一段時間去伺服器檢查是否有新訊息, 這時我們應該怎麼做 ?

首先, 我們一樣要先建立一個負責的 Service 類別, 程式碼如下 :
 
package com.example.infiniteservicesample;

import java.util.Random;

import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;

public class InfiniteService extends Service {
 public static final String ACTION = "com.example.infiniteservicesample";
 public static final String PARAM_RAND = "rand";
 public static final String URL = "URL";

 private Handler mHandler = new Handler();
 private long mDelay = 10000;
 
 @Override
 public IBinder onBind(Intent arg0) {
  return null;
 }

 @Override
 public int onStartCommand(Intent intent, int flags, int startId) {
  mHandler.postDelayed(mRunnable, 0);  
  return super.onStartCommand(intent, flags, startId);
 }

 @Override
 public void onDestroy() {
  mHandler.removeCallbacks(mRunnable);
  super.onDestroy();
 }

 private Runnable mRunnable = new Runnable() {

  @Override
  public void run() {
   try {
    Random r = new Random();
    int i = r.nextInt();

    broadcast(i);
   } catch (Exception e) {
    e.printStackTrace();
   } finally {
    mHandler.postDelayed(this, mDelay);
   }
  }
 };

 private void broadcast(int i) {
  Intent intent = new Intent();
  intent.setAction(ACTION);
  intent.putExtra(PARAM_RAND, i);
  this.sendBroadcast(intent);
 }
}


這邊和其它的 Service 類別沒什麼不同, 只是我們使用了 android.os.Handler 物件來管理要執行的任務(藍字部份)。而要執行的任務程式碼放在 Runnable 物件中 (紅字部份)。而每次任務執行完成時, 透過 sendBroadcast(Intent) 方式廣播通知其它人知道即可。

Runnable 物件在原生的 java 中最常拿來搭配 Thread 使用, 但要注意的是 Handler 物件本身仍是在主執行緒上執行。所以如果要存取 HTTP 的話, 仍需要自行處理非同步的部份。

而每次任務執行結束, 再透過 Handler 物件的 postDelayed(Runnable, int)方法交待多少毫秒後再執行一次任務, 如此循環, 這支 Service 就可以一直生存下去。

搭配 Broadcast/BroadcastReceiver/Notification 使用, 就可以達成每十秒接受一次資料的效果, 即使將 Activity 關閉仍會跑出 Notification 出來, 並藉由 Notification 來重新開啟應用程式。

其它 BroadcastReceiver 和 Notification 的部份, 不屬於本文重點, 在此就不貼程式碼了。有興趣的話可以下載原始碼自行研究囉。

沒有留言:

張貼留言