第10章 Service 和 BroadcastReceiver

第10章 Service 和 BroadcastReceiver

本章要点

  • Service组件的作用和意义
  • 创建、配置Service
  • 启动、停止Service
  • 绑定本地Service并与之通信
  • Service 的生命周期
  • IntentService的功能和用法
  • 开发远程AIDL Service
  • 在客户端程序中调用远程AIDL Service
  • TelephonyManager的功能和用法
  • 监听手机电话
  • SmsManager的功能和用法
  • 监听手机短信
  • AudioManager的功能和用法
  • Vibrator 的功能和用法
  • AlarmManager的功能和用法
  • BroadcastReceiver组件的作用和意义
  • 开发、配置BroadcastReceiver组件
  • 发送广播、发送有序广播
  • 使用BroadcastReceiver接收系统广播

Service 概述

Service 是 Android 四大组件中与 Activity 最相似的组件,它们都代表可执行的程序。Service 与 Activity 的区别在于:Service 一直在后台运行,它没有用户界面,所以绝不会到前台来。

一旦 Service 被启动起来之后,它就与 Activity 一样,完全具有自己的生命周期。

Activity 与 Service 的选择标准

如果某个程序组件需要在运行时向用户呈现某种界面,或者该程序需要与用户交互,就需要使用 Activity;否则就应该考虑使用 Service。

Service 开发步骤

开发者开发 Service 的步骤与开发 Activity 的步骤相似。开发 Service 组件需要:

  1. 开发一个 Service 子类。
  2. AndroidManifest.xml 文件中配置该 Service。配置时可通过 <intent-filter> 元素指定它可被哪些 Intent 启动。

Android 系统本身提供了大量的 Service 组件,开发者可通过这些系统 Service 来操作 Android 系统本身。

BroadcastReceiver 概述

本章还将向读者介绍 BroadcastReceiver 组件。BroadcastReceiver 组件就像一个全局的事件监听器,只不过它用于监听系统发出的 Broadcast。

通过使用 BroadcastReceiver,即可在不同应用程序之间通信。

10.1 Service 简介

Service 组件也是可执行的程序,它也有自己的生命周期。创建、配置 Service 与创建、配置 Activity 的过程基本相似,下面详细介绍 Android Service 的开发。

10.1.1 创建、配置 Service

就像开发 Activity 需要两个步骤:

  1. 开发 Activity 子类。
  2. AndroidManifest.xml 文件中配置 Activity。

开发 Service 也需要两个步骤:

  1. 定义一个继承 Service 的子类。
  2. AndroidManifest.xml 文件中配置该 Service。

Service 与 Activity 还有一点相似之处,它们都是从 Context 派生出来的,因此它们都可调用 Context 里定义的如 getResources()getContentResolver() 等方法。

与 Activity 相似的是,Service 中也定义了一系列生命周期方法,如下所示:

  • IBinder onBind(Intent intent): 该方法是 Service 子类必须实现的方法。该方法返回一个 IBinder 对象,应用程序可通过该对象与 Service 组件通信。
  • void onCreate(): 在该 Service 第一次被创建后将立即回调该方法。
  • void onDestroy(): 在该 Service 被关闭之前将会回调该方法。
  • void onStartCommand(Intent intent, int flags, int startId): 该方法的早期版本是 void onStart(Intent intent, int startId),每次客户端调用 startService(Intent) 方法启动该 Service 时都会回调该方法。
  • boolean onUnbind(Intent intent): 当该 Service 上绑定的所有客户端都断开连接时将会回调该方法。

下面的类定义了一个 Service 组件:

示例代码:FirstService.java

public class FirstService extends Service {
    // Service 被绑定时回调该方法
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    // Service 被创建时回调该方法
    @Override
    public void onCreate() {
        super.onCreate();
        System.out.println("Service is Created");
    }

    // Service 被启动时回调该方法
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        System.out.println("Service is Started");
        return START_STICKY;
    }

    // Service 被关闭之前回调该方法
    @Override
    public void onDestroy() {
        super.onDestroy();
        System.out.println("Service is Destroyed");
    }
}

上面的 Service 什么也没做——它只是重写了 Service 组件的 onCreate()onStartCommand()onDestroy()onBind() 等方法,重写这些方法时只是简单地输出了一个字符串。

提示:虽然这个 Service 什么都没做,但实际上它是 Service 组件的框架。如果希望 Service 组件做某些事情,那么只要在 onCreate()onStartCommand() 方法中定义相关业务代码即可。

配置 Service

在定义了上面的 Service 之后,接下来需要在 AndroidManifest.xml 文件中配置该 Service,配置 Service 使用 <service .../> 元素。与配置 Activity 相似的是,配置 Service 时也可为 <service .../> 元素配置 <intent-filter .../> 子元素,用于说明该 Service 可被哪些 Intent 启动。

在配置 <service .../> 元素时也可指定如下常用属性:

  • name: 指定该 Service 的实现类类名。
  • exported: 指定该 Service 是否能被其他 App 启动。如果在配置该 Service 时指定了 <intent-filter .../> 子元素,则该属性默认为 true
  • permission: 指定启动该 Service 所需的权限。
  • process: 指定该 Service 所处的进程。该 Service 组件默认处于该 App 所在的进程中。实际上,Android 的四大组件都可通过该属性指定进程。

AndroidManifest.xml 中的配置

<!-- 配置一个 Service 组件 -->
<service android:name=".FirstService">
</service>

上面的配置片段配置的 Service 组件没有配置 <intent-filter .../>,这意味着该 Service 不能响应任何 Intent,只能通过指定 Component 的 Intent 来启动。

从上面的配置片段不难看出,配置 Service 与配置 Activity 的差别并不大,只是配置 Service 使用 <service ...> 元素,而且无须指定 android:label 属性——因为 Service 没有界面,总是位于后台运行,为该 Service 指定标签没有太大的意义。

运行 Service

当该 Service 开发完成之后,接下来就可在程序中运行该 Service 了。在 Android 系统中运行 Service 有如下两种方式:

  1. 通过 Context 的 startService() 方法:通过该方法启动 Service,访问者与 Service 之间没有关联,即使访问者退出了,Service 也仍然运行。
  2. 通过 Context 的 bindService() 方法:使用该方法启动 Service,访问者与 Service 绑定在一起,访问者一旦退出,Service 也就终止了。

下面先示范第一种方式运行 Service。

10.1.2 启动和停止 Service

下面的程序使用 Activity 作为 Service 的访问者,该 Activity 的界面中包含两个按钮,其中一个按钮用于启动 Service,另一个按钮用于关闭 Service
Activity 的代码如下:

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 获取程序界面中的 start 和 stop 两个按钮
        Button startBn = findViewById(R.id.start);
        Button stopBn = findViewById(R.id.stop);

        // 创建启动 Service 的 Intent
        Intent intent = new Intent(this, FirstService.class);

        // 设置启动 Service 的点击事件
        startBn.setOnClickListener(view -> startService(intent)); // 启动指定 Service

        // 设置停止 Service 的点击事件
        stopBn.setOnClickListener(view -> stopService(intent)); // 停止指定 Service
    }
}

从上面的代码不难看出,启动、关闭 Service 十分简单,调用 Context 里定义的 startService()stopService() 方法即可启动、关闭 Service

注意:
从 Android 5.0 开始,Google 要求必须使用显式 Intent 启动 Service 组件。Android 要求的显式 Intent 必须要指定包名——既可通过 Context、目标 Service 类创建显式 Intent,也可通过包名、Action 属性来创建显式 Intent

运行该程序,通过程序界面先启动 Service,再关闭 Service,可以在 Android Studio 的 Logcat 面板中看到相关输出。

10.1.3 绑定本地 Service 并与之通信

当程序通过 startService()stopService() 启动、关闭 Service 时,Service 与访问者之间基本上不存在太多的关联,因此 Service 和访问者之间也无法进行通信、交换数据。

如果 Service 和访问者之间需要进行方法调用或交换数据,则应该使用 bindService()unbindService() 方法启动、关闭 ServiceContextbindService() 方法的完整签名为:

