13 服务:愿意为您效劳

1.引入

1.0 引入

你可能希望某些操作能一直运行,而不论哪一个应用得到焦点

举例来说,如果你在一个音乐应用中播放音乐文件,可能希望即使切换到另一个应用,这个音乐文件还能继续播放。这一章中,你会看到如何使用服务来处理类似这种情况。在这个过程中,你会了解如何使用Android的一些内置服务,还会了解如何利用通知服务保证你的用户随时得到通知,以及如何使用位置服务告诉你现在的位置。

1.1 服务

1.1.1 服务定义

Android应用是活动和其他组件构成的一个集合。很大一部分代码都是用来与用户交互,不过有时也需要在后台做些工作。例如,你可能想下载一个大文件、播放一段音乐,或者监听来自服务器的一个消息。
这些任务并不适合用活动来完成。如果还算简单,也可以创建一个线程来完成任务,不过,如果不当心,活动代码就会变得非常复杂,可读性很差。

正是因为存在这个问题,所以引入了服务。服务(service)是与活动类似的应用组件,只不过服务没有用户界面。与活动相比,服务的生命周期比较简单,它提供了另外一组特性,可以很容易地编写在后台运行的代码,在运行这个代码的同时,用户还可以放手去做其他的工作。

1.1.2 服务类型

有两种类型的服务:

  • 启动服务( Started services)

启动服务可以在后台无限期地运行,即使启动这个服务的活动已经撤销,也不会有影响。如果想要从互联网下载一个大文件,可能就要创建为一个启动服务。一旦操作完成,服务就会停止。

  • 绑定服务(Bound serviees)

绑定服务会绑定到另一个组件,如一个活动。这个活动可以与绑定服务交互,发送请求,并得到结果。只要与之绑定的组件还在运行,绑定服务就会一直运行下去。这个组件不再与它绑定时,服务则被撤销。如果想创建一个里程表,测量一辆车走过多少距离,就可以使用一个绑定服务。这样与服务绑定的活动可以不断向这个服务请求所走过距离的更新情况。

1.2 需求与分析

我们要创建一个新工程,其中包含一个名为MainActivity的活动,以及一个名为DelayedMessageService的服务。只要MainActivity调用DelayedMessageservice,它会等待10秒,然后显示一段文本。

 我们将分3个阶段完成这个工作:

1.在日志中显示消息。
首先在日志中显示消息,检查服务是否能正常工作。可以在Android Studio中查看日志。
2.在Toast中显示消息。
我们将在一个弹出的toast中显示消息,这样就不用为了查看服务的工作情况而让设备一直连接Android Studio。
3.在一个Notification中显示消息。
我们要让DelayedMessageService使用Android的内置通知服务在一个通知中显示消息。这说明,用户可以在以后某个时间查看这个消息。

1.3 创建意图服务-IntentService

要创建一个新服务,可以扩展Service类或IntentService类。Service类是创建服务的基类。它提供了基本的服务功能,如果想创建一个绑定服务,通常都会扩展这个类。

Intentservice类是service的一个子类,设计用来处理意图。如果想创建一个启动服务,通常会扩展这个类。

1.4 意图服务IntentService概览

用一个IntentService创建启动服务

1.活动创建一个显式意图,指出它要调用哪个服务。

这个意图指定他想要的服务

2.将这个意图传递到服务

3.服务启动并处理意图

调用IntentService onHandleIntent()方法,他在一个单独的线程中运行。如果向服务传递了多个意图,它会按顺序逐个处理,一次处理一个意图。一旦运行结束,服务就会停止。

可以看到,启动服务的方式与启动活动是一样的:都需要创建一个意图。区别在于,启动一个服务时,屏幕上不会有变化,因为服务没有用户界面。

1.4 在AndroidManifext.xml中声明服务

就像活动一样,要用<service>元素在AndroidManifest.xml中声明服务。这样Android才能调用这个服务;如果一个服务未在AndroidManifest.xml中声明,Android就无法调用这个服务

创建一个新服务时,Android Studio会在AndroidManifest.xml中自动声明这个服务。代码如下:

<service>元素包含两个属性:

  1. android:name属性告诉Android服务名是什么,在这里,服务名为DelayedMessageservice。
  2. android:exported属性告诉Android这个服务是否可以由其他应用使用。将这个属性设置为false表示这个服务只能在当前应用中使用。

1.6 使用日志记录服务消息并测试

在日志中增加消息非常有用,可以检查你的代码是否能像预期的那样工作。在Java代码中告诉Android要记录什么,应用运行时,可以检查Android日志(或logcat)中的输出。

可以使用Android.util.Log类的以下某个方法记录消息日志:

 我们希望这个服务从意图得到一段文本,等待10秒时间,然后在日志中显示这段文本。

