第8章 Android服務 |
|||||||||||||||||||
|
|
第8章 Android服務
服務(Service)是Android系統中4個應用程序組件之一(其他的組件詳見3.2節的內容)。服務主要用於兩個目的:後臺運行和跨進程訪問。通過啓動一個服務,可以在不顯示界面的前提下在後臺運行指定的任務,這樣可以不影響用戶做其他事情。通過AIDL服務可以實現不同進程之間的通信,這也是服務的重要用途之一。
本章內容
Service的生命週期
綁定Activity和Service
在BroadcastReceiver中啓動Service
系統服務
時間服務
在線程中更新GUI組件
AIDL服務
在AIDL服務中傳遞複雜的數據
8.1 Service起步
Service並沒有實際界面,而是一直在Android系統的後臺運行。一般使用Service爲應用程序提供一些服務,或不需要界面的功能,例如,從Internet下載文件、控制Video播放器等。本節主要介紹Service的啓動和結束過程(Service的生命週期)以及啓動Service的各種方法。
8.1.1 Service的生命週期
本節的例子代碼所在的工程目錄是src\ch08\ch08_servicelifecycle
Service與Activity一樣,也有一個從啓動到銷燬的過程,但Service的這個過程比Activity簡單得多。Service啓動到銷燬的過程只會經歷如下3個階段:
創建服務
開始服務
銷燬服務
一個服務實際上是一個繼承android.app.Service的類,當服務經歷上面3個階段後,會分別調用Service類中的3個事件方法進行交互,這3個事件方法如下:
1. public void onCreate(); // 創建服務
2. public void onStart(Intent intent, int startId); // 開始服務
3. public void onDestroy(); // 銷燬服務
一個服務只會創建一次,銷燬一次,但可以開始多次,因此,onCreate和onDestroy方法只會被調用一次,而onStart方法會被調用多次。
下面編寫一個服務類,具體看一下服務的生命週期由開始到銷燬的過程。
1. package net.blogjava.mobile.service;
2. import android.app.Service;
3. import android.content.Intent;
4. import android.os.IBinder;
5. import android.util.Log;
6.
7. // MyService是一個服務類,該類必須從android.app.Service類繼承
8. public class MyService extends Service
9. {
10. @Override
11. public IBinder onBind(Intent intent)
12. {
13. return null;
14. }
15. // 當服務第1次創建時調用該方法
16. @Override
17. public void onCreate()
18. {
19. Log.d("MyService", "onCreate");
20. super.onCreate();
21. }
22. // 當服務銷燬時調用該方法
23. @Override
24. public void onDestroy()
25. {
26. Log.d("MyService", "onDestroy");
27. super.onDestroy();
28. }
29. // 當開始服務時調用該方法
30. @Override
31. public void onStart(Intent intent, int startId)
32. {
33. Log.d("MyService", "onStart");
34. super.onStart(intent, startId);
35. }
36. }
在MyService中覆蓋了Service類中3個生命週期方法,並在這些方法中輸出了相應的日誌信息,以便更容易地觀察事件方法的調用情況。
讀者在編寫Android的應用組件時要注意,不管是編寫什麼組件(例如,Activity、Service等),都需要在AndroidManifest.xml文件中進行配置。MyService類也不例子。配置這個服務類很簡單,只需要在AndroidManifest.xml文件的<application>標籤中添加如下代碼即可:
1. <service android:enabled="true" android:name=".MyService" />
其中android:enabled屬性的值爲true,表示MyService服務處於激活狀態。雖然目前MyService是激活的,但系統仍然不會啓動MyService,要想啓動這個服務。必須顯式地調用startService方法。如果想停止服務,需要顯式地調用stopService方法,代碼如下:
1. public void onClick(View view)
2. {
3. switch (view.getId())
4. {
5. case R.id.btnStartService:
6.
startService(serviceIntent);
// 單擊【Start Service】按鈕啓動服務
7. break;
8. case R.id.btnStopService:
9.
stopService(serviceIntent);
// 單擊【Stop Service】按鈕停止服務
10. break;
11. }
12. }
其中serviceIntent是一個Intent對象,用於指定MyService服務,創建該對象的代碼如下:
1. serviceIntent = new Intent(this, MyService.class);
運行本節的例子後,會顯示如圖8.1所示的界面。
第1次單擊【Start Service】按鈕後,在DDMS透視圖的LogCat視圖的Message列會輸出如下兩行信息:
1. onCreate
2. onStart
然後單擊【Stop Service】按鈕,會在Message列中輸出如下信息:
1. onDestroy
下面按如下的單擊按鈕順序的重新測試一下本例。
【Start Service】→【Stop Service】→【Start Service】→【Start Service】→【Start Service】→【Stop Service】
測試完程序,就會看到如圖8.2所示的輸出信息。可以看出,只在第1次單擊【Start Service】按鈕後會調用onCreate方法,如果在未單擊【Stop Service】按鈕時多次單擊【Start Service】按鈕,系統只在第1次單擊【Start Service】按鈕時調用onCreate和onStart方法,再單擊該按鈕時,系統只會調用onStart方法,而不會再次調用onCreate方法。
在討論完服務的生命週期後,再來總結一下創建和開始服務的步驟。創建和開始一個服務需要如下3步:
(1)編寫一個服務類,該類必須從android.app.Service繼承。Service類涉及到3個生命週期方法,但這3個方法並不一定在子類中覆蓋,讀者可根據不同需求來決定使用哪些生命週期方法。在Service類中有一個onBind方法,該方法是一個抽象方法,在Service的子類中必須覆蓋。這個方法當Activity與Service綁定時被調用(將在8.1.3節詳細介紹)。
(2)在AndroidManifest.xml文件中使用<service>標籤來配置服務,一般需要將<service>標籤的android:enabled屬性值設爲true,並使用android:name屬性指定在第1步建立的服務類名。
(3)如果要開始一個服務,使用startService方法,停止一個服務要使用stopService方法。
8.1.2 綁定Activity和Service
本節的例子代碼所在的工程目錄是src\ch08\ch08_serviceactivity
如果使用8.1.1節介紹的方法啓動服務,並且未調用stopService來停止服務,這個服務就會隨着Android系統的啓動而啓動,隨着Android系統的關閉而關閉。也就是服務會在Android系統啓動後一直在後臺運行,直到Android系統關閉後服務才停止。但有時我們希望在啓動服務的Activity關閉後服務自動關閉,這就需要將Activity和Service綁定。
通過bindService方法可以將Activity和Service綁定。bindService方法的定義如下:
1.
public boolean bindService(Intent service,
ServiceConnection conn, int flags)
該方法的第1個參數表示與服務類相關聯的Intent對象,第2個參數是一個ServiceConnection類型的變量,負責連接Intent對象指定的服務。通過ServiceConnection對象可以獲得連接成功或失敗的狀態,並可以獲得連接後的服務對象。第3個參數是一個標誌位,一般設爲Context.BIND_AUTO_CREATE。
下面重新編寫8.1.1節的MyService類,在該類中增加了幾個與綁定相關的事件方法。
1. package net.blogjava.mobile.service;
2.
3. import android.app.Service;
4. import android.content.Intent;
5. import android.os.Binder;
6. import android.os.IBinder;
7. import android.util.Log;
8.
9. public class MyService extends Service
10. {
11. private MyBinder myBinder = new MyBinder();
12. // 成功綁定後調用該方法
13. @Override
14. public IBinder onBind(Intent intent)
15. {
16. Log.d("MyService", "onBind");
17. return myBinder;
18. }
19. // 重新綁定時調用該方法
20. @Override
21. public void onRebind(Intent intent)
22. {
23. Log.d("MyService", "onRebind");
24. super.onRebind(intent);
25. }
26. // 解除綁定時調用該方法
27. @Override
28. public boolean onUnbind(Intent intent)
29. {
30. Log.d("MyService", "onUnbind");
31. return super.onUnbind(intent);
32. }
33. @Override
34. public void onCreate()
35. {
36. Log.d("MyService", "onCreate");
37. super.onCreate();
38. }
39. @Override
40. public void onDestroy()
41. {
42. Log.d("MyService", "onDestroy");
43. super.onDestroy();
44. }
45. @Override
46. public void onStart(Intent intent, int startId)
47. {
48. Log.d("MyService", "onStart");
49. super.onStart(intent, startId);
50. }
51. public class MyBinder extends Binder
52. {
53. MyService getService()
54. {
55. return MyService.this;
56. }
57. }
58. }
現在定義一個MyService變量和一個ServiceConnection變量,代碼如下:
1. private MyService myService;
2. private ServiceConnection serviceConnection = new ServiceConnection()
3. {
4. // 連接服務失敗後,該方法被調用
5. @Override
6. public void onServiceDisconnected(ComponentName name)
7. {
8. myService = null;
9.
Toast.makeText(Main.this, "Service
Failed.", Toast.LENGTH_LONG).show();
10. }
11. // 成功連接服務後,該方法被調用。在該方法中可以獲得MyService對象
12. @Override
13. public void onServiceConnected(ComponentName name, IBinder service)
14. {
15. // 獲得MyService對象
16. myService = ((MyService.MyBinder) service).getService();
17.
Toast.makeText(Main.this, "Service
Connected.", Toast.LENGTH_LONG).show();
18. }
19. };
最後使用bindService方法來綁定Activity和Service,代碼如下:
1. bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE);
如果想解除綁定,可以使用下面的代碼:
1. unbindService(serviceConnection);
在MyService類中定義了一個MyBinder類,該類實際上是爲了獲得MyService的對象實例的。在ServiceConnection接口的onServiceConnected方法中的第2個參數是一個IBinder類型的變量,將該參數轉換成MyService.MyBinder對象,並使用MyBinder類中的getService方法獲得MyService對象。在獲得MyService對象後,就可以在Activity中隨意操作MyService了。
運行本節的例子後,單擊【BindService】按鈕,如果綁定成功,會顯示如圖8.3所示的信息提示框。關閉應用程序後,會看到在LogCat視圖中輸出了onUnbind和onDestroy信息,表明在關閉Activity後,服務先被解除綁定,最後被銷燬。如果先啓動(調用startService方法)一個服務,然後再綁定(調用bindService方法)服務,會怎麼樣呢?在這種情況下,雖然服務仍然會成功綁定到Activity上,但在Activity關閉後,服務雖然會被解除綁定,但並不會被銷燬,也就是說,MyService類的onDestroy方法不會被調用。
8.1.3 在BroadcastReceiver中啓動Service
本節的例子代碼所在的工程目錄是src\ch08\ch08_startupservice
在8.1.1節和8.1.2節都是先啓動了一個Activity,然後在Activity中啓動服務。如果是這樣,在啓動服務時必須要先啓動一個Activity。在很多時候這樣做有些多餘,閱讀完第7章的內容,會發現實例43可以利用Broadcast Receiver在Android系統啓動時運行一個Activity。也許我們會從中得到一些啓發,既然可以在Broadcast Receiver中啓動Activity,爲什麼不能啓動Service呢?說做就做,現在讓我們來驗證一下這個想法。
先編寫一個服務類,這個服務類沒什麼特別的,仍然使用前面兩節編寫的MyService類即可。在AndroidManifest.xml文件中配置MyService類的代碼也相同。
下面來完成最關鍵的一步,就是建立一個BroadcastReceiver,代碼如下:
1. package net.blogjava.mobile.startupservice;
2.
3. import android.content.BroadcastReceiver;
4. import android.content.Context;
5. import android.content.Intent;
6.
7. public class StartupReceiver extends BroadcastReceiver
8. {
9. @Override
10. public void onReceive(Context context, Intent intent)
11. {
12. // 啓動一個Service
13. Intent serviceIntent = new Intent(context, MyService.class);
14. context.startService(serviceIntent);
15. Intent activityIntent = new Intent(context, MessageActivity.class);
16. // 要想在Service中啓動Activity,必須設置如下標誌
17. activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
18. context.startActivity(activityIntent);
19. }
20. }
在StartupReceiver類的onReceive方法中完成了兩項工作:啓動服務和顯示一個Activity來提示服務啓動成功。其中MessageActivity是一個普通的Activity類,只是該類在配置時使用了"@android:style/Theme.Dialog"主題,因此,如果服務啓動成功,會顯示如圖8.4所示的信息。
如果安裝本例後,在重新啓動模擬器後並未出現如圖8.4所示的信息提示框,最大的可能是沒有在AndroidManifest.xml文件中配置BroadcastReceiver和Service,下面來看一下AndroidManifest.xml文件的完整代碼。
1. <?xml version="1.0" encoding="utf-8"?>
2. <manifest xmlns:android="http://schemas.android.com/apk/res/android"
3.
package="net.blogjava.mobile.startupservice"
android:versionCode="1"
4. android:versionName="1.0">
5.
<application android:icon="@drawable/icon"
android:label="@string/app_name">
6.
<activity android:name=".MessageActivity"
android:theme="@android:style/Theme.Dialog">
7. <intent-filter>
8.
<category android:name="android.
intent.category.LAUNCHER" />
9. </intent-filter>
10. </activity>
11. <receiver android:name="StartupReceiver">
12. <intent-filter>
13.
<action android:name="android.
intent.action.BOOT_COMPLETED" />
14.
<category android:name="android.
intent.category.LAUNCHER" />
15. </intent-filter>
16. </receiver>
17. <service android:enabled="true" android:name=".MyService" />
18. </application>
19. <uses-sdk android:minSdkVersion="3" />
20.
<uses-permission android:name="android.
permission.RECEIVE_BOOT_COMPLETED" />
21. </manifest>
現在運行本例,然後重啓一下模擬器,看看LogCat視圖中是否輸出了相應的日誌信息。
8.2 系統服務
在Android系統中有很多內置的軟件,例如,當手機接到來電時,會顯示對方的電話號。也可以根據周圍的環境將手機設置成震動或靜音。如果想把這些功能加到自己的軟件中應該怎麼辦呢?答案就是"系統服務"。在Android系統中提供了很多這種服務,通過這些服務,就可以像Android系統的內置軟件一樣隨心所欲地控制Android系統了。本節將介紹幾種常用的系統服務來幫助讀者理解和使用這些技術。
8.2.1 獲得系統服務
系統服務實際上可以看作是一個對象,通過Activity類的getSystemService方法可以獲得指定的對象(系統服務)。getSystemService方法只有一個String類型的參數,表示系統服務的ID,這個ID在整個Android系統中是唯一的。例如,audio表示音頻服務,window表示窗口服務,notification表示通知服務。
爲了便於記憶和管理,Android SDK在android.content.Context類中定義了這些ID,例如,下面的代碼是一些ID的定義。
1.
public static final String AUDIO_SERVICE = "audio";
// 定義音頻服務的ID
2.
public static final String WINDOW_SERVICE = "window";
// 定義窗口服務的ID
3.
public static final String NOTIFICATION_SERVICE =
"notification"; // 定義通知服務的ID
下面的代碼獲得了剪貼板服務(android.text.ClipboardManager對象)。
1. // 獲得ClipboardManager對象
2. android.text.ClipboardManager clipboardManager=
3.
(android.text.ClipboardManager)
getSystemService(Context.CLIPBOARD_SERVICE);
4. clipboardManager.setText("設置剪貼版中的內容");
在調用ClipboardManager.setText方法設置文本後,在Android系統中所有的文本輸入框都可以從這個剪貼板對象中獲得這段文本,讀者不妨自己試一試!
窗口服務(WindowManager對象)是最常用的系統服務之一,通過這個服務,可以獲得很多與窗口相關的信息,例如,窗口的長度和寬度,如下面的代碼所示:
1. // 獲得WindowManager對象
2. android.view.WindowManager windowManager = (android.view.WindowManager)
3.
4.
5.
6. getSystemService(Context.WINDOW_SERVICE);
7. // 在窗口的標題欄輸出當前窗口的寬度和高度,例如,320*480
8. setTitle(String.valueOf(windowManager.getDefaultDisplay().getWidth()) + "*"
9.
+ String.valueOf
(windowManager.getDefaultDisplay().getHeight()));
本節簡單介紹瞭如何獲得系統服務以及兩個常用的系統服務的使用方法,在接下來的實例47和實例48中將給出兩個完整的關於獲得和使用系統服務的例子以供讀者參考。
實例47:監聽手機來電
工程目錄:src\ch08\ch08_phonestate
當來電話時,手機會顯示對方的電話號,當接聽電話時,會顯示當前的通話狀態。在這期間存在兩個狀態:來電狀態和接聽狀態。如果在應用程序中要監聽這兩個狀態,並進行一些其他處理,就需要使用電話服務(TelephonyManager對象)。
本例通過TelephonyManager對象監聽來電狀態和接聽狀態,並在相應的狀態顯示一個Toast提示信息框。如果是來電狀態,會顯示對方的電話號,如果是通話狀態,會顯示"正在通話..."信息。下面先來看看來電和接聽時的效果,如圖8.5和圖8.6所示。
要想獲得TelephonyManager對象,需要使用Context.TELEPHONY_SERVICE常量,代碼如下:
1.
TelephonyManager tm = (TelephonyManager)
getSystemService(Context.TELEPHONY_SERVICE);
2. MyPhoneCallListener myPhoneCallListener = new MyPhoneCallListener();
3. // 設置電話狀態監聽器
4. tm.listen(myPhoneCallListener, PhoneStateListener.LISTEN_CALL_STATE);
其中MyPhoneCallListener類是一個電話狀態監聽器,該類是PhoneStateListener的子類,代碼如下:
1. public class MyPhoneCallListener extends PhoneStateListener
2. {
3. @Override
4. public void onCallStateChanged(int state, String incomingNumber)
5. {
6. switch (state)
7. {
8. // 通話狀態
9. case TelephonyManager.CALL_STATE_OFFHOOK:
10.
Toast.makeText(Main.this, "正在通話...",
Toast.LENGTH_SHORT).show();
11. break;
12. // 來電狀態
13. case TelephonyManager.CALL_STATE_RINGING:
14.
Toast.makeText(Main.this, incomingNumber,
Toast.LENGTH_SHORT).show();
15. break;
16. }
17. super.onCallStateChanged(state, incomingNumber);
18. }
19. }
如果讀者是在模擬器上測試本例,可以使用DDMS透視圖的【Emulator Control】視圖模擬打入電話。進入【Emulator Control】視圖,會看到如圖8.7所示的界面。在【Incoming number】文本框中輸入一個電話號,選中【Voice】選項,單擊【Call】按鈕,這時模擬器就會接到來電。如果已經運行本例,在來電和接聽狀態就會顯示如圖8.5和圖8.6所示的Toast提示信息。
實例48:來電黑名單
工程目錄:src\ch08\ch08_phoneblacklist
雖然手機爲我們帶來了方便,但有時實在不想接聽某人的電話,但又不好直接掛斷電話,怎麼辦呢?很簡單,如果發現是某人來的電話,直接將手機設成靜音,這樣就可以不予理睬了。
本例與實例47類似,也就是說,仍然需要獲得TelephonyManager對象,並監聽手機的來電狀態。爲了可以將手機靜音,還需要獲得一個音頻服務(AudioManager對象)。本例需要修改實例47中的手機接聽狀態方法onCallStateChanged中的代碼,修改後的結果如下:
1. public class MyPhoneCallListener extends PhoneStateListener
2. {
3. @Override
4. public void onCallStateChanged(int state, String incomingNumber)
5. {
6. // 獲得音頻服務(AudioManager對象)
7.
AudioManager audioManager = (AudioManager)
getSystemService(Context.AUDIO_SERVICE);
8. switch (state)
9. {
10. case TelephonyManager.CALL_STATE_IDLE:
11. // 在手機空閒狀態時,將手機音頻設爲正常狀態
12.
audioManager.setRingerMode
(AudioManager.RINGER_MODE_NORMAL);
13. break;
14. case TelephonyManager.CALL_STATE_RINGING:
15.
// 在來電狀態時,判斷打進來的是否爲要靜
音的電話號,如果是,則靜音
16. if ("12345678".equals(incomingNumber))
17. {
18. // 將電話靜音
19.
audioManager.setRingerMode
(AudioManager.RINGER_MODE_SILENT);
20. }
21. break;
22. }
23. super.onCallStateChanged(state, incomingNumber);
24. }
25. }
在上面的代碼中,只設置了"12345678"爲靜音電話號,讀者可以採用實例47的方法使用"12345678"打入電話,再使用其他的電話號打入,看看模擬器是否會響鈴。
8.2.2 在模擬器上模擬重力感應
衆所周知,Android系統支持重力感應,通過這種技術,可以利用手機的移動、翻轉來實現更爲有趣的程序。但遺憾的是,在Android模擬器上是無法進行重力感應測試的。既然Android系統支持重力感應,但又在模擬器上無法測試,該怎麼辦呢?彆着急,天無絕人之路,有一些第三方的工具可以幫助我們完成這個工作,本節將介紹一種在模擬器上模擬重力感應的工具(sensorsimulator)。這個工具分爲服務端和客戶端兩部分。服務端是一個在PC上運行的Java Swing GUI程序,客戶端是一個手機程序(apk文件),在運行時需要通過客戶端程序連接到服務端程序上纔可以在模擬器上模擬重力感應。
讀者可以從下面的地址下載這個工具:
1. http://code.google.com/p/openintents/downloads/list
進入下載頁面後,下載如圖8.8所示的黑框中的zip文件。
將zip文件解壓後,運行bin目錄中的sensorsimulator.jar文件,會顯示如圖8.9所示的界面。界面的左上角是一個模擬手機位置的三維圖形,右上角可以通過滑桿來模擬手機的翻轉、移動等操作。
下面來安裝客戶端程序,先啓動Android模擬器,然後使用下面的命令安裝bin目錄中的SensorSimulatorSettings.apk文件。
1. adb install SensorSimulatorSettings.apk
如果安裝成功,會在模擬器中看到如圖8.10所示黑框中的圖標。運行這個程序,會進入如圖8.11所示的界面。在IP地址中輸入如圖8.9所示黑框中的IP(注意,每次啓動服務端程序時這個IP可能不一樣,應以每次啓動服務端程序時的IP爲準)。最後進入【Testing】頁,單擊【Connect】按鈕,如果連接成功,會顯示如圖8.12所示的效果。
下面來測試一下SensorSimulator自帶的一個demo,在這個demo中輸出了通過模擬重力感應獲得的數據。
這個demo就在samples目錄中,該目錄有一個SensorDemo子目錄,是一個Eclipse工程目錄。讀者可以直接使用Eclipse導入這個目錄,並運行程序,如果顯示的結果如圖8.13所示,說明成功使用SensorSimulator在Android模擬器上模擬了重力感應。
在實例49中將給出一個完整的例子來演示如何利用重力感應的功能來實現手機翻轉靜音的效果。
實例49:手機翻轉靜音
1. 工程目錄:src\ch08\ch08_phonereversal
與手機來電一樣,手機翻轉狀態(重力感應)也由系統服務提供。重力感應服務(android.hardware.SensorManager對象)可以通過如下代碼獲得:
1.
SensorManager sensorManager = (SensorManager)
getSystemService(Context.SENSOR_SERVICE);
本例需要在模擬器上模擬重力感應,因此,在本例中使用SensorSimulator中的一個類(SensorManagerSimulator)來獲得重力感應服務,這個類封裝了SensorManager對象,並負責與服務端進行通信,監聽重力感應事件也需要一個監聽器,該監聽器需要實現SensorListener接口,並通過該接口的onSensorChanged事件方法獲得重力感應數據。本例完整的代碼如下:
1. package net.blogjava.mobile;
2.
3. import org.openintents.sensorsimulator.hardware.SensorManagerSimulator;
4. import android.app.Activity;
5. import android.content.Context;
6. import android.hardware.SensorListener;
7. import android.hardware.SensorManager;
8. import android.media.AudioManager;
9. import android.os.Bundle;
10. import android.widget.TextView;
11.
12. public class Main extends Activity implements SensorListener
13. {
14. private TextView tvSensorState;
15. private SensorManagerSimulator sensorManager;
16. @Override
17. public void onAccuracyChanged(int sensor, int accuracy)
18. {
19. }
20. @Override
21. public void onSensorChanged(int sensor, float[] values)
22. {
23. switch (sensor)
24. {
25. case SensorManager.SENSOR_ORIENTATION:
26. // 獲得聲音服務
27. AudioManager audioManager = (AudioManager)
28.
getSystemService(Context.AUDIO_SERVICE);
29.
// 在這裏規定翻轉角度小於-120度時靜音,
values[2]表示翻轉角度,也可以設置其他角度
30. if (values[2] < -120)
31. {
32.
audioManager.setRingerMode
(AudioManager.RINGER_MODE_SILENT);
33. }
34. else
35. {
36.
audioManager.setRingerMode
(AudioManager.RINGER_MODE_NORMAL);
37. }
38.
tvSensorState.setText("角度:" +
String.valueOf(values[2]));
39. break;
40. }
41. }
42. @Override
43. protected void onResume()
44. {
45. // 註冊重力感應監聽事件
46.
sensorManager.registerListener(this,
SensorManager.SENSOR_ORIENTATION);
47. super.onResume();
48. }
49. @Override
50. protected void onStop()
51. {
52. // 取消對重力感應的監聽
53. sensorManager.unregisterListener(this);
54. super.onStop();
55. }
56. @Override
57. public void onCreate(Bundle savedInstanceState)
58. {
59. super.onCreate(savedInstanceState);
60. setContentView(R.layout.main);
61. // 通過SensorManagerSimulator對象獲得重力感應服務
62. sensorManager = (SensorManagerSimulator) SensorManagerSimulator
63. .getSystemService(this, Context.SENSOR_SERVICE);
64. // 連接到服務端程序(必須執行下面的代碼)
65. sensorManager.connectSimulator();
66. }
67. }
在上面的代碼中使用了一個SensorManagerSimulator類,該類在SensorSimulator工具包帶的sensorsimulator-lib.jar文件中,可以在lib目錄中找到這個jar文件。在使用SensorManagerSimulator類之前,必須在相應的Eclipse工程中引用這個jar文件。
現在運行本例,並通過服務端主界面右側的【Roll】滑動杆移動到指定的角度,例如,-74.0和-142.0,這時設置的角度會顯示在屏幕上,如圖8.14和圖8.15所示。
讀者可以在如圖8.14和圖8.15所示的翻轉狀態下撥入電話,會發現翻轉角度在-74.0度時來電仍然會響鈴,而翻轉角度在-142.0度時就不再響鈴了。
由於SensorSimulator目前不支持Android SDK 1.5及以上版本,因此,只能使用Android SDK
1.1中的SensorListener接口來監聽重力感應事件。在Android SDK 1.5及以上版本並不建議繼續使用這個接口,代替它的是android.hardware.SensorEventListener接口。
8.3.1計時器:Chronometer
8.3 時間服務
在Android SDK中提供了多種時間服務。這些時間服務主要處理在一定時間間隔或未來某一時間發生的任務。Android系統中的時間服務的作用域既可以是應用程序本身,也可以是整個Android系統。本節將詳細介紹這些時間服務的使用方法,並給出大量的實例供讀者學習。
8.3.1 計時器:Chronometer
本節的例子代碼所在的工程目錄是src\ch08\ch08_chronometer
Chronometer是TextView的子類,也是一個Android組件。這個組件可以用1秒的時間間隔進行計時,並顯示出計時結果。
Chronometer類有3個重要的方法:start、stop和setBase,其中start方法表示開始計時;stop方法表示停止計時;setBase方法表示重新計時。start和stop方法沒有任何參數,setBase方法有一個參數,表示開始計時的基準時間。如果要從當前時刻重新計時,可以將該參數值設爲SystemClock.elapsedRealtime()。
還可以對Chronometer組件做進一步設置。在默認情況下,Chronometer組件只輸出MM:SS或H:MM:SS的時間格式。例如,當計時到1分20秒時,Chronometer組件會顯示01:20。如果想改變顯示的信息內容,可以使用Chronometer類的setFormat方法。該方法需要一個String變量,並使用"%s"表示計時信息。例如,使用setFormat("計時信息:%s")設置顯示信息,Chronometer組件會顯示如下計時信息:
計時信息:10:20
Chronometer組件還可以通過onChronometerTick事件方法來捕捉計時動作。該方法1秒調用一次。要想使用onChronometerTick事件方法,必須實現如下接口:
1. android.widget.Chronometer.OnChronometerTickListener
在本例中有3個按鈕,分別用來開始、停止和重置計時器,並通過onChronometerTick事件方法顯示當前時間,代碼如下:
1. package net.blogjava.mobile;
2.
3. import java.text.SimpleDateFormat;
4. import java.util.Date;
5. import android.app.Activity;
6. import android.os.Bundle;
7. import android.os.SystemClock;
8. import android.view.View;
9. import android.view.View.OnClickListener;
10. import android.widget.Button;
11. import android.widget.Chronometer;
12. import android.widget.TextView;
13. import android.widget.Chronometer.OnChronometerTickListener;
14.
15.
public class Main extends Activity implements
OnClickListener, OnChronometerTickListener
16. {
17. private Chronometer chronometer;
18. private TextView tvTime;
19. @Override
20. public void onClick(View view)
21. {
22. switch (view.getId())
23. {
24. case R.id.btnStart:
25. // 開始計時器
26. chronometer.start();
27. break;
28. case R.id.btnStop:
29. // 停止計時器
30. chronometer.stop();
31. break;
32. case R.id.btnReset
33. // 重置計時器:
34. chronometer.setBase(SystemClock.elapsedRealtime());
35. break;
36. }
37. }
38. @Override
39. public void onChronometerTick(Chronometer chronometer)
40. {
41. SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
42. // 將當前時間顯示在TextView組件中
43. tvTime.setText("當前時間:" + sdf.format(new Date()));
44. }
45. @Override
46. public void onCreate(Bundle savedInstanceState)
47. {
48. super.onCreate(savedInstanceState);
49. setContentView(R.layout.main);
50. tvTime = (TextView)findViewById(R.id.tvTime);
51. Button btnStart = (Button) findViewById(R.id.btnStart);
52. Button btnStop = (Button) findViewById(R.id.btnStop);
53. Button btnReset = (Button) findViewById(R.id.btnReset);
54. chronometer = (Chronometer) findViewById(R.id.chronometer);
55. btnStart.setOnClickListener(this);
56. btnStop.setOnClickListener(this);
57. btnReset.setOnClickListener(this);
58. // 設置計時監聽事件
59. chronometer.setOnChronometerTickListener(this);
60. // 設置計時信息的格式
61. chronometer.setFormat("計時器:%s");
62. }
63. }
運行本節的例子,並單擊【開始】按鈕,在按鈕下方會顯示計時信息,在按鈕的上方會顯示當前時間,如圖8.16所示。單擊【重置】按鈕後,按鈕下方的計時信息會從"計時器:00:00"開始顯示。
8.3.2 預約時間Handler
本節的例子代碼所在的工程目錄是src\ch08\ch08_handler
android.os.Handler是Android SDK中處理定時操作的核心類。通過Handler類,可以提交和處理一個Runnable對象。這個對象的run方法可以立刻執行,也可以在指定時間後執行(也可稱爲預約執行)。
Handler類主要可以使用如下3個方法來設置執行Runnable對象的時間:
1. // 立即執行Runnable對象
2. public final boolean post(Runnable r);
3. // 在指定的時間(uptimeMillis)執行Runnable對象
4. public final boolean postAtTime(Runnable r, long uptimeMillis);
5. // 在指定的時間間隔(delayMillis)執行Runnable對象
6. public final boolean postDelayed(Runnable r, long delayMillis);
從上面3個方法可以看出,第1個參數的類型都是Runnable,因此,在調用這3個方法之前,需要有一個實現Runnable接口的類,Runnable接口的代碼如下:
1. public interface Runnable
2. {
3. public void run(); // 線程要執行的方法
4. }
在Runnable接口中只有一個run方法,該方法爲線程執行方法。在本例中Main類實現了Runnable接口。可以使用如下代碼指定在5秒後調用run方法:
1. Handler handler = new Handler();
2. handler.postDelayed(this, 5000);
如果想在5秒內停止計時,可以使用如下代碼:
1. handler.removeCallbacks(this);
除此之外,還可以使用postAtTime方法指定未來的某一個精確時間來執行Runnable對象,代碼如下:
1. Handler handler = new Handler();
2. handler.postAtTime(new RunToast(this)
3. {
4.
}, android.os.SystemClock.uptimeMillis() + 15 *
1000); // 在15秒後執行Runnable對象
其中RunToast是一個實現Runnable接口的類,代碼如下:
1. class RunToast implements Runnable
2. {
3. private Context context;
4. public RunToast(Context context)
5. {
6. this.context = context;
7. }
8. @Override
9. public void run()
10. {
11.
Toast.makeText(context, "15秒後顯
示Toast提示信息", Toast.LENGTH_LONG).show();
12. }
13. }
postAtTime的第2個參數表示一個精確時間的毫秒數,如果從當前時間算起,需要使用android.os.SystemClock.uptimeMillis()獲得基準時間。
要注意的是,不管使用哪個方法來執行Runnable對象,都只能運行一次。如果想循環執行,必須在執行完後再次調用post、postAtTime或postDelayed方法。例如,在Main類的run方法中再次調用了postDelayed方法,代碼如下:
1. public void run()
2. {
3. tvCount.setText("Count:" + String.valueOf(++count));
4.
// 再次調用postDelayed方法,5秒後run方法仍被
調用,然後再一次調用postDelayed方法,這樣就形成了
5. // 循環調用
6. handler.postDelayed(this, 5000);
7. }
運行本例後,單擊【開始計數】按鈕,5秒後,會在按鈕上方顯示計數信息。然後單擊【15秒後顯示Toast信息框】按鈕,過15秒後,會顯示一個Toast信息框,如圖8.17所示。
8.3.4 在線程中更新GUI組件
本節的例子代碼所在的工程目錄是src\ch08\ch08_thread
除了前面介紹的時間服務可以執行定時任務外,也可以採用線程的方式在後臺執行任務。在Android系統中創建和啓動線程的方法與傳統的Java程序相同,首先要創建一個Thread對象,然後使用Thread類的start方法開始一個線程。線程在啓動後,就會執行Runnable接口的run方法。
本例中啓動了兩個線程,分別用來更新兩個進度條組件。在8.3.3節曾介紹過,在線程中更新GUI組件需要使用Handler類,當然,直接利用線程作爲後臺服務也不例外。下面先來看看本例的完整源代碼。
1. package net.blogjava.mobile;
2.
3. import android.app.Activity;
4. import android.os.Bundle;
5. import android.os.Handler;
6. import android.widget.ProgressBar;
7.
8. public class Main extends Activity
9. {
10. private ProgressBar progressBar1;
11. private ProgressBar progressBar2;
12. private Handler handler = new Handler();
13. private int count1 = 0;
14. private int count2 = 0;
15. private Runnable doUpdateProgressBar1 = new Runnable()
16. {
17. @Override
18. public void run()
19. {
20. for (count1 = 0; count1 <= progressBar1.getMax(); count1++)
21. {
22. // 使用post方法立即執行Runnable接口的run方法
23. handler.post(new Runnable()
24. {
25. @Override
26. public void run()
27. {
28. progressBar1.setProgress(count1);
29. }
30. });
31. }
32. }
33. };
34. private Runnable doUpdateProgressBar2 = new Runnable()
35. {
36. @Override
37. public void run()
38. {
39. for (count2 = 0; count2 <= progressBar2.getMax(); count2++)
40. {
41. // 使用post方法立即執行Runnable接口的run方法
42. handler.post(new Runnable()
43. {
44. @Override
45. public void run()
46. {
47. progressBar2.setProgress(count2);
48. }
49. });
50. }
51. }
52. };
53. @Override
54. public void onCreate(Bundle savedInstanceState)
55. {
56. super.onCreate(savedInstanceState);
57. setContentView(R.layout.main);
58. progressBar1 = (ProgressBar) findViewById(R.id.progressbar1);
59. progressBar2 = (ProgressBar) findViewById(R.id.progressbar2);
60. Thread thread1 = new Thread(doUpdateProgressBar1, "thread1");
61. // 啓動第1個線程
62. thread1.start();
63. Thread thread2 = new Thread(doUpdateProgressBar2, "thread2");
64. // 啓動第2個線程
65. thread2.start();
66. }
67. }
在編寫上面代碼時要注意一點,使用Handler類時既可以使用sendMessage方法發送消息來調用handleMessage方法處理任務(見8.3.3節的介紹),也可以直接使用post、postAtTime或postDelayed方法來處理任務。本例中爲了方便,直接調用了post方法立即執行run方法來更新進度條組件。
運行本例後,會看到屏幕上有兩個進度條的進度在不斷變化,如圖8.19所示。
8.3.5全局定時器AlarmManager(1)
8.3.5 全局定時器AlarmManager(1)
本節的例子代碼所在的工程目錄是src\ch08\ch08_alarm
前面介紹的時間服務的作用域都是應用程序,也就是說,將當前的應用程序關閉後,時間服務就會停止。但在很多時候,需要時間服務不依賴應用程序而存在。也就是說,雖然是應用程序啓動的服務,但即使將應用程序關閉,服務仍然可以正常運行。
爲了達到服務與應用程序獨立的目的,需要獲得AlarmManager對象。該對象需要通過如下代碼獲得:
1.
AlarmManager alarmManager = (AlarmManager)
getSystemService(Context.ALARM_SERVICE);
AlarmManager類的一個非常重要的方法是setRepeating,通過該方法,可以設置執行時間間隔和相應的動作。setRepeating方法的定義如下:
1.
public void setRepeating(int type, long triggerAtTime,
long interval, PendingIntent operation);
setRepeating方法有4個參數,這些參數的含義如下:
type:表示警報類型,一般可以取的值是AlarmManager.RTC和AlarmManager.RTC_WAKEUP。如果將type參數值設爲AlarmManager.RTC,表示是一個正常的定時器,如果將type參數值設爲AlarmManager.RTC_WAKEUP,除了有定時器的功能外,還會發出警報聲(例如,響鈴、震動)。
triggerAtTime:第1次運行時要等待的時間,也就是執行延遲時間,單位是毫秒。
interval:表示執行的時間間隔,單位是毫秒。
operation:一個PendingIntent對象,表示到時間後要執行的操作。PendingIntent與Intent類似,可以封裝Activity、BroadcastReceiver和Service。但與Intent不同的是,PendingIntent可以脫離應用程序而存在。
從setRepeating方法的4個參數可以看出,使用setRepeating方法最重要的就是創建PendingIntent對象。例如,在下面的代碼中用PendingIntent指定了一個Activity。
1. Intent intent = new Intent(this, MyActivity.class);
2.
PendingIntent pendingActivityIntent = PendingIntent.
getActivity(this, 0,intent, 0);
在創建完PendingIntent對象後,就可以使用setRepeating方法設置定時器了,代碼如下:
1.
AlarmManager alarmManager = (AlarmManager)
getSystemService(Context.ALARM_SERVICE);
2.
alarmManager.setRepeating(AlarmManager.RTC,
0, 5000, pendingActivityIntent);
執行上面的代碼,即使應用程序關閉後,每隔5秒,系統仍然會顯示MyActivity。如果要取消定時器,可以使用如下代碼:
1. alarmManager.cancel(pendingActivityIntent);
運行本節的例子,界面如圖8.20所示。單擊【GetActivity】按鈕,然後關閉當前應用程序,會發現系統5秒後會顯示MyActivity。關閉MyActivity後,在5秒後仍然會再次顯示MyActivity。
本節只介紹瞭如何用PendingIntent來指定Activity,讀者在實例50和實例51中將會看到利用BroadcastReceiver和Service執行定時任務。
實例50:定時更換壁紙
工程目錄:src\ch08\ch08_changewallpaper
使用AlarmManager可以實現很多有趣的功能。本例中將實現一個可以定時更換手機壁紙的程序。在編寫代碼之前,先來看一下如圖8.21所示的效果。單擊【定時更換壁紙】按鈕後,手機的壁紙會每隔5秒變換一次。
本例使用Service來完成更換壁紙的工作,下面先編寫一個Service類,代碼如下:
1. package net.blogjava.mobile;
2.
3. import java.io.InputStream;
4. import android.app.Service;
5. import android.content.Intent;
6. import android.os.IBinder;
7.
8. public class ChangeWallpaperService extends Service
9. {
10. private static int index = 0;
11. // 保存res\raw目錄中圖像資源的ID
12.
private int[] resIds = new int[]{ R.raw.wp1,
R.raw.wp2, R.raw.wp3, R.raw.wp4, R.raw.wp5};
13. @Override
14. public void onStart(Intent intent, int startId)
15. {
16. if(index == 5)
17. index = 0;
18. // 獲得res\raw目錄中圖像資源的InputStream對象
19.
InputStream inputStream = getResources().
openRawResource(resIds[index++]);
20. try
21. {
22. // 更換壁紙
23. setWallpaper(inputStream);
24. }
25. catch (Exception e)
26. {
27. }
28. super.onStart(intent, startId);
29. }
30. @Override
31. public void onCreate()
32. {
33. super.onCreate();
34. }
35. @Override
36. public IBinder onBind(Intent intent)
37. {
38. return null;
39. }
40. }
8.3.5全局定時器AlarmManager(2)
8.3.5 全局定時器AlarmManager(2)
在編寫ChangeWallpaperService類時應注意如下3點:
爲了通過InputStream獲得圖像資源,需要將圖像文件放在res\raw目錄中,而不是res\drawable目錄中。
本例採用了循環更換壁紙的方法。也就是說,共有5個圖像文件,系統會從第1個圖像文件開始更換,更換完第5個文件後,又從第1個文件開始更換。
更換壁紙需要使用Context.setWallpaper方法,該方法需要一個描述圖像的InputStream對象。該對象通過getResources().openRawResource(...)方法獲得。
在AndroidManifest.xml文件中配置ChangeWallpaperService類,代碼如下:
1. <service android:name=".ChangeWallpaperService" />
最後來看一下本例的主程序(Main類),代碼如下:
1. package net.blogjava.mobile;
2.
3. import android.app.Activity;
4. import android.app.AlarmManager;
5. import android.app.PendingIntent;
6. import android.content.Context;
7. import android.content.Intent;
8. import android.os.Bundle;
9. import android.view.View;
10. import android.view.View.OnClickListener;
11. import android.widget.Button;
12.
13. public class Main extends Activity implements OnClickListener
14. {
15. private Button btnStart;
16. private Button btnStop;
17. @Override
18. public void onClick(View view)
19. {
20.
AlarmManager alarmManager = (AlarmManager)
getSystemService(Context.ALARM_SERVICE);
21. // 指定ChangeWallpaperService的PendingIntent對象
22. PendingIntent pendingIntent = PendingIntent.getService(this, 0,
23. new Intent(this, ChangeWallpaperService.class), 0);
24. switch (view.getId())
25. {
26. case R.id.btnStart:
27. // 開始每5秒更換一次壁紙
28.
alarmManager.setRepeating(AlarmManager.RTC,
0, 5000, pendingIntent);
29. btnStart.setEnabled(false);
30. btnStop.setEnabled(true);
31. break;
32. case R.id.btnStop:
33. // 停止更換一次壁紙
34. alarmManager.cancel(pendingIntent);
35. btnStart.setEnabled(true);
36. btnStop.setEnabled(false);
37. break;
38. }
39. }
40. @Override
41. public void onCreate(Bundle savedInstanceState)
42. {
43. super.onCreate(savedInstanceState);
44. setContentView(R.layout.main);
45. btnStart = (Button) findViewById(R.id.btnStart);
46. btnStop = (Button) findViewById(R.id.btnStop);
47. btnStop.setEnabled(false);
48. btnStart.setOnClickListener(this);
49. btnStop.setOnClickListener(this);
50. }
51. }
在編寫上面代碼時應注意如下3點:
在創建PendingIntent對象時指定了ChangeWallpaperService.class,這說明這個PendingIntent對象與ChangeWallpaperService綁定。AlarmManager在執行任務時會執行ChangeWallpaperService類中的onStart方法。
不要將任務代碼寫在onCreate方法中,因爲onCreate方法只會執行一次,一旦服務被創建,該方法就不會被執行了,而onStart方法在每次訪問服務時都會被調用。
獲得指定Service的PendingIntent對象需要使用getService方法。在8.3.5節介紹過獲得指定Activity的PendingIntent對象應使用getActivity方法。在實例51中將介紹使用getBroadcast方法獲得指定BroadcastReceiver的PendingIntent對象。
實例51:多次定時提醒
工程目錄:src\ch08\ch08_multialarm
在很多軟件中都支持定時提醒功能,也就是說,事先設置未來的某個時間,當到這個時間後,系統會發出聲音或進行其他的工作。本例中將實現這個功能。本例不僅可以設置定時提醒功能,而且支持設置多個時間點。運行本例後,單擊【添加提醒時間】按鈕,會彈出設置時間點的對話框,如圖8.22所示。當設置完一系列的時間點後(如圖8.23所示),如果到了某個時間點,系統就會播放一個聲音文件以提醒用戶。
下面先介紹一下定時提醒的原理。在添加時間點後,需要將所添加的時間點保存在文件或數據庫中。本例使用SharedPreferences來保存時間點,key和value都是時間點。然後使用AlarmManager每隔1分鐘掃描一次,在掃描過程中從文件獲得當前時間(時:分)的value。如果成功獲得value,則說明當前時間爲時間點,需要播放聲音文件,否則繼續掃描。
8.3.5 全局定時器AlarmManager(2)
在編寫ChangeWallpaperService類時應注意如下3點:
爲了通過InputStream獲得圖像資源,需要將圖像文件放在res\raw目錄中,而不是res\drawable目錄中。
本例採用了循環更換壁紙的方法。也就是說,共有5個圖像文件,系統會從第1個圖像文件開始更換,更換完第5個文件後,又從第1個文件開始更換。
更換壁紙需要使用Context.setWallpaper方法,該方法需要一個描述圖像的InputStream對象。該對象通過getResources().openRawResource(...)方法獲得。
在AndroidManifest.xml文件中配置ChangeWallpaperService類,代碼如下:
1. <service android:name=".ChangeWallpaperService" />
最後來看一下本例的主程序(Main類),代碼如下:
1. package net.blogjava.mobile;
2.
3. import android.app.Activity;
4. import android.app.AlarmManager;
5. import android.app.PendingIntent;
6. import android.content.Context;
7. import android.content.Intent;
8. import android.os.Bundle;
9. import android.view.View;
10. import android.view.View.OnClickListener;
11. import android.widget.Button;
12.
13. public class Main extends Activity implements OnClickListener
14. {
15. private Button btnStart;
16. private Button btnStop;
17. @Override
18. public void onClick(View view)
19. {
20.
AlarmManager alarmManager = (AlarmManager)
getSystemService(Context.ALARM_SERVICE);
21. // 指定ChangeWallpaperService的PendingIntent對象
22. PendingIntent pendingIntent = PendingIntent.getService(this, 0,
23. new Intent(this, ChangeWallpaperService.class), 0);
24. switch (view.getId())
25. {
26. case R.id.btnStart:
27. // 開始每5秒更換一次壁紙
28.
alarmManager.setRepeating(AlarmManager.RTC,
0, 5000, pendingIntent);
29. btnStart.setEnabled(false);
30. btnStop.setEnabled(true);
31. break;
32. case R.id.btnStop:
33. // 停止更換一次壁紙
34. alarmManager.cancel(pendingIntent);
35. btnStart.setEnabled(true);
36. btnStop.setEnabled(false);
37. break;
38. }
39. }
40. @Override
41. public void onCreate(Bundle savedInstanceState)
42. {
43. super.onCreate(savedInstanceState);
44. setContentView(R.layout.main);
45. btnStart = (Button) findViewById(R.id.btnStart);
46. btnStop = (Button) findViewById(R.id.btnStop);
47. btnStop.setEnabled(false);
48. btnStart.setOnClickListener(this);
49. btnStop.setOnClickListener(this);
50. }
51. }
在編寫上面代碼時應注意如下3點:
在創建PendingIntent對象時指定了ChangeWallpaperService.class,這說明這個PendingIntent對象與ChangeWallpaperService綁定。AlarmManager在執行任務時會執行ChangeWallpaperService類中的onStart方法。
不要將任務代碼寫在onCreate方法中,因爲onCreate方法只會執行一次,一旦服務被創建,該方法就不會被執行了,而onStart方法在每次訪問服務時都會被調用。
獲得指定Service的PendingIntent對象需要使用getService方法。在8.3.5節介紹過獲得指定Activity的PendingIntent對象應使用getActivity方法。在實例51中將介紹使用getBroadcast方法獲得指定BroadcastReceiver的PendingIntent對象。
實例51:多次定時提醒
工程目錄:src\ch08\ch08_multialarm
在很多軟件中都支持定時提醒功能,也就是說,事先設置未來的某個時間,當到這個時間後,系統會發出聲音或進行其他的工作。本例中將實現這個功能。本例不僅可以設置定時提醒功能,而且支持設置多個時間點。運行本例後,單擊【添加提醒時間】按鈕,會彈出設置時間點的對話框,如圖8.22所示。當設置完一系列的時間點後(如圖8.23所示),如果到了某個時間點,系統就會播放一個聲音文件以提醒用戶。
下面先介紹一下定時提醒的原理。在添加時間點後,需要將所添加的時間點保存在文件或數據庫中。本例使用SharedPreferences來保存時間點,key和value都是時間點。然後使用AlarmManager每隔1分鐘掃描一次,在掃描過程中從文件獲得當前時間(時:分)的value。如果成功獲得value,則說明當前時間爲時間點,需要播放聲音文件,否則繼續掃描。
8.3.5 全局定時器AlarmManager(3)
本例使用BroadcastReceiver來處理定時提醒任務。BroadcastReceiver類的代碼如下:
1. package net.blogjava.mobile;
2.
3. import java.util.Calendar;
4. import android.app.Activity;
5. import android.content.BroadcastReceiver;
6. import android.content.Context;
7. import android.content.Intent;
8. import android.content.SharedPreferences;
9. import android.media.MediaPlayer;
10.
11. public class AlarmReceiver extends BroadcastReceiver
12. {
13. @Override
14. public void onReceive(Context context, Intent intent)
15. {
16.
SharedPreferences sharedPreferences =
context.getSharedPreferences(
17. "alarm_record", Activity.MODE_PRIVATE);
18.
String hour = String.valueOf(Calendar.
getInstance().get(Calendar.HOUR_OF_DAY));
19.
String minute = String.valueOf(Calendar.
getInstance().get(Calendar.MINUTE));
20. // 從XML文件中獲得描述當前時間點的value
21.
String time = sharedPreferences.
getString(hour + ":" + minute, null);
22. if (time != null)
23. {
24. // 播放聲音
25.
MediaPlayer mediaPlayer =
MediaPlayer.create(context, R.raw.ring);
26. mediaPlayer.start();
27. }
28. }
29. }
配置AlarmReceiver類的代碼如下:
1. <receiver android:name=".AlarmReceiver" android:enabled="true" />
在主程序中每添加一個時間點,就會在XML文件中保存所添加的時間點,代碼如下:
1. package net.blogjava.mobile;
2.
3. import android.app.Activity;
4. import android.app.AlarmManager;
5. import android.app.AlertDialog;
6. import android.app.PendingIntent;
7. import android.content.Context;
8. import android.content.DialogInterface;
9. import android.content.Intent;
10. import android.content.SharedPreferences;
11. import android.os.Bundle;
12. import android.view.View;
13. import android.view.View.OnClickListener;
14. import android.widget.Button;
15. import android.widget.TextView;
16. import android.widget.TimePicker;
17.
18. public class Main extends Activity implements OnClickListener
19. {
20. private TextView tvAlarmRecord;
21. private SharedPreferences sharedPreferences;
22. @Override
23. public void onClick(View v)
24. {
25. View view = getLayoutInflater().inflate(R.layout.alarm, null);
26.
final TimePicker timePicker = (TimePicker)
view.findViewById(R.id.timepicker);
27. timePicker.setIs24HourView(true);
28. // 顯示設置時間點的對話框
29. new AlertDialog.Builder(this).setTitle("設置提醒時間").setView(view)
30.
.setPositiveButton("確定", new
DialogInterface.OnClickListener()
31. {
32. @Override
33. public void onClick(DialogInterface dialog, int which)
34. {
35. String timeStr = String.valueOf(timePicker
36. .getCurrentHour()) + ":"
37.
+ String.valueOf
(timePicker.getCurrentMinute());
38. // 將時間點添加到TextView組件中
39.
tvAlarmRecord.setText
(tvAlarmRecord.getText().toString() + "\n" + timeStr);
40. // 保存時間點
41.
sharedPreferences.edit().
putString(timeStr, timeStr).commit();
42. }
43. }).setNegativeButton("取消", null).show();
44. }
45. @Override
46. public void onCreate(Bundle savedInstanceState)
47. {
48. super.onCreate(savedInstanceState);
49. setContentView(R.layout.main);
50. Button btnAddAlarm = (Button) findViewById(R.id.btnAddAlarm);
51. tvAlarmRecord = (TextView) findViewById(R.id.tvAlarmRecord);
52. btnAddAlarm.setOnClickListener(this);
53. sharedPreferences = getSharedPreferences("alarm_record",
54. Activity.MODE_PRIVATE);
55.
AlarmManager alarmManager = (AlarmManager)
getSystemService(Context.ALARM_SERVICE);
56. Intent intent = new Intent(this, AlarmReceiver.class);
57. // 創建封裝BroadcastReceiver的pendingIntent對象
58.
PendingIntent pendingIntent = PendingIntent.
getBroadcast(this, 0,intent, 0);
59. // 開始定時器,每1分鐘執行一次
60.
alarmManager.setRepeating(AlarmManager.RTC,
0, 60 * 1000, pendingIntent);
61. }
62. }
在使用本例添加若干個時間點後,會在alarm_record.xml文件中看到類似下面的內容:
1. <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
2. <map>
3. <string name="18:52">18:52</string>
4. <string name="20:16">20:16</string>
5. <string name="19:11">19:11</string>
6. <string name="19:58">19:58</string>
7. <string name="22:51">22:51</string>
8. <string name="22:10">22:10</string>
9. <string name="22:11">22:11</string>
10. <string name="20:10">20:10</string>
11. </map>
上面每個<string>元素都是一個時間點,定時器將每隔1分鐘查一次alarm_record.xml文件。
8.4.1什麼是AIDL服務
8.4 跨進程訪問(AIDL服務)
Android系統中的進程之間不能共享內存,因此,需要提供一些機制在不同進程之間進行數據通信。第7章介紹的Activity和Broadcast都可以跨進程通信,除此之外,還可以使用Content Provider(見6.6節的介紹)進行跨進程通信。現在我們已經瞭解了4個Android應用程序組件中的3個(Activity、Broadcast和Content Provider)都可以進行跨進程訪問,另外一個Android應用程序組件Service同樣可以。這就是本節要介紹的AIDL服務。
8.4.1 什麼是AIDL服務
本章前面的部分介紹了開發人員如何定製自己的服務,但這些服務並不能被其他的應用程序訪問。爲了使其他的應用程序也可以訪問本應用程序提供的服務,Android系統採用了遠程過程調用(Remote Procedure Call,RPC)方式來實現。與很多其他的基於RPC的解決方案一樣,Android使用一種接口定義語言(Interface Definition Language,IDL)來公開服務的接口。因此,可以將這種可以跨進程訪問的服務稱爲AIDL(Android Interface Definition Language)服務。
8.4.2建立AIDL服務的步驟(1)
8.4.2 建立AIDL服務的步驟(1)
建立AIDL服務要比建立普通的服務複雜一些,具體步驟如下:
(1)在Eclipse Android工程的Java包目錄中建立一個擴展名爲aidl的文件。該文件的語法類似於Java代碼,但會稍有不同。詳細介紹見實例52的內容。
(2)如果aidl文件的內容是正確的,ADT會自動生成一個Java接口文件(*.java)。
(3)建立一個服務類(Service的子類)。
(4)實現由aidl文件生成的Java接口。
(5)在AndroidManifest.xml文件中配置AIDL服務,尤其要注意的是,<action>標籤中android:name的屬性值就是客戶端要引用該服務的ID,也就是Intent類的參數值。這一點將在實例52和實例53中看到。
實例52:建立AIDL服務
AIDL服務工程目錄:src\ch08\ch08_aidl
客戶端程序工程目錄:src\ch08\ch08_aidlclient
本例中將建立一個簡單的AIDL服務。這個AIDL服務只有一個getValue方法,該方法返回一個String類型的值。在安裝完服務後,會在客戶端調用這個getValue方法,並將返回值在TextView組件中輸出。建立這個AIDL服務的步驟如下:
(1)建立一個aidl文件。在Java包目錄中建立一個IMyService.aidl文件。IMyService.aidl文件的位置如圖8.24所示。
IMyService.aidl文件的內容如下:
1. package net.blogjava.mobile.aidl;
2. interface IMyService
3. {
4. String getValue();
5. }
IMyService.aidl文件的內容與Java代碼非常相似,但要注意,不能加修飾符(例如,public、private)、AIDL服務不支持的數據類型(例如,InputStream、OutputStream)等內容。
(2)如果IMyService.aidl文件中的內容輸入正確,ADT會自動生成一個IMyService.java文件。讀者一般並不需要關心這個文件的具體內容,也不需要維護這個文件。關於該文件的具體內容,讀者可以查看本節提供的源代碼。
(3)編寫一個MyService類。MyService是Service的子類,在MyService類中定義了一個內嵌類(MyServiceImpl),該類是IMyService.Stub的子類。MyService類的代碼如下:
1. package net.blogjava.mobile.aidl;
2.
3. import android.app.Service;
4. import android.content.Intent;
5. import android.os.IBinder;
6. import android.os.RemoteException;
7.
8. public class MyService extends Service
9. {
10. public class MyServiceImpl extends IMyService.Stub
11. {
12. @Override
13. public String getValue() throws RemoteException
14. {
15. return "Android/OPhone開發講義";
16. }
17. }
18. @Override
19. public IBinder onBind(Intent intent)
20. {
21. return new MyServiceImpl();
22. }
23. }
在編寫上面代碼時要注意如下兩點:
IMyService.Stub是根據IMyService.aidl文件自動生成的,一般並不需要管這個類的內容,只需要編寫一個繼承於IMyService.Stub類的子類(MyServiceImpl類)即可。
onBind方法必須返回MyServiceImpl類的對象實例,否則客戶端無法獲得服務對象。
(4)在AndroidManifest.xml文件中配置MyService類,代碼如下:
1. <service android:name=".MyService" >
2. <intent-filter>
3. <action android:name="net.blogjava.mobile.aidl.IMyService" />
4. </intent-filter>
5. </service>
其中"net.blogjava.mobile.aidl.IMyService"是客戶端用於訪問AIDL服務的ID。
下面來編寫客戶端的調用代碼。首先新建一個Eclipse Android工程(ch08_aidlclient),並將自動生成的IMyService.java文件連同包目錄一起復制到ch08_aidlclient工程的src目錄中,如圖8.25所示。
調用AIDL服務首先要綁定服務,然後才能獲得服務對象,代碼如下:
1. package net.blogjava.mobile;
2.
3. import net.blogjava.mobile.aidl.IMyService;
4. import android.app.Activity;
5. import android.content.ComponentName;
6. import android.content.Context;
7. import android.content.Intent;
8. import android.content.ServiceConnection;
9. import android.os.Bundle;
10. import android.os.IBinder;
11. import android.view.View;
12. import android.view.View.OnClickListener;
13. import android.widget.Button;
14. import android.widget.TextView;
15.
16. public class Main extends Activity implements OnClickListener
17. {
18. private IMyService myService = null;
19. private Button btnInvokeAIDLService;
20. private Button btnBindAIDLService;
21. private TextView textView;
22.
private ServiceConnection serviceConnection =
new ServiceConnection()
23. {
24. @Override
25.
public void onServiceConnected(ComponentName
name, IBinder service)
26. {
27. // 獲得服務對象
28. myService = IMyService.Stub.asInterface(service);
29. btnInvokeAIDLService.setEnabled(true);
30. }
31. @Override
32. public void onServiceDisconnected(ComponentName name)
33. {
34. }
35. };
36. @Override
37. public void onClick(View view)
38. {
39. switch (view.getId())
40. {
41. case R.id.btnBindAIDLService:
42. // 綁定AIDL服務
43.
bindService(new Intent("net.blogjava.
mobile.aidl.IMyService"),
44. serviceConnection, Context.BIND_AUTO_CREATE);
45. break;
46. case R.id.btnInvokeAIDLService:
47. try
48. {
49.
textView.setText(myService.
getValue()); // 調用服務端的getValue方法
50. }
51. catch (Exception e)
52. {
53. }
54. break;
55. }
56. }
57. @Override
58. public void onCreate(Bundle savedInstanceState)
59. {
60. super.onCreate(savedInstanceState);
61. setContentView(R.layout.main);
62.
btnInvokeAIDLService = (Button) findViewById
(R.id.btnInvokeAIDLService);
63.
btnBindAIDLService = (Button) findViewById
(R.id.btnBindAIDLService);
64. btnInvokeAIDLService.setEnabled(false);
65. textView = (TextView) findViewById(R.id.textview);
66. btnInvokeAIDLService.setOnClickListener(this);
67. btnBindAIDLService.setOnClickListener(this);
68. }
69. }
8.4.2建立AIDL服務的步驟(2)
8.4.2 建立AIDL服務的步驟(2)
在編寫上面代碼時應注意如下兩點:
使用bindService方法來綁定AIDL服務。其中需要使用Intent對象指定AIDL服務的ID,也就是<action>標籤中android:name屬性的值。
在綁定時需要一個ServiceConnection對象。創建ServiceConnection對象的過程中如果綁定成功,系統會調用onServiceConnected方法,通過該方法的service參數值可獲得AIDL服務對象。
首先運行AIDL服務程序,然後運行客戶端程序,單擊【綁定AIDL服務】按鈕,如果綁定成功,【調用AIDL服務】按鈕會變爲可選狀態,單擊這個按鈕,會輸出getValue方法的返回值,如圖8.26所示。
實例53:傳遞複雜數據的AIDL服務
AIDL服務工程目錄:src\ch08\ch08_complextypeaidl
客戶端程序工程目錄:src\ch08\ch08_complextypeaidlclient
AIDL服務只支持有限的數據類型,因此,如果用AIDL服務傳遞一些複雜的數據就需要做更一步處理。AIDL服務支持的數據類型如下:
Java的簡單類型(int、char、boolean等)。不需要導入(import)。
String和CharSequence。不需要導入(import)。
List和Map。但要注意,List和Map對象的元素類型必須是AIDL服務支持的數據類型。不需要導入(import)。
AIDL自動生成的接口。需要導入(import)。
實現android.os.Parcelable接口的類。需要導入(import)。
其中後兩種數據類型需要使用import進行導入,將在本章的後面詳細介紹。
傳遞不需要import的數據類型的值的方式相同。傳遞一個需要import的數據類型的值(例如,實現android.os.Parcelable接口的類)的步驟略顯複雜。除了要建立一個實現android.os.Parcelable接口的類外,還需要爲這個類單獨建立一個aidl文件,並使用parcelable關鍵字進行定義。具體的實現步驟如下:
(1)建立一個IMyService.aidl文件,並輸入如下代碼:
1. package net.blogjava.mobile.complex.type.aidl;
2. import net.blogjava.mobile.complex.type.aidl.Product;
3. interface IMyService
4. {
5. Map getMap(in String country, in Product product);
6. Product getProduct();
7. }
在編寫上面代碼時要注意如下兩點:
Product是一個實現android.os.Parcelable接口的類,需要使用import導入這個類。
如果方法的類型是非簡單類型,例如,String、List或自定義的類,需要使用in、out或inout修飾。其中in表示這個值被客戶端設置;out表示這個值被服務端設置;inout表示這個值既被客戶端設置,又被服務端設置。
(2)編寫Product類。該類是用於傳遞的數據類型,代碼如下:
1. package net.blogjava.mobile.complex.type.aidl;
2.
3. import android.os.Parcel;
4. import android.os.Parcelable;
5.
6. public class Product implements Parcelable
7. {
8. private int id;
9. private String name;
10. private float price;
11.
public static final Parcelable.Creator<Product>
CREATOR = new Parcelable.Creator<Product>()
12. {
13. public Product createFromParcel(Parcel in)
14. {
15. return new Product(in);
16. }
17.
18. public Product[] newArray(int size)
19. {
20. return new Product[size];
21. }
22. };
23. public Product()
24. {
25. }
26. private Product(Parcel in)
27. {
28. readFromParcel(in);
29. }
30. @Override
31. public int describeContents()
32. {
33. return 0;
34. }
35. public void readFromParcel(Parcel in)
36. {
37. id = in.readInt();
38. name = in.readString();
39. price = in.readFloat();
40. }
41. @Override
42. public void writeToParcel(Parcel dest, int flags)
43. {
44. dest.writeInt(id);
45. dest.writeString(name);
46. dest.writeFloat(price);
47. }
48. // 此處省略了屬性的getter和setter方法
49. ... ...
50. }
在編寫Product類時應注意如下3點:
Product類必須實現android.os.Parcelable接口。該接口用於序列化對象。在Android中之所以使用Pacelable接口序列化,而不是java.io.Serializable接口,是因爲Google在開發Android時發現Serializable序列化的效率並不高,因此,特意提供了一個Parcelable接口來序列化對象。
在Product類中必須有一個靜態常量,常量名必須是CREATOR,而且CREATOR常量的數據類型必須是Parcelable.Creator。
在writeToParcel方法中需要將要序列化的值寫入Parcel對象。
(3)建立一個Product.aidl文件,並輸入如下內容:
1. parcelable Product;
8.4.2建立AIDL服務的步驟(3)
8.4.2 建立AIDL服務的步驟(3)
(4)編寫一個MyService類,代碼如下:
1. package net.blogjava.mobile.complex.type.aidl;
2.
3. import java.util.HashMap;
4. import java.util.Map;
5. import android.app.Service;
6. import android.content.Intent;
7. import android.os.IBinder;
8. import android.os.RemoteException;
9. // AIDL服務類
10. public class MyService extends Service
11. {
12. public class MyServiceImpl extends IMyService.Stub
13. {
14. @Override
15. public Product getProduct() throws RemoteException
16. {
17. Product product = new Product();
18. product.setId(1234);
19. product.setName("汽車");
20. product.setPrice(31000);
21. return product;
22. }
23. @Override
24.
public Map getMap(String country, Product
product) throws RemoteException
25. {
26. Map map = new HashMap<String, String>();
27. map.put("country", country);
28. map.put("id", product.getId());
29. map.put("name", product.getName());
30. map.put("price", product.getPrice());
31. map.put("product", product);
32. return map;
33. }
34. }
35. @Override
36. public IBinder onBind(Intent intent)
37. {
38. return new MyServiceImpl();
39. }
40. }
(5)在AndroidManifest.xml文件中配置MyService類,代碼如下:
1. <service android:name=".MyService" >
2. <intent-filter>
3.
<action android:name="net.blogjava.
mobile.complex.type.aidl.IMyService" />
4. </intent-filter>
5. </service>
在客戶端調用AIDL服務的方法與實例52介紹的方法相同,首先將IMyService.java和Product.java文件複製到客戶端工程(ch08_complextypeaidlclient),然後綁定AIDL服務,並獲得AIDL服務對象,最後調用AIDL服務中的方法。完整的客戶端代碼如下:
1. package net.blogjava.mobile;
2.
3. import net.blogjava.mobile.complex.type.aidl.IMyService;
4. import android.app.Activity;
5. import android.content.ComponentName;
6. import android.content.Context;
7. import android.content.Intent;
8. import android.content.ServiceConnection;
9. import android.os.Bundle;
10. import android.os.IBinder;
11. import android.view.View;
12. import android.view.View.OnClickListener;
13. import android.widget.Button;
14. import android.widget.TextView;
15.
16. public class Main extends Activity implements OnClickListener
17. {
18. private IMyService myService = null;
19. private Button btnInvokeAIDLService;
20. private Button btnBindAIDLService;
21. private TextView textView;
22. private ServiceConnection serviceConnection = new ServiceConnection()
23. {
24. @Override
25. public void onServiceConnected(ComponentName name, IBinder service)
26. {
27. // 獲得AIDL服務對象
28. myService = IMyService.Stub.asInterface(service);
29. btnInvokeAIDLService.setEnabled(true);
30. }
31. @Override
32. public void onServiceDisconnected(ComponentName name)
33. {
34. }
35. };
36. @Override
37. public void onClick(View view)
38. {
39. switch (view.getId())
40. {
41. case R.id.btnBindAIDLService:
42. // 綁定AIDL服務
43.
bindService(new Intent("net.blogjava.
mobile.complex.type.aidl.IMyService"),
44. serviceConnection, Context.BIND_AUTO_CREATE);
45. break;
46. case R.id.btnInvokeAIDLService:
47. try
48. {
49. String s = "";
50. // 調用AIDL服務中的方法
51.
s = "Product.id = " + myService.
getProduct().getId() + "\n";
52.
s += "Product.name = " + myService.
getProduct().getName() + "\n";
53.
s += "Product.price = " + myService.
getProduct().getPrice() + "\n";
54.
s += myService.getMap("China",
myService.getProduct()).toString();
55. textView.setText(s);
56. }
57. catch (Exception e)
58. {
59. }
60. break;
61. }
62. }
63. @Override
64. public void onCreate(Bundle savedInstanceState)
65. {
66. super.onCreate(savedInstanceState);
67. setContentView(R.layout.main);
68. btnInvokeAIDLService = (Button) findViewById(R.id.btnInvokeAIDLService);
69. btnBindAIDLService = (Button) findViewById(R.id.btnBindAIDLService);
70. btnInvokeAIDLService.setEnabled(false);
71. textView = (TextView) findViewById(R.id.textview);
72. btnInvokeAIDLService.setOnClickListener(this);
73. btnBindAIDLService.setOnClickListener(this);
74. }
75. }
首先運行服務端程序,然後運行客戶端程序,單擊【綁定AIDL服務】按鈕,待成功綁定後,單擊【調用AIDL服務】按鈕,會輸出如圖8.27所示的內容。
8.5 本章小結
8.5 本章小結
本章主要介紹了Android系統中的服務(Service)技術。Service是Android中4個應用程序組件之一。在Android系統內部提供了很多的系統服務,通過這些系統服務,可以實現更爲複雜的功能,例如,監聽來電、重力感應等。Android系統還允許開發人員自定義服務。自定義的服務可以用來在後臺運行程序,也可以通過AIDL服務提供給其他的應用使用。除此之外,在Android系統中還有很多專用於時間的服務和組件,例如,Chronometer、Timer、Handler、AlarmManager等。通過這些服務,可以完成關於時間的定時、預約等操作。