bindService(Intent service, ServiceConnection conn, int flags)

该方法的三个参数解释如下:

  1. service: 通过 Intent 指定要启动的 Service
  2. conn: 是一个 ServiceConnection 对象,该对象用于监听访问者与 Service 之间的连接情况。
    • 当访问者与 Service 之间连接成功时,回调 ServiceConnection 对象的 onServiceConnected(ComponentName name, IBinder service) 方法。
    • Service 所在的宿主进程由于异常中止或其他原因终止,导致该 Service 与访问者之间断开连接时,回调 ServiceConnection 对象的 onServiceDisconnected(ComponentName name) 方法。

注意:
当调用者主动通过 unbindService() 方法断开与 Service 的连接时,ServiceConnection 对象的 onServiceDisconnected(ComponentName name) 方法并不会被调用。

  1. flags: 指定绑定时是否自动创建 Service(如果 Service 还未创建)。该参数可指定为 0(不自动创建)或 BIND_AUTO_CREATE(自动创建)。

在绑定本地 Service 的情况下,onBind(Intent intent) 方法返回的 IBinder 对象将传递给 ServiceConnection 对象的 onServiceConnected(ComponentName name, IBinder service) 方法的 service 参数,这样访问者就可通过该 IBinder 对象与 Service 进行通信了。

提示:

IBinder 对象相当于 Service 组件的内部钩子,当其他程序组件绑定该 Service 时,Service 将会把 IBinder 对象返回给其他程序组件,其他程序组件通过该 IBinder 对象即可与 Service 组件进行实时通信。

实际上,开发时通常会采用继承 BinderIBinder 的实现类)的方式实现自己的 IBinder 对象。

10.1.4 Service的生命周期

通过前面两个示例,读者应该已经大致明白 Service 的生命周期了。随着应用程序启动 Service 方式的不同,Service 的生命周期也略有差异。

  • 如果应用程序通过 startService() 方法来启动 Service,Service 的生命周期如图 10.6 左边所示。
  • 如果应用程序通过 bindService() 方法来启动 Service,Service 的生命周期如图 10.6 右边所示。

此外,Service 生命周期还有一种特殊的情形:如果 Service 已由某个客户端通过 startService() 方法启动了,接下来其他客户端调用 bindService() 方法绑定该 Service 后,再调用 unbindService() 方法解除绑定,最后又调用 bindService() 方法再次绑定到 Service,这个过程所触发的生命周期方法如下:

  1. onCreate()
  2. onStartCommand()
  3. onBind()
  4. onUnbind() (重写该方法时返回了 true) →
  5. onRebind()
图 10.6 Service 的生命周期
  • 非绑定 Service 的生命周期
    调用 startService()
    onCreate()onStartCommand() → Service 运行中 → Service 被自己或调用者停止 → onDestroy() → Service 被关闭。

  • 被绑定 Service 的生命周期
    调用 bindService()
    onCreate()onBind() → 客户端绑定到 Service → 调用 unbindService() 取消绑定 → onUnbind()onDestroy() → Service 被关闭。

在上面的触发过程中,onCreate() 是创建该 Service 后立即调用的,只有当该 Service 被创建时才会被调用;onStartCommand() 方法则是由客户端调用 startService() 方法时触发的。

如图 10.7 所示的 Logcat 显示了上面生命周期的输出。

图 10.7 先启动、再绑定的 Service 生命周期

在图 10.7 所示的输出中,可以看到 ServiceonRebind() 方法被回调了。如果希望该方法被回调,除了需要该 Service 是由 ActivitystartService() 方法启动之外,还需要 Service 子类重写 onUnbind() 方法时返回 true

在图 10.7 的输出中,并没有发现 Service 回调 onDestroy() 方法,这是因为该 Service 并不是由 Activity 通过 bindService() 方法启动的(该 Service 事先已由 Activity 通过 startService() 方法启动了),因此当 Activity 调用 unbindService() 方法取消与该 Service 的绑定时,该 Service 并不会终止。

由此可见,当 Activity 调用 bindService() 绑定一个已启动的 Service 时,系统只是把 Service 内部的 IBinder 对象传给 Activity,并不会把该 Service 生命周期完全“绑定”到该 Activity。因而当 Activity 调用 unbindService() 方法取消与该 Service 的绑定时,也只是切断该 ActivityService 之间的关联,并不能停止该 Service 组件。

10.1.5 使用 IntentService

IntentServiceService 的子类,因此它不是普通的 Service,它比普通的 Service 增加了额外的功能。

先看 Service 本身存在的两个问题:

  1. Service 不会专门启动一个单独的进程,Service 与它所在的应用位于同一个进程中。
  2. Service 不是一条新的线程,因此不应该在 Service 中直接处理耗时的任务。

提示:
如果开发者需要在 Service 中处理耗时任务,建议在 Service 中另外启动一条新线程来处理该耗时任务。比如在前面的 BindService 示例中,程序在 BindServiceonCreate() 方法中启动了一条新线程来处理耗时任务。

可能有人会疑惑:为什么不直接在其他程序组件中启动子线程来处理耗时任务?这种方式不太可靠,因为:

  • Activity 可能会被用户退出,而 BroadcastReceiver 的生命周期本身就很短。
  • 在子线程还未结束时,Activity 已经被用户退出,或者 BroadcastReceiver 已经结束。此时,它们所在的进程就变成了空进程,系统在需要内存时可能会优先终止该进程。如果宿主进程被终止,进程内的所有子线程也会被中止,导致子线程无法执行完成。

IntentService 可以弥补 Service 的这些不足:IntentService 使用队列来管理请求的 Intent。每当客户端代码通过 Intent 请求启动 IntentService 时,IntentService 会将该 Intent 加入队列中,并开启一条新的 worker 线程来处理 Intent。对于异步的 startService() 请求,IntentService 按顺序依次处理队列中的 Intent,保证同一时刻只处理一个 Intent。由于 IntentService 使用新的 worker 线程处理 Intent 请求,因此不会阻塞主线程。

总结一下,IntentService 具有如下特征:

  • IntentService 会创建单独的 worker 线程来处理所有的 Intent 请求。
  • IntentService 创建的 worker 线程会处理 onHandleIntent() 方法中的代码,因此开发者无需处理多线程问题。
  • 当所有请求处理完成后,IntentService 会自动停止,因此无需调用 stopSelf() 方法来停止该 Service
  • IntentServiceonBind() 方法提供了默认实现,返回 null
  • IntentServiceonStartCommand() 方法提供了默认实现,该实现将请求的 Intent 添加到队列中。

从上面的介绍可以看出,扩展 IntentService 实现 Service 时,无需重写 onBind()onStartCommand() 方法,只需重写 onHandleIntent() 方法。

示例:普通 Service 与 IntentService

下面的示例程序界面中包含两个按钮,分别用于启动普通 ServiceIntentService,两个 Service 都需要处理耗时任务。