为此,我们要创建一个showText()方法记录文本,然后在一段延迟之后从onHandleIntent()方法调用这个showText()方法。

DeylayedMessageService代码:

public class DelayedMessageService extends IntentService {

    private static final String TAG="DelayedMessageService";

    public static final String EXTRA_MESSAGE="message";

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

    /**
     * 服务接收到意图时  运行该方法
     * @param intent  服务所接受到的意图
     */
    @Override
    protected void onHandleIntent(@Nullable Intent intent) {
        synchronized (this){
            try {
                wait(10000);//等待10秒
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            String text=intent.getStringExtra(EXTRA_MESSAGE);

        }
    }

    private void showText(final String text){
        Log.i(TAG, "The message is "+text);
    }
}

为activity_main.xml增加一个按钮

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/button_text"
        android:id="@+id/button"
        android:onClick="onClick"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"/>

</RelativeLayout>

然后来更新MainActivity.java中的代码

从活动启动一个服务与从活动启动另一个活动很类似。要创建一个显式意图,指定你想要启动的服务。然后使用startService()方法启动服务:

MainActivity中代码:

package com.hfad.joke;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;

public class MainActivity extends AppCompatActivity {

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

    /**
     * 单击按钮就会运行这个方法
     * @param view
     */
    public void onClick(View view){
        Intent intent=new Intent(this,DelayedMessageService.class);
        //向意图增加文本
        intent.putExtra(DelayedMessageService.EXTRA_MESSAGE,
                getResources().getString(R.string.button_response));//从资源中获取String
        startService(intent);//启动服务
    }
}

 测试运行:

1.7 用显示Toast在屏幕上显示

1.7.1 实现分析-屏幕更新要在主线程中完成

与活动不同,服务没有用户界面,不过这并不是说服务不需要告诉用户发生了什么。例如,用户可能需要知道一个文件什么时候下载完毕。

在这个应用中,如果能够在屏幕上的一个toast中显示消息,而不是在日志中显示这个消息,肯定会酷得多。但是要记住一点,所有更新用户界面的代码都要在主线程中运行

屏幕更新要在主线程中完成

使用意图服务时,要把需要运行的代码放在onHandleIntent()方法中。这些代码会在后台的一个单独的线程中运行。这对于希望在后台运行的代码是很合适的,不过,如果你想要更新用户界面,这就不适用了,因为只能在主线程中更新用户界面

要解决这个问题,需要使用一个处理器。在第4章我们说过,利用处理器,可以把需要运行的代码提交到一个单独的线程。可以使用处理器的post()方法提交代码,在主线程中创建一个toast。这个代码将在主线程中运行,正确地显示这个toast。

要让这个代码正确运行,需要完成下面的工作:

  1. 在主线程中创建一个处理器
  2. 在服务的onHandleIntent()方法中使用Handlerpost()方法显示个toast。

1.7.2 onStartCommand()在主线程运行

要在主线程创建处理器,需要在主线程中运行的一个方法中创建一个Handler对象。不能使用onHandleIntent()方法,因为它在后台线程中运行。这里要使用onstartcommand()方法。

每次启动意图服务时都会调用onstartCommand ()方法。onstartCommand ()方法在主线程中运行,而且是在onHandleIntent()方法之前运行

如果在onstartCommand()方法中创建了一个处理器,就能用它在onHandleIntent()方法中向主线程提交代码,该方法会在主线程运行,因此他会在主线程中创建一个新的处理器。<因为,这个代码里面会new 1个Handler,而这个代码在主线程中运行,因此就是在主线程中创建的>

使用onStartCommand()时,必须使用以下代码调用它的超类实现:

这样,意图服务才能正确地处理其后台线程的生命周期。

下面给出DelayedMessageService的代码:

package com.hfad.joke;

import android.app.IntentService;
import android.content.Intent;
import android.content.Context;
import android.os.Handler;
import android.util.Log;
import android.widget.Toast;

import androidx.annotation.Nullable;

/**
 * An {@link IntentService} subclass for handling asynchronous task requests in
 * a service on a separate handler thread.
 * <p>
 * TODO: Customize class - update intent actions, extra parameters and static
 * helper methods.
 */
public class DelayedMessageService extends IntentService {

    private static final String TAG="DelayedMessageService";

    public static final String EXTRA_MESSAGE="message";

    private Handler handler;

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

    /**
     * 该方法会在主线程运行,因此他会在主线程中创建一个新的处理器。
     */
    @Override
    public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
        handler=new Handler();
        return super.onStartCommand(intent, flags, startId);
    }

    /**
     * 服务接收到意图时  运行该方法
     * @param intent  服务所接受到的意图
     */
    @Override
    protected void onHandleIntent(@Nullable Intent intent) {
        synchronized (this){
            try {
                wait(10000);//等待10秒
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            String text=intent.getStringExtra(EXTRA_MESSAGE);
            showText(text);
        }
    }

    private void showText(final String text){
        handler.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(getApplicationContext(),text,Toast.LENGTH_LONG).show();
                //getApplicationContext()---显示toast的上下文。
            }
        });
    }


}

 应用上下文:

