第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所示的界面。
|
图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方法。
|
图8.2 服务的生命周期方法的调用情况 |
在讨论完服务的生命周期后,再来总结一下创建和开始服务的步骤。创建和开始一个服务需要如下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了。
运行本节的例子后,单击【Bind Service】按钮,如果绑定成功,会显示如图8.3所示的信息提示框。关闭应用程序后,会看到在LogCat视图中输出了onUnbind和onDestroy信息,表明在关闭Activity后,服务先被解除绑定,最后被销毁。如果先启动(调用startService方法)一个服务,然后再绑定(调用bindService方法)服务,会怎么样呢?在这种情况下,虽然服务仍然会成功绑定到Activity上,但在Activity关闭后,服务虽然会被解除绑定,但并不会被销毁,也就是说,MyService类的onDestroy方法不会被调用。
|
图8.3 绑定服务 |
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 在BroadcastReceiver中启动服务 |
如果安装本例后,在重新启动模拟器后并未出现如图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所示。
|
图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提示信息。
|
图8.7 用【Emulator Control】视图模拟拨打电话 |
实例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文件。
|
(点击查看大图)图8.8 sensorsimulator下载页面 |
将zip文件解压后,运行bin目录中的sensorsimulator.jar文件,会显示如图8.9所示的界面。界面的左上角是一个模拟手机位置的三维图形,右上角可以通过滑杆来模拟手机的翻转、移动等操作。
|
(点击查看大图)图8.9 sensorsimulator主界面 |
下面来安装客户端程序,先启动Android模拟器,然后使用下面的命令安装bin目录中的SensorSimulatorSettings.apk文件。
1. adb install SensorSimulatorSettings.apk
如果安装成功,会在模拟器中看到如图8.10所示黑框中的图标。运行这个程序,会进入如图8.11所示的界面。在IP地址中输入如图8.9所示黑框中的IP(注意,每次启动服务端程序时这个IP可能不一样,应以每次启动服务端程序时的IP为准)。最后进入【Testing】页,单击【Connect】按钮,如果连接成功,会显示如图8.12所示的效果。
|
图8.10 安装客户端设置软件 |
|
图8.11 进行客户端设置 |
下面来测试一下SensorSimulator自带的一个demo,在这个demo中输出了通过模拟重力感应获得的数据。
这个demo就在samples目录中,该目录有一个SensorDemo子目录,是一个Eclipse工程目录。读者可以直接使用Eclipse导入这个目录,并运行程序,如果显示的结果如图8.13所示,说明成功使用SensorSimulator在Android模拟器上模拟了重力感应。
|
图8.12 测试连接状态 |
|
图8.13 测试重力感应demo |
在实例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 翻转角度大于-120度 |
|
图8.15 翻转角度小于-120度 |
读者可以在如图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.16 Chronometer组件的计时效果 |
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.17 使用Handler预约时间 |
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.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秒变换一次。
|
图8.20 全局定时器(显示Activity) |
|
图8.21 定时更换壁纸 |
本例使用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所示),如果到了某个时间点,系统就会播放一个声音文件以提醒用户。
|
图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所示),如果到了某个时间点,系统就会播放一个声音文件以提醒用户。
|
图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所示。
|
图8.24 IMyService.aidl文件的位置 |
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所示。
|
图8.25 IMyService.java文件 |
调用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所示。
|
图8.26 调用AIDL服务的客户端程序 |
实例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.27 调用传递复杂数据的AIDL服务 |
8.5 本章小结
8.5 本章小结
本章主要介绍了Android系统中的服务(Service)技术。Service是Android中4个应用程序组件之一。在Android系统内部提供了很多的系统服务,通过这些系统服务,可以实现更为复杂的功能,例如,监听来电、重力感应等。Android系统还允许开发人员自定义服务。自定义的服务可以用来在后台运行程序,也可以通过AIDL服务提供给其他的应用使用。除此之外,在Android系统中还有很多专用于时间的服务和组件,例如,Chronometer、Timer、Handler、AlarmManager等。通过这些服务,可以完成关于时间的定时、预约等操作。