MainActivity.java
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void startService(View source) {
        // 创建启动普通 Service 的 Intent
        Intent intent = new Intent(this, MyService.class);
        // 启动普通 Service
        startService(intent);
    }

    public void startIntentService(View source) {
        // 创建启动 IntentService 的 Intent
        Intent intent = new Intent(this, MyIntentService.class);
        // 启动 IntentService
        startService(intent);
    }
}
MyService.java
public class MyService extends Service {
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // 模拟耗时任务
        long endTime = System.currentTimeMillis() + 20 * 1000;
        while (System.currentTimeMillis() < endTime) {
            System.out.println("onStart");
            synchronized (this) {
                try {
                    wait(endTime - System.currentTimeMillis());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println("---耗时任务执行完成---");
        return START_STICKY;
    }
}

上面 MyServiceonStartCommand() 方法中使用线程暂停的方式模拟了耗时任务,该任务需要执行 20 秒。由于普通 Service 会阻塞主线程,因此启动该线程会导致程序出现 ANR(Application Not Responding)异常。

MyIntentService.java
public class MyIntentService extends IntentService {
    public MyIntentService() {
        super("MyIntentService");
    }

    @Override
    protected void onHandleIntent(@Nullable Intent intent) {
        // 模拟耗时任务
        long endTime = System.currentTimeMillis() + 20 * 1000;
        System.out.println("onStartCommand");
        while (System.currentTimeMillis() < endTime) {
            synchronized (this) {
                try {
                    wait(endTime - System.currentTimeMillis());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println("---耗时任务执行完成---");
    }
}

MyIntentService 继承了 IntentService,不需要实现 onBind()onStartCommand() 方法,只需实现 onHandleIntent() 方法。在该方法中定义了 Service 需要完成的任务。本示例的 onHandleIntent() 方法也用线程暂停的方式模拟了耗时任务,但由于 IntentService 会使用单独的线程来完成该任务,因此启动 MyIntentService 不会阻塞主线程。

运行效果
  • 如果单击界面上的“启动普通 Service”按钮,将会触发 startService() 方法,启动 MyService 去执行耗时任务,此时会导致程序的 UI 线程被阻塞,程序界面失去响应,并出现 ANR 异常,如图 10.8 所示。

  • 如果单击“启动 IntentService”按钮启动 MyIntentService,虽然它也需要执行耗时任务,但由于 IntentService 使用单独的 worker 线程,因此不会阻塞前台 UI 线程,程序界面仍可正常响应。

10.2 跨进程调用 Service (AIDL Service)

在 Android 系统中,各应用程序都运行在自己的进程中,进程之间一般无法直接进行数据交换。为了实现跨进程通信(Interprocess Communication,简称 IPC),Android 提供了 AIDL Service。AIDL Service 与传统技术 Corba、Java 中的 RMI(远程方法调用)有一定的相似之处。

10.2.1 AIDL Service 简介

Android 的远程 Service 调用与 Java 的 RMI 基本相似,都是先定义一个远程调用接口,然后为该接口提供一个实现类即可。与 RMI 不同的是,客户端访问 Service 时,Android 并不是直接返回 Service 对象给客户端的,而是通过 onBind() 方法将 IBinder 对象的代理传给客户端。因此,Android 的 AIDL 远程接口的实现类就是那个 IBinder 实现类。

10.2.2 创建 AIDL 文件

Android 使用 AIDL(Android Interface Definition Language)来定义远程接口。AIDL 的语法与 Java 接口很相似,但有几点差异:

  • AIDL 文件必须以 .aidl 结尾。
  • 除了基本数据类型、StringListMapCharSequence 之外,其他类型都需要导包,即使它们在同一个包中也需要导包。

例如,定义如下 AIDL 接口:

package org.crazyit.service;

interface ICat {
    String getColor();
    double getWeight();
}

Android Studio 会自动生成一个 ICat.java 接口,其中包含一个 Stub 内部类,它实现了 IBinderICat 接口。这个 Stub 类是远程 Service 的回调类,它实现了 IBinder 接口,因此可以作为 ServiceonBind() 方法的返回值。

10.2.3 将接口暴露给客户端

定义好 AIDL 接口后,可以定义一个 Service 实现类。该 Service 的 onBind() 方法返回的 IBinder 对象应该是 ICat.Stub 的子类实例。其他部分与开发本地 Service 类似。

public class AidlService extends Service {
    private CatBinder catBinder;
    private Timer timer = new Timer();
    private String[] colors = {"红色", "黄色", "黑色"};
    private double[] weights = {2.3, 3.1, 1.58};
    private String color;
    private double weight;

    // 实现 ICat 接口的 CatBinder 类
    class CatBinder extends ICat.Stub {
        @Override
        public String getColor() {
            return AidlService.this.color;
        }

        @Override
        public double getWeight() {
            return AidlService.this.weight;
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
        catBinder = new CatBinder();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                int rand = (int) (Math.random() * 3);
                color = colors[rand];
                weight = weights[rand];
            }
        }, 0, 800);
    }

    @Override
    public IBinder onBind(Intent intent) {
        return catBinder;
    }

    @Override
    public void onDestroy() {
        timer.cancel();
    }
}

在 AndroidManifest.xml 文件中配置该 Service:

<service android:name=".AidlService">
    <intent-filter>
        <action android:name="org.crazyit.aidl.action.AIDL_SERVICE"/>
    </intent-filter>
</service>

部署该应用后,它的 Service 可以供其他应用程序调用。

10.2.4 客户端访问 AIDL Service

客户端访问 AIDL Service 的步骤与访问本地 Service 类似,只是绑定远程 Service 时需要通过 ICat.Stub.asInterface() 方法获取 IBinder 对象的代理。

public class MainActivity extends Activity {
    private ICat catService;
    private ServiceConnection conn = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // 获取远程 Service 的 IBinder 对象代理
            catService = ICat.Stub.asInterface(service);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            catService = null;
        }
    };

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

        Button getBn = findViewById(R.id.get);
        TextView colorTv = findViewById(R.id.color);
        TextView weightTv = findViewById(R.id.weight);

        // 创建 Intent 绑定远程 Service
        Intent intent = new Intent();
        intent.setAction("org.crazyit.aidl.action.AIDL_SERVICE");
        intent.setPackage("org.crazyit.service");
        bindService(intent, conn, Service.BIND_AUTO_CREATE);

        getBn.setOnClickListener(view -> {
            try {
                // 获取远程 Service 的数据并显示
                colorTv.setText("颜色: " + catService.getColor());
                weightTv.setText("重量: " + catService.getWeight());
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(conn);
    }
}

运行该程序,单击“获取远程 SERVICE 的状态”按钮,可以看到远程 Service 返回的数据。

实例:传递复杂数据的 AIDL Service

本实例演示了如何通过 AIDL Service 传递自定义类型的数据,与之前的示例不同,本实例传输的类型是自定义的 PersonPet 对象。Person 对象作为调用远程 Service 的参数,而 Pet 作为返回值。与 Java 中的 RMI 机制类似,Android 要求传递到远程 Service 的参数和返回值必须实现 Parcelable 接口。

1. 定义 Person

首先,使用 AIDL 来定义 Person 类:

package org.crazyit.service;

parcelable Person;

定义好 AIDL 文件后,需要在 Java 中实现 Person 类并实现 Parcelable 接口:

public class Person implements Parcelable {
    private int id;
    private String name;
    private String pass;

    public Person() {}

    public Person(int id, String name, String pass) {
        this.id = id;
        this.name = name;
        this.pass = pass;
    }

    // 省略 getter 和 setter 方法

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(id);
        dest.writeString(name);
        dest.writeString(pass);
    }

    public static final Creator<Person> CREATOR = new Creator<Person>() {
        @Override
        public Person createFromParcel(Parcel in) {
            return new Person(in.readInt(), in.readString(), in.readString());
        }

        @Override
        public Person[] newArray(int size) {
            return new Person[size];
        }
    };
}

Person 类实现了 Parcelable 接口,并通过 writeToParcel() 方法将对象数据写入 Parcel 中,同时通过 CREATOR 常量来从 Parcel 恢复 Person 对象。

2. 定义 Pet

Pet 类的定义与 Person 类相似,同样需要实现 Parcelable 接口。以下是 Pet 类的简要定义:

public class Pet implements Parcelable {
    private String name;
    private double weight;

    public Pet(String name, double weight) {
        this.name = name;
        this.weight = weight;
    }

    // 省略 getter 和 setter 方法

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
        dest.writeDouble(weight);
    }

    public static final Creator<Pet> CREATOR = new Creator<Pet>() {
        @Override
        public Pet createFromParcel(Parcel in) {
            return new Pet(in.readString(), in.readDouble());
        }

        @Override
        public Pet[] newArray(int size) {
            return new Pet[size];
        }
    };
}
3. 定义 AIDL 接口

接下来使用 AIDL 定义用于传递 PersonPet 的接口:

package org.crazyit.service;

import org.crazyit.service.Person;
import org.crazyit.service.Pet;

interface IPet {
    List<Pet> getPets(in Person owner);
}

该接口定义了一个方法 getPets(),接收一个 Person 对象作为参数,并返回 List<Pet>

4. 实现 ParcelableService

在 Service 中实现该接口,代码如下:

public class ParcelableService extends Service {
    private static Map<Person, List<Pet>> pets = new HashMap<>();
    private PetBinder petBinder;