Toast.makeText()方法的第一个参数是显示toast的上下文。在一个活动中创建toast时,要用this传入当前活动的实例。

不过这对于服务是不可行的,因为服务上下文不能访问屏幕。在这种情况下,如果服务中需要一个上下文,必须使用getApplicationContext()。这会提供运行代码的前台上下文。这说明,即使我们已经切换到另一个应用,服务也能在屏幕上显示toast。

1.8 用通知在屏幕上显示

通知是出现在屏幕上方一个列表中的消息。如果创建通知,即使用户没有及时看到也没有关系。用户只需要从屏幕上方向下滑动手指打开导航抽屉,就能看到这些通知。

发送通知时,要使用Android的一个内置服务,也就是通知服务( notification service)

Android提供了很多可以在应用中使用的内置服务。这包括闹钟服务(用于控制闹钟)、下载服务(用来请求HTTP下载),以及位置服务(用来控制位置更新)。我们用通知服务来管理通知。

1.8.1 如何使用通知服务

下面介绍本应用如何使用Android通知服务:

1.MainActivity传入一个意图启动DelayedMessageService.

2.DelayedMessageService创建一个新的Notification对象

这个Notification对象包含应当如何配置通知的有关详细信息,如它的文本、标题和图标。

3.DelayedMessageService创建一个NotifacationManager对象,用来访问Android的通知服务

DelayedMessageService将这个Notification对象传递到NotificationManager来显示通知。


 

1.8.2 创建通知

使用通知生成器创建通知

创建通知时,要用通知生成器创建一个新的Notification对象。利用通知生成器,无需编写太多代码就可以创建包含一组特定特性的通知。每个通知必须包含一个小图标、一个标题和一些文本。

下面给出一个例子,可以用这些代码创建一个通知。这里会显示一个高优先级的通知,显示通知时会振动,单击后通知将消失:

 除了上面的一部分属性,还可以设置另外一些属性,如设置可见性来控制在锁屏时是否显示通知等等。具体看这个网址:

 还可以指定用户单击通知时是否显示活动,这也是一个好主意。例如,在这里,我们可以在单击通知时让Android显示MainActivity

1.8.3 让通知启动一个活动

单击通知时可以使用一个挂起意图(pending intent)让通知启动一个活动。挂起意图是一种特殊的意图,你的应用可以把这个意图传递到其他应用,使这些应用在以后某个时间代表你的应用提交意图(在1.9中进行讲解)

创建挂起意图的步骤如下:

1.创建一个显式意图

首先,创建一个简单的显式意图,指定单击这个通知时想要启动的活动为MainActivity

2.将这个意图传递到TaskStackBuilder

接下来,使用一个TaskStackBuilder确保活动启动时后退按钮能正常工作。TaskStackBuilder允许访问后退按钮使用的活动历史。我们要得到与活动相关的后退堆栈,然后为它增加刚才创建的意图:

3.从TaskStackBuilder得到挂起意图

接下来,从TaskstackBuilder使用getPendingIntent()方法得到挂起意图。getPendingIntent()方法有两个int参数,一个是请求码,可以用来标识这个意图,另一个是标志,用来指定挂起意图的行为

下面给出各个标志选项:

在这里,我们要使用FLAG_UPDATE_CURRENT修改现有的挂起意图。下面给出代码:

4.向通知增加意图

 最后,使用setContentIntent()方法向通知增加这个意图;

 一旦为通知指定了挂起意图,告诉它单击通知时要启动哪个活动,接下来只需要显示这个通知。

1.8.4 使用通知服务发送通知

到目前为止,我们已经了解了如何创建和配置通知。下面要把它传递到Android通知服务,在设备上显示这个通知。

使用getsystemService()方法访问Android的内置服务。这个方法有一个参数,也就是想要使用的服务名。

在这里,我们要使用通知服务,所以可以使用下面的代码:

 NOTIFICATION_ ID用来标识通知。如果发送另一个有相同ID的通知,它会替换当前的通知。如果想用新信息更新一个现有的通知,这就很有用。

通知服务会处理后台服务向屏幕发送更新时有关的所有问题。这说明,你不再需要使用一个处理器更新用户界面;这些工作会由通知服务来处理。

1.8.5 编写代码与问题解决

package com.hfad.joke;

import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.TaskStackBuilder;
import android.content.Context;
import android.content.Intent;
import android.os.Build;

import androidx.annotation.RequiresApi;
// import android.util.Log; // Used in log version
// import android.os.Handler; // Used in Toast version
// import android.widget.Toast; // Used in Toast version