    static {
        // 初始化 pets 集合
        List<Pet> list1 = new ArrayList<>();
        list1.add(new Pet("旺财", 4.3));
        list1.add(new Pet("来福", 5.1));
        pets.put(new Person(1, "sun", "sun"), list1);

        List<Pet> list2 = new ArrayList<>();
        list2.add(new Pet("kitty", 2.3));
        list2.add(new Pet("garfield", 3.1));
        pets.put(new Person(2, "bai", "bai"), list2);
    }

    class PetBinder extends IPet.Stub {
        @Override
        public List<Pet> getPets(Person owner) {
            return pets.get(owner);
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return petBinder;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        petBinder = new PetBinder();
    }
}

ParcelableServicePersonPet 对象保存在一个 Map 中,客户端通过 getPets() 方法可以传递 Person 对象并获取对应的 Pet 列表。

AndroidManifest.xml 文件中配置该 Service:

<service android:name=".ParcelableService">
    <intent-filter>
        <action android:name="org.crazyit.aidl.action.PARCELABLE_SERVICE"/>
    </intent-filter>
</service>
5. 客户端访问 ParcelableService

客户端需要复制 IPet.aidlPerson.javaPet.java 文件。客户端绑定 Service 并通过 IPet.Stub.asInterface() 获取代理对象,然后调用 getPets() 方法获取数据:

public class MainActivity extends Activity {
    private IPet petService;
    private ServiceConnection conn = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            petService = IPet.Stub.asInterface(service);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            petService = null;
        }
    };

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

        EditText personView = findViewById(R.id.person);
        ListView showView = findViewById(R.id.show);
        Button getBn = findViewById(R.id.get);

        // 创建绑定 Service 的 Intent
        Intent intent = new Intent();
        intent.setAction("org.crazyit.aidl.action.PARCELABLE_SERVICE");
        intent.setPackage("org.crazyit.service");
        bindService(intent, conn, Service.BIND_AUTO_CREATE);

        getBn.setOnClickListener(view -> {
            String personName = personView.getText().toString();
            try {
                // 调用远程 Service 的方法
                List<Pet> pets = petService.getPets(new Person(1, personName, personName));
                ArrayAdapter<Pet> adapter = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_list_item_1, pets);
                showView.setAdapter(adapter);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(conn);
    }
}

运行该程序后,输入 Person 名字并单击按钮,程序将获取远程 Service 返回的 Pet 列表,并显示在界面上。

运行结果将如图 10.10 所示,显示从远程 Service 获取到的 Pet 数据。


通过以上步骤,本示例实现了使用 AIDL Service 传递自定义类型数据的跨进程调用。

10.3 电话管理器 (TelephonyManager)

TelephonyManager 是一个管理手机通话状态和电话网络信息的服务类,提供了许多 getXxx() 方法来获取相关的电话网络信息。

在程序中获取 TelephonyManager 非常简单,只需调用以下代码:

TelephonyManager tManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);

接下来可以通过 TelephonyManager 获取相关信息或进行相关操作。

实例:获取网络和 SIM 卡信息

通过 TelephonyManager 提供的方法,可以获取手机网络和 SIM 卡的相关信息。以下是一个使用 ListView 来显示这些信息的示例:

public class MainActivity extends ListActivity {
    private List<String> statusValues = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.READ_PHONE_STATE}, 0x123);
    }

    @Override
    @SuppressLint("MissingPermission")
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == 0x123 && grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED) {
            TelephonyManager tManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
            String[] statusNames = getResources().getStringArray(R.array.statusNames);
            String[] simState = getResources().getStringArray(R.array.simState);
            String[] phoneType = getResources().getStringArray(R.array.phoneType);

            // 获取相关信息
            statusValues.add(tManager.getImei());
            statusValues.add(tManager.getDeviceSoftwareVersion() != null ? tManager.getDeviceSoftwareVersion() : "未知");
            statusValues.add(tManager.getNetworkOperator());
            statusValues.add(tManager.getNetworkOperatorName());
            statusValues.add(phoneType[tManager.getPhoneType()]);
            statusValues.add(tManager.getAllCellInfo() != null ? tManager.getAllCellInfo().toString() : "未知信息");
            statusValues.add(tManager.getSimCountryIso());
            statusValues.add(tManager.getSimSerialNumber());
            statusValues.add(simState[tManager.getSimState()]);

            // 封装数据并显示在 ListView 中
            List<Map<String, String>> status = new ArrayList<>();
            for (int i = 0; i < statusValues.size(); i++) {
                Map<String, String> map = new HashMap<>();
                map.put("name", statusNames[i]);
                map.put("value", statusValues.get(i));
                status.add(map);
            }

            SimpleAdapter adapter = new SimpleAdapter(this, status, R.layout.line, new String[]{"name", "value"}, new int[]{R.id.name, R.id.value});
            setListAdapter(adapter);
        } else {
            Toast.makeText(this, R.string.permission_tip, Toast.LENGTH_SHORT).show();
        }
    }
}
AndroidManifest.xml 配置

由于该应用需要获取手机的位置和状态信息,因此需要在 AndroidManifest.xml 文件中增加以下权限:

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>

运行该程序后,可以看到如图 10.11 所示的输出,显示了 SIM 卡和网络的相关信息。

实例:监听手机来电

通过 TelephonyManagerlisten() 方法可以监听通话状态,下面的示例程序演示了如何监听手机的来电。

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        requestPermissions(new String[]{Manifest.permission.READ_PHONE_STATE}, 0x123);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == 0x123 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            TelephonyManager tManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);

            PhoneStateListener listener = new PhoneStateListener() {
                @Override
                public void onCallStateChanged(int state, String number) {
                    switch (state) {
                        case TelephonyManager.CALL_STATE_IDLE:
                            break;
                        case TelephonyManager.CALL_STATE_OFFHOOK:
                            break;
                        case TelephonyManager.CALL_STATE_RINGING:
                            try (OutputStream os = openFileOutput("phoneList", Context.MODE_APPEND);
                                 PrintStream ps = new PrintStream(os)) {
                                ps.println(new Date().toString() + " 来电: " + number);
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                            break;
                    }
                    super.onCallStateChanged(state, number);
                }
            };

            // 监听电话状态
            tManager.listen(listener, PhoneStateListener.LISTEN_CALL_STATE);
        }
    }
}
AndroidManifest.xml 配置

由于该程序需要获取手机的通话状态,因此必须在 AndroidManifest.xml 文件中增加以下权限:

<uses-permission android:name="android.permission.READ_PHONE_STATE"/>

运行该程序,在手机来电时,程序会将来电号码记录到文件中,并保存在 data/data/<package-name>/files/phoneList 路径下。

10.4 短信管理器 (SmsManager)

SmsManager 是 Android 提供的一个常见服务,主要用于发送短信。SmsManager 提供了一系列 sendXxxMessage() 方法来发送短信,其中最常用的是 sendTextMessage() 方法,用于发送普通的文本短信。

实例:发送短信

该实例提供了一个简单的界面,用户可以输入收件人号码和短信内容,并通过点击“发送”按钮来发送短信。

public class MainActivity extends Activity {
    private EditText numberEt;
    private EditText contentEt;
    private Button sendBn;

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

        // 获取界面上的文本框和按钮
        numberEt = findViewById(R.id.number);
        contentEt = findViewById(R.id.content);
        sendBn = findViewById(R.id.send);

        // 请求发送短信的权限
        requestPermissions(new String[]{Manifest.permission.SEND_SMS}, 0x123);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == 0x123 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 获取 SmsManager 实例
            SmsManager sManager = SmsManager.getDefault();

            // 设置发送按钮的点击监听器
            sendBn.setOnClickListener(view -> {
                PendingIntent pi = PendingIntent.getActivity(MainActivity.this, 0, new Intent(), 0);
                // 发送短信
                sManager.sendTextMessage(numberEt.getText().toString(), null, contentEt.getText().toString(), pi, null);
                // 提示短信发送完成
                Toast.makeText(MainActivity.this, "短信发送完成", Toast.LENGTH_SHORT).show();
            });
        } else {
            Toast.makeText(this, R.string.permission_tip, Toast.LENGTH_SHORT).show();
        }
    }
}