public class DelayedMessageService extends IntentService {

    public static final String EXTRA_MESSAGE = "message";
    // private Handler handler; // Used in Toast version
    public static final int NOTIFICATION_ID = 5453;

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


    @Override
    protected void onHandleIntent(Intent intent) {
        synchronized (this) {
            try {
                wait(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        String text = intent.getStringExtra(EXTRA_MESSAGE);
        showText(text);
    }


    private void showText(final String text) {

        Intent intent = new Intent(this, MainActivity.class);
        TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
        stackBuilder.addParentStack(MainActivity.class);
        stackBuilder.addNextIntent(intent);
        PendingIntent pendingIntent =
                stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
        //1、NotificationManager
        NotificationManager notificationManager =
                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        /** 2、Builder->Notification
         *  必要属性有三项
         *  小图标,通过 setSmallIcon() 方法设置
         *  标题,通过 setContentTitle() 方法设置
         *  内容,通过 setContentText() 方法设置*/
        Notification.Builder  builder = new Notification.Builder(this)
                .setSmallIcon(R.mipmap.ic_launcher)
                .setContentTitle(getString(R.string.app_name))
                .setAutoCancel(true)
                .setPriority(Notification.PRIORITY_MAX)
                .setDefaults(Notification.DEFAULT_VIBRATE)
                .setContentIntent(pendingIntent)
                .setContentText(text);

        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel("001","my_channel",NotificationManager.IMPORTANCE_DEFAULT);
            channel.enableLights(true); //是否在桌面icon右上角展示小红点
            channel.setShowBadge(true); //是否在久按桌面图标时显示此渠道的通知
            notificationManager.createNotificationChannel(channel);
            builder.setChannelId("001");
        }
        Notification notification=builder.build();
        //3、manager.notify()
        notificationManager.notify(NOTIFICATION_ID, notification);
    }
}

需要注意的是:

如果按照书上的代码来执行的话,通知是不会显示出来的。

因为Log日志会爆出警告:

E/NotificationService: No Channel found for pkg=com.hfad.joke, channelId=null, id=5453, tag=null, opPkg=com.hfad.joke, callingUid=10143, userId=0, incomingUserId=0, notificat

查阅资料可知,Android 8.0不能弹出通知,NotificationChannel是Android O新增的通知渠道,其允许您为要显示的每种通知类型创建用户可自定义的渠道。

解决措施:

如果你需要发送属于某个自定义渠道的通知,你需要在发送通知前创建自定义通知渠道,示例代码:

//ChannelId为"001",ChannelName为"my_channel"
NotificationChannel channel = new NotificationChannel("1",
                "my_channel", NotificationManager.IMPORTANCE_DEFAULT);
channel.enableLights(true); //是否在桌面icon右上角展示小红点
channel.setLightColor(Color.GREEN); //小红点颜色
channel.setShowBadge(true); //是否在久按桌面图标时显示此渠道的通知
notificationManager.createNotificationChannel(channel);

//同时,Notification.Builder需要多设置一个
builder.setChannelId("001");

解决该部分问题的参考链接:

​​​​​​Notification Android8.0中无法发送通知,提示:No Channel found for pkg_Errol_King的博客-CSDN博客_android 无法发送通知

 Android中的Notification_Errol_King的博客-CSDN博客_notification

1.8.6 运行代码结果

运行过程分析:

1.MainActivity会启动DelayedMessageService,传入一个意图。这个意图包含MainActivity希望DelayedMessageService显示的一个消息。

2.DelayedMessageService等待10秒

3.DelayedMessageService为MainActivity创建一个意图。

 

4.DelayedMessageService创建一个TaskStackBuilder,让它将这个意图增加到MainActivity的后退堆栈。

5.TaskStackBuilder使用这个意图创建一个挂起意图,把它传递到DelayedMessageService.

6. DelayedMessageService创建一个Notifieation对象,设置这个对象的详细配置信息,传入挂起意图。

7. DelayedMessageService创建一个NotificationManager对象来访问Android的通知服务,传入Notification。

通知服务会显示这个通知。 

 8.用户单击Notification时,Notification使用它的挂起意图启动 MainActivity。

 

虚拟机运行:

1.9 挂起意图PendingIntent

1.9.1 与Intent的区别

  • Intent 是意图的意思。Android 中的 Intent 正是取自这个意思,它是一个消息对象,通过它,Android 系统的四大组件能够方便的通信,并且保证解耦。

Intent 可以说明某种意图,携带一种行为和相应的数据,发送到目标组件。

Intent 是及时启动,intent 随所在的activity 消失而消失

  • PendingIntent是对Intent的封装,但它不是立刻执行某个行为,而是满足某些条件或触发某些事件后才执行指定的行为,比如在通知Notification中用于跳转页面,但不是马上跳转。

Pendingintent,一般用在 Notification上,可以理解为延迟执行的intent,PendingIntent是对Intent一个包装。

A组件创建了一个 PendingIntent 的对象然后传给 B组件,B 在执行这个 PendingIntent 的 send 时候,它里面的 Intent 会被发送出去,而接受到这个 Intent 的 C 组件会认为是 A 发的。
B以A的权限和身份发送了这个Intent.

我们的 Activity 如果设置了 exported = false,其他应用如果使用 Intent 就访问不到这个 Activity,但是使用 PendingIntent 是可以的。

即:PendingIntent将某个动作的触发时机交给其他应用;让那个应用代表自己去执行那个动作(权限都给他)

1.9.2 使用场景

关于PendingIntent的使用场景主要用于闹钟、通知、桌面部件。

大体的原理是: A应用希望让B应用帮忙触发一个行为,这是跨应用的通信,需要 Android 系统作为中间人,这里的中间人就是 ActivityManager。 A应用创建建 PendingIntent,在创建 PendingIntent 的过程中,向 ActivityManager 注册了这个 PendingIntent,所以,即使A应用死了,当它再次苏醒时,只要提供相同的参数,还是可以获取到之前那个 PendingIntent 的。当 A 将 PendingIntent 调用系统 API 比如 AlarmManager.set(),实际是将权限给了B应用,这时候, B应用可以根据参数信息,来从 ActivityManager 获取到 A 设置的 PendingInten

1.9.3 获取PendingIntent

关于PendingIntent的实例获取一般有以下五种方法,分别对应Activity、Broadcast、Service

  • getActivity()
  • getActivities()
  • getBroadcast()
  • getService()
  • getForegroundService()

它们的参数都相同,都是四个:Context,requestCode, Intent, flags,分别对应上下文对象、请求码、请求意图用以指明启动类及数据传递、关键标志位。
前面三个参数共同标志一个行为的唯一性,而第四个参数flags:

  • FLAG_CANCEL_CURRENT:如果当前系统中已经存在一个相同的PendingIntent对象,那么就将先将已有的PendingIntent取消,然后重新生成一个PendingIntent对象。
  • FLAG_NO_CREATE:如果当前系统中不存在相同的PendingIntent对象,系统将不会创建该PendingIntent对象而是直接返回null,如果之前设置过,这次就能获取到。
  • FLAG_ONE_SHOT:该PendingIntent只作用一次。在该PendingIntent对象通过send()方法触发过后,PendingIntent将自动调用cancel()进行销毁,那么如果你再调用send()方法的话,系统将会返回一个SendIntentException。
  • FLAG_UPDATE_CURRENT:如果系统中有一个和你描述的PendingIntent对等的PendingInent,那么系统将使用该PendingIntent对象,但是会使用新的Intent来更新之前PendingIntent中的Intent对象数据,例如更新Intent中的Extras

1.9.4 参考链接

​​​​​​Intent和PendingIntent的区别_笑对生活_展望未来的博客-CSDN博客_pendingintent

Android基础——PendingIntent理解_者文的博客-CSDN博客_pendingintent

1.10 异步消息处理机制-Handler

启动服务在后台无限期地运行,即使启动这个服务的活动撤销,也不会对启动服务有影响。一旦操作完成,服务会自行停止。

绑定服务会绑到另一个组件,如一个活动。这个活动可以与绑定的服务交互,发送请求以及得到结果。为了查看绑定服务的实际工作,下面要创建一个新应用,其中使用一个绑定服务,它类似于记录车辆行驶距离的里程表。

2.绑定服务

2.1 绑定服务使用场景引入

启动服务在后台无限期地运行,即使启动这个服务的活动撤销,也不会对启动服务有影响。一旦操作完成,服务会自行停止。

绑定服务会绑到另一个组件,如一个活动。这个活动可以与绑定的服务交互,发送请求以及得到结果。为了查看绑定服务的实际工作,下面要创建一个新应用,其中使用一个绑定服务,它类似于记录车辆行驶距离的里程表

2.1.1 实现分析

里程表应用如何工作:

我们要创建一个新工程,包含一个活动MainActivity,另外包含一个服务,名为OdometerService。MainActivity将使用OdometerService得到已走过的距离。

1.MainActivity绑定到OdometerService。MainActivity使用OdometerService  getMiles()方法来请求走过的里程数。
2.OdometerServiee使用Android的位置服务跟踪设备的移动情况。它使用这些位置来计算设备走过多远的距离。
3.OdometerService将走过的距离返回MainActivity。MainActivity向用户显示走过的距离。

2.1.2 实现步骤

创建OdometerService需要完成下面的步骤:

1.定义一个OdometerBinder

Binder对象允许活动绑定到服务。我们将定义Binder的一个子类,名为OdometerBinder,利用这个对象可以把活动连接到OdometerService.

2.创建一个LocationListener,向Android的位置服务注册这个监听器。

这会允许OdometerService监听设备位置的改变,并计算已经走过的里程数。

3.创建一个公共getMiles()方法。

活动可以使用这个方法得到走过的里程数。

2.1.3 绑定如何工作

创建了一个Service之后,Adnroid Studio会自动创建绑定服务的代码:

onBind()方法用于将服务与一个活动绑定。

绑定如何工作: 

1.活动创建一个ServiceConnection对象。

ServiceConnection用来建立与服务的连接。

 

2.活动通过这个连接将一个意图传递到服务。

这个意图包含活动要传递到服务的所有额外信息

3.绑定服务创建一个Binder对象。

Binder包含这个绑定服务的一个引用。服务通过连接发回Binder.

4.活动接收到这个Binder后,取出Service对象,开始直接使用这个服务

 

要让活动与服务绑定,服务需要创建Binder对象,使用onBind()方法把它传递到活动

2.1.4 定义Binder

活动使用服务连接请求绑定到一个服务时,这个连接会调用服务的onBind()方法。onBind()方法向连接返回一个Binder。然后把这个对象传回活动

创建一个绑定服务时,要自己定义Binder,此处定义名为OdometerBinder的Binder,在这里把它声明为一个内部类:

活动使用getOdometer()方法得到OdometerService的引用。

 然后在服务的onBind()方法中返回OdometerBinder的一个实例:

活动通过一个服务连接绑定到服务时,这个连接会调用onBind()方法,它会返回odometerBinder对象。活动从连接接收到odometerBinder时,将使用getodometer()方法得到odometerService对象

2.1.5 让服务做些工作

要让服务能告诉活动这个设备走了多远的距离,需要做两件事情:

1.创建服务时要创建一个监听器,监听设备位置的改变

2.只要活动有请求,就向活动返回走过的里程数。

2.2 Service类的关键方法

Service类提供了4个很可能用到的关键方法:

方法何时调用用途
onCreate()第一次创建服务时一次性设置过程,如实例化
onStartCommand()活动使用startService()方法启动服务时如果不是启动服务,不需要事先这个方法。只有当服务是用startService()启动时才会运行这个方法
onBind()活动要绑定到服务时必须实现这个方法,返回一个IBinder方法;如果不希望活动绑定到这个服务,则返回null
onDestroy()服务不再使用,将被撤销时可以使用这个方法清理占用的资源

2.3 位置更新的功能实现

2.3.1 创建位置监听器LocationListener

如果想得到设备的位置,需要使用Android位置服务。位置服务会使用GPS系统提供的信息,另外利用附近WiFi的名字和信号强度来找出你的地表位置。

首先创建一个LocationListener。位置监听器可以在设备位置改变时得到位置的更新情况。如下创建位置监听器:

为了跟踪位置间的距离,需要覆盖LocationListener  onLocationchanged()方法。这个方法有一个参数,这是一个Location对象,表示设备当前的位置。

可以使用Location  distanceTo()方法得到两个位置之间的距离(单位为米)。例如,如果使用一个名为lastLoca-tion的Location对象记录设备的上一个位置,可以使用下面的代码得到两个位置之间的距离(单位为米):

希望创建服务时就开始得到位置的更新,由于这个是一次性设置,因此在onCreate()方法中实现位置监听器的设计。

package com.hfad.odometer;

import android.app.Service;
import android.content.Intent;
import android.location.Location;
import android.location.LocationListener;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;

import androidx.annotation.NonNull;

public class OdometerService extends Service {

    private final IBinder binder=new OdometerBinder();
    //将已经走过的距离(单位:m)以及上一个位置存储为静态私有变量
    private static double distanceInMeters;
    private static Location lastLocation=null;


    public class OdometerBinder extends Binder {
        OdometerService getOdometer(){
            return OdometerService.this;
        }
    }

    public OdometerService() {
    }

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

    @Override
    public void onCreate() {
        LocationListener listener=new LocationListener() {//创建监听器
            @Override
            public void onLocationChanged(@NonNull Location location) {
                if (lastLocation == null){
                    lastLocation=location;
                }
                distanceInMeters+=location.distanceTo(lastLocation);
                lastLocation=location;
            }

            @Override
            public void onStatusChanged(String provider, int status, Bundle extras) {

            }

            @Override
            public void onProviderEnabled(@NonNull String provider) {

            }

            @Override
            public void onProviderDisabled(@NonNull String provider) {

            }
        };
    }
}

2.3.2 注册LocationListener

要用一个LocationManager对象向Android位置服务注册位置监听器。可以利用位置管理器访问位置服务,如下创建一个位置管理器:

LocationManager locManager=(LocationManager) getSystemService(Context.LOCATION_SERVICE);

getSystemService()方法返回一个系统级服务的引用。在这里,要使用到Android的位置服务,因此需要使用下面的代码:

getSystemService(Context.LOCATION_SERVICE);

一旦得到位置管理器,可以用它的requestLocationUpdates()方法向位置服务注册位置监听器,并指定规则来限制监听器的监听频率(也就是多久监听一次位置的更新)

requestLocationUpdates()方法有4个参数:

  • 一个GPS提供者
  • 位置更新的最小时间间隔(单位为毫秒)
  • 位置更新的最小距离(单位为米)
  • LocationListener

下面的代码展示了如何使用这个方法在设备移动距离超过1米时每秒得到一次更新:

可以在Service onCreate()方法中使用这个方法向位置服务注册我们创建的监听器,确保它定期得到位置的更新。下面给出代码:

 利用上面的代码就可以向位置服务注册监听器,让它跟踪所走过的距离。

接下来,我们要让它向活动报告这个距离

2.3.3 告诉活动设备走过的距离

需要让服务做两件事:

  1. 第一件事是让它跟踪设备走过多远的距离。现在已经做到了这一点,为此我们创建了一个位置监听器,并向位置服务注册这个监听器。<服务可以通过监听器来得知当前走过的距离>
  2. 第二件事是让服务告诉活动设备已经走了多远的距离,活动才能进一步告诉用户。为此,我们要在服务中创建一个简单的getMiles()方法,把当前走过的距离转换为英里数。只要想知道距离,活动就会调用这个方法。<服务将得到的距离 传递给活动>

 getMiles()方法如下:

2.3.4 完整的OdometerService代码

package com.hfad.odometer;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;

public class OdometerService extends Service {

    private final IBinder binder = new OdometerBinder();
    //将已经走过的距离(单位:m)以及上一个位置存储为静态私有变量
    private static double distanceInMeters;
    private static Location lastLocation = null;

    public class OdometerBinder extends Binder {
        OdometerService getOdometer() {
            return OdometerService.this;
        }
    }

    public OdometerService() {
    }

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

    @RequiresApi(api = Build.VERSION_CODES.M)
    @SuppressLint("ServiceCast")
    @Override
    public void onCreate() {
        //创建服务时建立位置监听器
        LocationListener listener = new LocationListener() {//创建监听器
            @Override
            public void onLocationChanged(@NonNull Location location) {
                if (lastLocation == null) {
                    lastLocation = location;
                }
                distanceInMeters += location.distanceTo(lastLocation);
                lastLocation = location;
            }

            @Override
            public void onStatusChanged(String provider, int status, Bundle extras) {

            }

            @Override
            public void onProviderEnabled(@NonNull String provider) {

            }

            @Override
            public void onProviderDisabled(@NonNull String provider) {

            }
        };
        LocationManager locManager = (LocationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            // TODO: Consider calling
            //    Activity#requestPermissions
            // here to request the missing permissions, and then overriding
            //   public void onRequestPermissionsResult(int requestCode, String[] permissions,
            //                                          int[] grantResults)
            // to handle the case where the user grants the permission. See the documentation
            // for Activity#requestPermissions for more details.
            return;
        }
        locManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 1, listener);
    }

    //将走过的距离转换为英里数并返回
    public double getMiles(){
        return  this.distanceInMeters/1609.344;
    }
}

2.3.5 权限授权--更新AndroidManifest.xml

创建应用时,Android会默认允许完成大多数动作。不过,有些动作需要用户授权才能完成。其中就包括使用设备GPS。如果你的应用需要使用设备GPS,安装应用时就要得到用户的授权

要使用<uses-permission>元素告诉Android:这个应用需要用户授权来使用GPS,如下所示:

 如果AndroidManifest.xml中没有包含这个授权,应用将会崩溃。

还要检查Android Studio是否已经把你的服务增加到AndroidManifest.xml :

完整的AdnroidManifest.xml中开启权限后为:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.hfad.odometer">

    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <service
            android:name=".OdometerService"
            android:enabled="true"
            android:exported="false"></service>

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

2.4 将活动与服务绑定

2.4.1 任务需求

1.MainActivity绑定到OdometerService。MainActivity使用OdometerService getMiles()方法请求走过的英里数。
2.OdometerService使用Android位置服务跟踪设备的移动情况。使用这些位置来计算设备移动的距离。
3.OdometerServiee将走过的距离返回给MainActivity。MainActivity向用户显示走过的距离。

2.4.2 更新MainActivity的布局

 我们要让MainActivity使用这个服务显示设备走过的英里数,所以首先要更新布局文件activity_main.xml。要为布局增加一个文本视图,用来显示里程。我们将在Java代码中让这个文本视图每秒更新一次

2.4.3 将活动与服务连接--创建ServiceConnection

活动使用一个ServiceConnection对象绑定到服务。ServiceConnection是一个接口,包括两个方法:onServiceConnected()和onServiceDisconnected().

 

建立与服务的连接时会调用onServiceConnected()方法,将从服务接收到一个Binder对象。可以使用这个绑定器得到服务的一个引用

失去服务连接时会使用onServiceDisconnected()方法。

要让一个活动绑定某个服务,需要创建你自己的ServiceConnection实现。下面给出我们的serviceConnection实现:

package com.hfad.odometer;

import androidx.appcompat.app.AppCompatActivity;

import android.content.ComponentName;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;

public class MainActivity extends AppCompatActivity {