该程序调用 SmsManager.sendTextMessage() 方法来发送短信,使用了 PendingIntent 来监控短信发送的状态。

AndroidManifest.xml 配置

由于程序需要发送短信,因此必须在 AndroidManifest.xml 文件中添加以下权限:

<uses-permission android:name="android.permission.SEND_SMS"/>
实例:短信群发

短信群发可以将同一条短信发送给多个收件人。该程序提供了一个界面供用户选择多个收件人,并向每个收件人发送短信。

public class MainActivity extends Activity {
    private TextView numbersTv;
    private EditText contentEt;
    private Button selectBn;
    private Button sendBn;
    private List<String> sendList = new ArrayList<>();

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

        numbersTv = findViewById(R.id.numbers);
        contentEt = findViewById(R.id.content);
        selectBn = findViewById(R.id.select);
        sendBn = findViewById(R.id.send);

        // 请求发送短信和读取联系人的权限
        requestPermissions(new String[]{Manifest.permission.SEND_SMS, Manifest.permission.READ_CONTACTS}, 0x123);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == 0x123 && grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED) {
            // 获取 SmsManager 实例
            SmsManager sManager = SmsManager.getDefault();

            // 设置发送按钮的点击监听器
            sendBn.setOnClickListener(view -> {
                for (String number : sendList) {
                    PendingIntent pi = PendingIntent.getActivity(MainActivity.this, 0, new Intent(), 0);
                    sManager.sendTextMessage(number, null, contentEt.getText().toString(), pi, null);
                }
                Toast.makeText(MainActivity.this, "短信群发完成", Toast.LENGTH_SHORT).show();
            });

            // 设置选择联系人按钮的点击监听器
            selectBn.setOnClickListener(view -> {
                Cursor cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);
                List<String> numberList = new ArrayList<>();
                while (cursor.moveToNext()) {
                    numberList.add(cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)).replace("-", "").replace(" ", ""));
                }
                cursor.close();

                // 显示联系人选择对话框
                BaseAdapter adapter = new BaseAdapter() {
                    @Override
                    public int getCount() {
                        return numberList.size();
                    }

                    @Override
                    public Object getItem(int position) {
                        return position;
                    }

                    @Override
                    public long getItemId(int position) {
                        return position;
                    }

                    @Override
                    public View getView(int position, View convertView, ViewGroup parent) {
                        CheckBox rb;
                        if (convertView == null) {
                            rb = new CheckBox(MainActivity.this);
                        } else {
                            rb = (CheckBox) convertView;
                        }

                        String number = numberList.get(position);
                        rb.setText(number);
                        if (sendList.contains(number)) {
                            rb.setChecked(true);
                        }

                        return rb;
                    }
                };

                View selectView = getLayoutInflater().inflate(R.layout.list, null);
                ListView listView = selectView.findViewById(R.id.list);
                listView.setAdapter(adapter);

                new AlertDialog.Builder(MainActivity.this)
                        .setView(selectView)
                        .setPositiveButton("确定", (v, which) -> {
                            sendList.clear();
                            for (int i = 0; i < listView.getCount(); i++) {
                                CheckBox checkBox = (CheckBox) listView.getChildAt(i);
                                if (checkBox.isChecked()) {
                                    sendList.add(checkBox.getText().toString());
                                }
                            }
                            numbersTv.setText(sendList.toString());
                        }).show();
            });
        } else {
            Toast.makeText(this, R.string.permission_tip, Toast.LENGTH_SHORT).show();
        }
    }
}
AndroidManifest.xml 配置

该程序不仅需要发送短信,还需要访问联系人信息,因此需要添加以下权限:

<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.READ_CONTACTS"/>

提示

短信群发程序有一个潜在的风险:如果发送的收件人过多且网络延迟严重,群发短信可能会成为一个耗时任务。此时,可以考虑使用 IntentService 来处理群发短信,并通过广播通知前台 Activity

10.5 音频管理器 (AudioManager)

在某些情况下,程序可能需要管理系统音量,或者让系统静音,这时可以使用 Android 提供的 AudioManager 来实现。通过调用 getSystemService() 方法获取系统的音频管理器,然后使用 AudioManager 的方法来控制手机音频。

10.5.1 AudioManager 简介

获取到 AudioManager 对象后,可以通过以下常用方法来控制手机音频:

  • adjustStreamVolume(int streamType, int direction, int flags): 调整指定类型的声音。

    • 第一个参数 streamType 指定声音类型,例如:
      • STREAM_ALARM: 手机闹铃声音
      • STREAM_MUSIC: 手机音乐声音
      • STREAM_RING: 电话铃声
    • 第二个参数 direction 指定音量增大、减小或静音。
    • 第三个参数 flags 设置调整音量时的标志,例如 FLAG_SHOW_UI 可以显示音量进度条。
  • setMicrophoneMute(boolean on): 设置麦克风是否静音。

  • setMode(int mode): 设置声音模式,例如 NORMALRINGTONEIN_CALL

  • setRingerMode(int ringerMode): 设置电话铃声模式,例如 RINGER_MODE_NORMALRINGER_MODE_SILENT

  • setSpeakerphoneOn(boolean on): 设置是否打开扩音器。

  • setStreamVolume(int streamType, int index, int flags): 直接设置指定类型的音量。

10.5.2 实例:使用 AudioManager 控制手机音频

该实例演示了如何使用 AudioManager 来控制手机音量。程序提供了按钮来播放音乐,并提供了增大、减小音量的功能,还通过一个 ToggleButton 控制是否静音。

public class MainActivity extends Activity {
    private AudioManager aManager;

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

        // 获取系统的音频服务
        aManager = (AudioManager) getSystemService(Service.AUDIO_SERVICE);

        // 获取界面上的按钮和 ToggleButton
        Button playBn = findViewById(R.id.play);
        Button upBn = findViewById(R.id.up);
        Button downBn = findViewById(R.id.down);
        ToggleButton muteTb = findViewById(R.id.mute);

        // 设置播放按钮的点击事件
        playBn.setOnClickListener(view -> {
            // 初始化 MediaPlayer 对象,播放音乐
            MediaPlayer mPlayer = MediaPlayer.create(MainActivity.this, R.raw.earth);
            mPlayer.setLooping(true);
            mPlayer.start(); // 开始播放
        });

        // 设置增大音量按钮的点击事件
        upBn.setOnClickListener(view -> {
            // 调节音乐音量,增大音量,显示音量图形
            aManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_RAISE, AudioManager.FLAG_SHOW_UI);
        });

        // 设置降低音量按钮的点击事件
        downBn.setOnClickListener(view -> {
            // 调节音乐音量,降低音量,显示音量图形
            aManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_LOWER, AudioManager.FLAG_SHOW_UI);
        });

        // 设置静音按钮的点击事件
        muteTb.setOnCheckedChangeListener((source, isChecked) -> {
            // 根据 isChecked 来确定是否静音
            aManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, isChecked ? AudioManager.ADJUST_MUTE : AudioManager.ADJUST_UNMUTE, AudioManager.FLAG_SHOW_UI);
        });
    }
}
关键代码解析
  • 使用 AudioManager.adjustStreamVolume() 方法控制音乐音量的增大、减小和静音。
  • MediaPlayer.create() 用于播放本地音乐文件,setLooping() 设置音乐循环播放。
AndroidManifest.xml 配置

无需特殊权限,只要程序中涉及到播放音频文件,需要在 AndroidManifest.xml 文件中添加权限来访问该音频文件。

运行该程序,用户可以通过点击按钮来增大或降低音乐的音量,或者切换静音状态。音量调整的效果会实时反映在系统音量控制条上,如图 10.12 所示。

10.6 振动器 (Vibrator)

在某些情况下,程序可能需要启动系统振动器。例如,在手机静音时使用振动提醒用户,或者在游戏中使用振动模拟碰撞、爆炸等效果。振动作为多媒体体验的一部分,可以为用户带来更真实的感受。Android 提供了 Vibrator 类来控制手机的振动,通过调用 getSystemService() 方法即可获取系统振动器。

10.6.1 Vibrator 简介

Vibrator 类提供了以下几个常用方法来控制手机振动:

  • vibrate(VibrationEffect vibe): 按照 VibrationEffect 效果控制手机振动。
  • vibrate(VibrationEffect vibe, AudioAttributes attributes): 按照 VibrationEffect 效果和 AudioAttributes 声音效果控制振动。
  • cancel(): 停止手机振动。

其中,VibrationEffect 参数用于定义振动效果,Vibrator 提供了以下方法来创建振动效果:

  • createOneShot(long milliseconds, int amplitude): 创建只振动一次的效果。

    • milliseconds:振动持续时间(毫秒)。
    • amplitude:振动强度(0-255)。
  • createWaveform(long[] timings, int[] amplitudes, int repeat): 创建波形振动效果。

    • timings:振动时间的数组。
    • amplitudes:振动强度的数组。
  • createWaveform(long[] timings, int repeat): 创建简单的波形振动效果,只包含时间控制。

掌握了这些方法后,可以在程序中通过 Vibrator 控制手机振动。

10.6.2 实例:使用 Vibrator 控制手机振动

下面的实例演示了如何使用 Vibrator 来控制手机振动。当用户长按触摸屏时,手机将振动 2 秒。代码如下:

public class MainActivity extends Activity {
    private Vibrator vibrator;
    private GestureDetector detector;

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

        // 获取系统的 Vibrator 服务
        vibrator = (Vibrator) getSystemService(Service.VIBRATOR_SERVICE);

        // 初始化手势检测器,检测长按事件
        detector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public void onLongPress(MotionEvent e) {
                Toast.makeText(MainActivity.this, "手机振动", Toast.LENGTH_SHORT).show();
                // 控制手机振动 2 秒,振动强度为 180
                vibrator.vibrate(VibrationEffect.createOneShot(2000, 180));
            }
        });
    }

    // 重写 onTouchEvent 方法,检测用户触摸屏幕事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return detector.onTouchEvent(event);
    }
}
关键代码解析
  1. Vibrator vibrator = (Vibrator) getSystemService(Service.VIBRATOR_SERVICE);:通过系统服务获取 Vibrator 对象。
  2. vibrator.vibrate(VibrationEffect.createOneShot(2000, 180));:调用 vibrate() 方法,让手机振动 2 秒,并设置振动强度为 180。
  3. 通过 GestureDetector 监听长按事件,并在用户长按时触发振动。
AndroidManifest.xml 配置

程序需要权限来访问振动器,因此需要在 AndroidManifest.xml 文件中增加以下权限声明:

<uses-permission android:name="android.permission.VIBRATE"/>
运行程序

在模拟器中运行该程序看不到振动效果,建议将该程序部署到真机上运行。长按屏幕时,手机会振动 2 秒,用户可以感受到振动效果。

10.7 手机闹钟服务 (AlarmManager)

AlarmManager 的主要用途是开发手机闹钟应用,但它的本质是一个全局定时器,可以在指定时间或周期启动其他组件(如 ActivityServiceBroadcastReceiver)。

10.7.1 AlarmManager 简介

AlarmManager 不仅可以用于开发闹钟应用,还可以作为全局定时器使用。可以通过 Context.getSystemService() 方法获取 AlarmManager 对象。一旦获取到该对象,就可以使用以下方法设置定时启动指定组件:

  • set(int type, long triggerAtTime, PendingIntent operation):设置在 triggerAtTime 时间启动由 operation 参数指定的组件。

    • type 参数可以取以下值:
      • ELAPSED_REALTIME:从设备启动到现在经过的时间来启动组件。
      • ELAPSED_REALTIME_WAKEUP:类似 ELAPSED_REALTIME,但系统休眠时也会唤醒并启动组件。
      • RTC:使用系统时间启动组件。
      • RTC_WAKEUP:类似 RTC,但系统休眠时也会唤醒并启动组件。
  • setInexactRepeating(int type, long triggerAtTime, long interval, PendingIntent operation):设置非精确的周期性定时任务。

  • setRepeating(int type, long triggerAtTime, long interval, PendingIntent operation):设置精确的周期性定时任务。

  • cancel(PendingIntent operation):取消已设置的定时任务。

  • setExact(int type, long triggerAtMillis, PendingIntent operation):设置在精确时间激发的定时任务。

  • setExactAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation):即使在低功耗空闲状态,也可精确激发。

10.7.2 设置闹钟

这个示例演示了如何使用 AlarmManager 设置闹钟。用户可以设置闹铃时间,闹钟会在设定的时间启动一个 Activity,即使用户退出程序也会执行。

MainActivity.java
public class MainActivity extends Activity {
    private AlarmManager aManager;

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

        // 获取 AlarmManager 对象
        aManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);

        // 获取界面上的按钮
        Button setTimeBn = findViewById(R.id.setTime);

        // 设置闹钟按钮的点击事件
        setTimeBn.setOnClickListener(view -> {
            Calendar currentTime = Calendar.getInstance();

            new TimePickerDialog(MainActivity.this, (dialog, hourOfDay, minute) -> {
                // 启动 AlarmActivity
                Intent intent = new Intent(MainActivity.this, AlarmActivity.class);
                PendingIntent pi = PendingIntent.getActivity(MainActivity.this, 0, intent, 0);

                // 设置闹铃时间
                Calendar c = Calendar.getInstance();
                c.set(Calendar.HOUR_OF_DAY, hourOfDay);
                c.set(Calendar.MINUTE, minute);

                // 设置闹钟
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    aManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, c.getTimeInMillis(), pi);
                } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                    aManager.setExact(AlarmManager.RTC_WAKEUP, c.getTimeInMillis(), pi);
                } else {
                    aManager.set(AlarmManager.RTC_WAKEUP, c.getTimeInMillis(), pi);
                }

                // 提示闹钟设置成功
                Toast.makeText(MainActivity.this, "闹铃设置成功", Toast.LENGTH_SHORT).show();
            }, currentTime.get(Calendar.HOUR_OF_DAY), currentTime.get(Calendar.MINUTE), false).show();
        });
    }
}
AlarmActivity.java

AlarmActivity 是在闹钟时间到达时启动的 Activity,它通过一个对话框和音乐提醒用户。

public class AlarmActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 加载并播放闹钟音乐
        MediaPlayer alarmMusic = MediaPlayer.create(this, R.raw.alarm);
        alarmMusic.setLooping(true);
        alarmMusic.start();

        // 显示闹钟响了的提示对话框
        new AlertDialog.Builder(AlarmActivity.this)
                .setTitle("闹钟")
                .setMessage("闹钟响了,起床啦!")
                .setPositiveButton("确定", (dialog, which) -> {
                    // 停止音乐
                    alarmMusic.stop();
                    finish();  // 结束 Activity
                })
                .show();
    }
}
AndroidManifest.xml 配置

别忘了在 AndroidManifest.xml 中配置 AlarmActivity,否则程序不会正确启动该 Activity

<activity android:name=".AlarmActivity" />
运行效果

运行该程序后,用户可以设置闹钟时间。当设定的时间到达时,系统会唤醒并启动 AlarmActivity,显示闹钟响了的提示并播放音乐提醒用户,如图 10.13 所示。

通过这个例子,用户可以更好地理解如何使用 AlarmManager 实现定时任务或闹钟功能。

10.8 广播接收器 (BroadcastReceiver)

BroadcastReceiver 是 Android 四大组件之一,本质上是一个全局监听器,用于监听系统全局的广播消息。BroadcastReceiver 可以非常方便地实现系统中不同组件之间的通信。通过广播,系统可以在不同的应用程序之间发送消息。Android 8 引入了一个新规则,要求启动 BroadcastReceiverIntent 必须是显式的 Intent,类似于启动 Service

10.8.1 BroadcastReceiver 简介

BroadcastReceiver 用于接收应用程序或系统发出的广播 Intent,其工作流程与启动 ActivityService 类似。