    private OdometerService odometer;
    private boolean bound=false;//存储活动是否绑定到服务

    private ServiceConnection connection=new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder binder) {
            OdometerService.OdometerBinder odometerBinder=
                    (OdometerService.OdometerBinder)binder;
            //将Binder强制转换为一个OdometerBinder,然后用它得到OdometerService的一个引用。
            odometer=odometerBinder.getOdometer();
            bound=true;//连接服务时,设置Bound为true
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            bound=false;//服务断开连接时,设置bound为false
        }
    };

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


}

2.4.5 活动启动时绑定到服务

我们要使用服务连接在活动变为可见时绑定到服务。应该还记得,活动可见时会调用它的onStart()方法。

要绑定到服务,首先要创建一个显式意图,指定你想要绑定的服务。然后使用活动的bindservice()方法绑定到服务:

 如果服务还不存在,代码context.BIND_AUTO_CREATE告诉Android要创建这个服务。

2.4.6 活动停止时解除服务绑定

活动不再可见时,我们要解除服务绑定。活动不可见时会调用它的onstop()方法。

要用unbindService()连接解除服务绑定。这个方法有一个参数:也就是服务连接。我们要检查活动不可见时服务是否绑定,如果是绑定的,则要解除绑定:

2.4.7 显示走过的距离 