要启动 BroadcastReceiver,需要执行以下两步:

  1. 创建需要启动 BroadcastReceiverIntent
  2. 调用 ContextsendBroadcast()sendOrderedBroadcast() 方法来启动指定的 BroadcastReceiver

当广播 Intent 发送后,所有匹配该 IntentBroadcastReceiver 都可能被激发。

ActivityService 不同,BroadcastReceiver 没有完整的生命周期,它本质上是一个系统级的监听器,只要有匹配的 Intent,它就会被触发。

BroadcastReceiver 的注册可以通过代码或在 AndroidManifest.xml 中进行:

  • 代码注册:使用 Context.registerReceiver() 方法。
    IntentFilter filter = new IntentFilter("android.provider.Telephony.SMS_RECEIVED");
    registerReceiver(new MyReceiver(), filter);
    
  • 静态注册:在 AndroidManifest.xml 文件中配置。
    <receiver android:name=".MyReceiver">
      <intent-filter>
        <action android:name="android.provider.Telephony.SMS_RECEIVED" />
      </intent-filter>
    </receiver>
    

每当系统的广播事件发生时,系统会创建 BroadcastReceiver 实例并自动触发它的 onReceive() 方法,执行完成后实例就会被销毁。由于 onReceive() 方法的生命周期非常短,因此不适合在其中执行耗时操作。需要执行耗时任务时,可以通过启动 Service 来完成。

10.8.2 发送广播

在应用程序中发送广播非常简单,只需要调用 Context.sendBroadcast(Intent intent) 方法即可。

MainActivity.java
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 获取界面上的发送按钮
        Button sendBn = findViewById(R.id.send);
        sendBn.setOnClickListener(view -> {
            // 创建 Intent 对象并设置 Action 和 package
            Intent intent = new Intent();
            intent.setAction("org.crazyit.action.CRAZY_BROADCAST");
            intent.setPackage("org.crazyit.broadcast");
            intent.putExtra("msg", "简单的消息");
            // 发送广播
            sendBroadcast(intent);
        });
    }
}
MyReceiver.java

接收广播的 BroadcastReceiver 需要重写 onReceive() 方法:

public class MyReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "接收到的Intent的Action为:" + intent.getAction()
                + "\n消息内容是:" + intent.getStringExtra("msg"), Toast.LENGTH_LONG).show();
    }
}

AndroidManifest.xml 文件中配置接收该广播的 BroadcastReceiver

<receiver android:name=".MyReceiver">
  <intent-filter>
    <action android:name="org.crazyit.action.CRAZY_BROADCAST" />
  </intent-filter>
</receiver>

运行程序后,点击发送按钮即可看到广播接收到的消息。

10.8.3 有序广播

Broadcast 可以分为两种:

  1. 普通广播 (Normal Broadcast):异步广播,所有接收者几乎同时接收广播,无法传递处理结果,也不能终止广播的传播。
  2. 有序广播 (Ordered Broadcast):接收者按照优先级依次接收广播,优先级较高的接收者可以传递数据给下一个接收者,也可以终止广播的传播。

发送有序广播时,可以使用 sendOrderedBroadcast() 方法,并设置接收者的优先级。优先接收到广播的接收者可以调用 abortBroadcast() 方法终止广播传播。

MainActivity.java
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 获取界面上的发送按钮
        Button sendBn = findViewById(R.id.send);
        sendBn.setOnClickListener(view -> {
            // 创建 Intent 对象并设置 Action 和 package
            Intent intent = new Intent();
            intent.setAction("org.crazyit.action.CRAZY_BROADCAST");
            intent.setPackage("org.crazyit.broadcast");
            intent.putExtra("msg", "简单的消息");
            // 发送有序广播
            sendOrderedBroadcast(intent, null);
        });
    }
}
MyReceiver1.java

第一个接收广播的 BroadcastReceiver 可以终止广播的传播:

public class MyReceiver1 extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "第一个接收者: " + intent.getStringExtra("msg"), Toast.LENGTH_SHORT).show();
        Bundle bundle = new Bundle();
        bundle.putString("first", "第一个BroadcastReceiver 存入的消息");
        setResultExtras(bundle);
        abortBroadcast();  // 终止广播传播
    }
}
MyReceiver2.java

第二个接收广播的 BroadcastReceiver 解析前一个接收者存入的数据:

public class MyReceiver2 extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Bundle bundle = getResultExtras(true);
        String first = bundle.getString("first");
        Toast.makeText(context, "第一个 Broadcast 存入的消息为:" + first, Toast.LENGTH_LONG).show();
    }
}

AndroidManifest.xml 中配置两个 BroadcastReceiver 的优先级:

<receiver android:name=".MyReceiver1">
  <intent-filter android:priority="20">
    <action android:name="org.crazyit.action.CRAZY_BROADCAST" />
  </intent-filter>
</receiver>

<receiver android:name=".MyReceiver2">
  <intent-filter android:priority="0">
    <action android:name="org.crazyit.action.CRAZY_BROADCAST" />
  </intent-filter>
</receiver>

运行程序并发送有序广播时,按优先级依次触发接收者的 onReceive() 方法。

实例:基于 Service 的音乐播放器

本实例展示了如何利用 BroadcastReceiverActivityService 之间通信,创建一个基于 Service 的音乐播放器。播放器通过后台 Service 播放音乐,Activity 用来控制音乐的播放和显示歌曲信息。

程序设计思路如下:

  • 音乐的播放由后台运行的 Service 控制。
  • Activity 通过广播通知 Service 控制音乐的播放、暂停和停止。
  • Service 通过广播通知 Activity 播放状态和当前歌曲的信息,更新界面。

1. 前台 Activity 代码

public class MainActivity extends Activity {
    public static final String CTL_ACTION = "org.crazyit.action.CTL_ACTION";
    public static final String UPDATE_ACTION = "org.crazyit.action.UPDATE_ACTION";

    // 界面元素
    private TextView titleTv;
    private TextView authorTv;
    private ImageButton playBn;
    private ImageButton stopBn;

    // 歌曲信息
    String[] titleStrs = {"心愿", "约定", "美丽新世界"};
    String[] authorStrs = {"未知艺术家", "周蕙", "伍佰"};
    int status = 0x11; // 播放状态:0x11代表未播放,0x12代表播放中,0x13代表暂停
    private ActivityReceiver activityReceiver;

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

        // 获取界面控件
        playBn = findViewById(R.id.play);
        stopBn = findViewById(R.id.stop);
        titleTv = findViewById(R.id.title);
        authorTv = findViewById(R.id.author);

        // 监听器,用于发送控制广播
        View.OnClickListener listener = source -> {
            Intent intent = new Intent(CTL_ACTION);
            intent.setPackage("org.crazyit.broadcast");
            switch (source.getId()) {
                case R.id.play:
                    intent.putExtra("control", 1); // 控制播放/暂停
                    break;
                case R.id.stop:
                    intent.putExtra("control", 2); // 控制停止
                    break;
            }
            sendBroadcast(intent); // 发送广播
        };

        // 按钮绑定监听器
        playBn.setOnClickListener(listener);
        stopBn.setOnClickListener(listener);

        // 注册广播接收器
        activityReceiver = new ActivityReceiver();
        IntentFilter filter = new IntentFilter(UPDATE_ACTION);
        registerReceiver(activityReceiver, filter);

        // 启动后台 Service
        Intent intent = new Intent(this, MusicService.class);
        startService(intent);
    }

    // 自定义 BroadcastReceiver,用于接收 Service 发来的状态更新广播
    class ActivityReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            int current = intent.getIntExtra("current", -1);
            if (current >= 0) {
                titleTv.setText(titleStrs[current]);
                authorTv.setText(authorStrs[current]);
            }

            int update = intent.getIntExtra("update", -1);
            switch (update) {
                case 0x11:
                    playBn.setImageResource(R.drawable.play);
                    status = 0x11;
                    break;
                case 0x12:
                    playBn.setImageResource(R.drawable.pause);
                    status = 0x12;
                    break;
                case 0x13:
                    playBn.setImageResource(R.drawable.play);
                    status = 0x13;
                    break;
            }
        }
    }
}

2. 后台 Service 代码

public class MusicService extends Service {
    private MyReceiver serviceReceiver;
    private MediaPlayer mPlayer;
    private String[] musics = {"wish.mp3", "promise.mp3", "beautiful.mp3"};
    private int status = 0x11; // 当前播放状态
    private int current = 0; // 当前播放的歌曲索引
    private AssetManager am;

    @Override
    public void onCreate() {
        super.onCreate();
        am = getAssets();
        serviceReceiver = new MyReceiver();
        IntentFilter filter = new IntentFilter(MainActivity.CTL_ACTION);
        registerReceiver(serviceReceiver, filter);

        mPlayer = new MediaPlayer();
        mPlayer.setOnCompletionListener(mp -> {
            current = (current + 1) % musics.length;
            Intent sendIntent = new Intent(MainActivity.UPDATE_ACTION);
            sendIntent.setPackage("org.crazyit.broadcast");
            sendIntent.putExtra("current", current);
            sendBroadcast(sendIntent);
            prepareAndPlay(musics[current]);
        });
    }

    class MyReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            int control = intent.getIntExtra("control", -1);
            switch (control) {
                case 1: // 播放/暂停
                    if (status == 0x11) {
                        prepareAndPlay(musics[current]);
                        status = 0x12;
                    } else if (status == 0x12) {
                        mPlayer.pause();
                        status = 0x13;
                    } else if (status == 0x13) {
                        mPlayer.start();
                        status = 0x12;
                    }
                    break;
                case 2: // 停止
                    if (status == 0x12 || status == 0x13) {
                        mPlayer.stop();
                        status = 0x11;
                    }
                    break;
            }
            Intent sendIntent = new Intent(MainActivity.UPDATE_ACTION);
            sendIntent.setPackage("org.crazyit.broadcast");
            sendIntent.putExtra("update", status);
            sendIntent.putExtra("current", current);
            sendBroadcast(sendIntent);
        }
    }

    private void prepareAndPlay(String music) {
        try {
            AssetFileDescriptor afd = am.openFd(music);
            mPlayer.reset();
            mPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
            mPlayer.prepare();
            mPlayer.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

3. AndroidManifest.xml 配置

AndroidManifest.xml 文件中只需注册 ActivityService,不需要注册 BroadcastReceiver

<activity android:name=".MainActivity" />
<service android:name=".MusicService" />

4. 实现思路

  • MainActivity 发送控制广播来控制音乐的播放、暂停或停止。
  • MusicService 通过 BroadcastReceiver 接收这些控制广播,控制音乐播放状态,并通过广播将播放状态和歌曲信息传回 Activity
  • ActivityBroadcastReceiver 接收 Service 发来的状态更新广播,并更新界面。

5. 运行效果

运行该程序后,用户可以通过界面的按钮控制音乐的播放、暂停和停止。即使退出 Activity,音乐仍会在后台播放,这是因为 Service 继续运行并控制音乐的播放。用户可以通过停止按钮停止音乐播放。

10.9 接收系统广播消息

除了接收应用发出的广播消息,BroadcastReceiver 还能用于接收系统广播。系统会在特定事件发生时发送广播,应用可以通过监听这些广播来执行对应操作。以下是一些常见的系统广播事件:

  • ACTION_TIME_CHANGED: 系统时间被改变
  • ACTION_DATE_CHANGED: 系统日期被改变
  • ACTION_TIMEZONE_CHANGED: 系统时区被改变
  • ACTION_BOOT_COMPLETED: 系统启动完成
  • ACTION_PACKAGE_ADDED: 系统安装了一个应用
  • ACTION_PACKAGE_REMOVED: 系统移除了一个应用
  • ACTION_BATTERY_CHANGED: 电池电量发生改变
  • ACTION_BATTERY_LOW: 电池电量过低
  • ACTION_POWER_CONNECTED: 系统连接电源
  • ACTION_POWER_DISCONNECTED: 系统断开电源
  • ACTION_SHUTDOWN: 系统关机

实例 1:开机自动启动的 Activity

此示例展示了如何监听系统开机广播 (ACTION_BOOT_COMPLETED),并在系统启动时自动运行应用的 Activity

1. BroadcastReceiver 代码

public class LaunchReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        // 启动主Activity
        Intent tIntent = new Intent(context, MainActivity.class);
        tIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);  // 必须设置FLAG_ACTIVITY_NEW_TASK
        context.startActivity(tIntent);
    }
}

2. AndroidManifest.xml 配置

<receiver android:name=".LaunchReceiver">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>

<!-- 授予应用接收系统开机广播的权限 -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

该程序在运行过一次之后,每次手机开机时都会自动启动指定的 Activity

实例 2:手机电量提示

通过监听系统电池相关的广播,应用可以在电池电量过低时提示用户。这里展示如何监听 ACTION_BATTERY_CHANGEDACTION_BATTERY_LOW 广播事件来实现电量监控。

1. BroadcastReceiver 代码

public class BatteryReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Bundle bundle = intent.getExtras();
        // 获取当前电量
        int current = bundle.getInt("level");
        // 获取总电量
        int total = bundle.getInt("scale");
        // 如果当前电量低于15%
        if (current * 1.0 / total < 0.15) {
            Toast.makeText(context, "电量过低,请尽快充电!", Toast.LENGTH_LONG).show();
        }
    }
}

2. 注册广播接收器的 Activity 代码

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 注册BatteryReceiver,用于监听电量变化
        IntentFilter batteryFilter = new IntentFilter();
        batteryFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
        registerReceiver(new BatteryReceiver(), batteryFilter);
    }
}

3. AndroidManifest.xml 权限配置

<!-- 授权应用读取电量信息 -->
<uses-permission android:name="android.permission.BATTERY_STATS" />

运行该应用后,可以通过改变模拟器中的电量来触发电量变化广播,应用会显示低电量提示。

使用 BatteryManager 读取电池信息

除了通过广播接收器监听电量变化外,还可以通过 BatteryManager 直接获取设备的电池状态和电量信息。例如:

BatteryManager bm = (BatteryManager) getSystemService(BATTERY_SERVICE);

// 获取电池状态
int status = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_STATUS);

// 获取电池剩余电量百分比
int capacity = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);

// 获取电池当前的平均电流,正值表示充电,负值表示放电
int currentAverage = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_AVERAGE);

// 获取电池的瞬时电流
int currentNow = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW);

10.10 本章小结

本章通过详细介绍了 Android 系统中的四大核心组件之一——ServiceBroadcastReceiver,至此,Android 的四大组件已经全部介绍完毕,包括 ActivityContentProviderServiceBroadcastReceiver

在学习 Service 时,需要重点掌握以下内容:

  • Service 的创建和配置:了解如何编写和在 Manifest 文件中配置 Service。
  • Service 的启动与停止:掌握如何启动和停止服务,尤其是如何通过 startService()bindService() 方法来管理服务的生命周期。
  • IntentService 与 AIDL 服务:理解 IntentService 的异步任务处理机制,以及如何使用 AIDL 实现跨进程的服务调用。

在学习 BroadcastReceiver 时,需要重点掌握以下内容:

  • BroadcastReceiver 的创建和配置:理解如何编写广播接收器,并在代码中注册或者在 AndroidManifest.xml 中配置接收器。
  • 广播的发送和接收:掌握如何通过 sendBroadcast()sendOrderedBroadcast() 发送广播,并如何在接收器中处理收到的广播信息。

此外,本章还介绍了许多 Android 系统服务的使用方法,这些服务是实际开发中不可或缺的工具,包括:

  • TelephonyManager:用于获取手机状态和 SIM 卡信息。
  • SmsManager:用于发送短信。
  • AudioManager:控制系统音量和声音模式。
  • Vibrator:控制手机的振动功能。
  • AlarmManager:设置闹钟或定时任务。

通过本章的学习,读者应当对 Android 系统中的服务机制、广播接收器的使用,以及如何与系统服务交互有了全面的了解,能够在实际项目中灵活运用这些组件和服务。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值