一旦有了服务的连接,接下来可以调用它的方法。我们要每秒调用一次odometerservicegetMiles()方法来得到走过的距离,然后使用这个距离更新布局中的文本视图。需要每秒调用一次getMiles()方法,并在每次调用这个方法时更新文本视图。

为了做到这一点,我们要编写一个新方法,名为watchMileage()。它的工作与第4章使用的runTimer()方法很类似。唯一的区别是这里会显示走过的里程数而不是经过的时间。

 private void watchMileage(){
        final TextView distanceView = (TextView) findViewById(R.id.distance);
        final Handler handler=new Handler();
        handler.post(new Runnable() {
            @Override
            public void run() {
                double distance=0.0;
                if (odometer!=null){
                    distance=odometer.getMiles();
                }
                String distanceStr=String.format("%1$.2f  miles",distance );
                distanceView.setText(distanceStr);
                handler.postDelayed(this,1000);
            }
        });
    }

然后在活动的onCreate()方法中调用这个方法,这样一来,活动创建时就会运行这个方法。

在运行时报错:

java.lang.SecurityException: "gps" location provider requires ACCESS_FINE_LOCATION permission.
这个需要在安装好应用之后,通过设置-应用权限管理,运行使用定位服务。

3.本章总结

  1. 服务是可以在后台完成任务的组件。服务没有用户界面。
  2. 启动服务可以在后台无限期地运行,甚至启动这个服务的活动已经撤销也不会有影响。一旦操作完成,它会自行停止。
  3. 要使用<service >元素在AndroidManifest .xml中声明服务。
  4. 可以扩展IntentService类并覆盖它的onHandleIntent()方法创建一个简单的启动服务。Intentservice类设计用来处理意图。
  5. 可以使用startService()服务启动一个启动服务。
  6. 如果覆盖IntentService  StartCommand()方法,必须调用它的超类实现。
  7. 可以使用通知生成器创建一个通知。使用挂起意图让通知启动一个活动。然后使用Android的通知服务来显示通知。绑定服务绑定到另一个组件(如一个活动)。活动可以与它交互,并得到结果。
  8. 通常通过扩展Service类来创建绑定服务。必须定义你自己的Binder对象,并覆盖onBind()方法。组件希望绑定到服务时会调用这个方法。
  9. 创建服务时会调用Service onCreate()方法。使用这个方法来完成实例化。
  10. 服务将要撤销时会调用ServiceonDestroy()方法。
  11. 可以使用Android位置服务来得到设备的当前位置。要创建一个LocationListener,然后向位置服务注册这个监听器。可以增加相应的规则,指定监听器多久得到一次
  12. 变更通知。使用设备GPS时,需要在AndroidManifest.xml中为它增加一个授权。
  13. 要将一个活动绑定到一个服务,需要创建一个serviceconnection覆羔onServiceConnected()方法来得到服务的一个引用。
  14. 使用bindservice()方法绑定到服务。使用unbindservice()方法与服务解除绑定。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值