The Android Developer Challenge is back! Submit your idea before December 2.

接收定期位置信息更新

如果您的应用可以持续跟踪位置,那么就可以向用户提供更相关的信息。例如,如果应用可以在用户步行或驾车时帮助他们找到路,或者如果应用会跟踪资产的位置,那么就需要定期获取设备的位置信息。除了地理位置(纬度和经度)之外,您可能还需要向用户提供其他信息,如设备的方位(水平行进方向)、高度或速度。Location 对象中提供了这些信息以及更多信息,应用可以从一体化位置信息提供器中检索这些信息。

虽然您可以使用 getLastLocation() 获取设备的位置信息(如获取最近一次的已知位置信息一课中所述),但是一种更直接的方法是向一体化位置信息提供器请求定期更新信息。作为响应,API 会根据 WLAN 和 GPS(全球定位系统)等当前可用的位置信息提供器,用可用的最佳位置信息定期更新应用。位置信息的准确性由提供器、您已请求的位置信息权限以及您在位置信息请求中设置的选项决定。

这节课将介绍如何在一体化位置信息提供器中使用 requestLocationUpdates() 方法请求对设备的位置信息进行定期更新。

声明权限

使用位置信息服务的应用必须请求位置信息权限。在大多数情况下,您可以请求粗略位置权限,但仍然可以从可用的位置信息提供器那获得还算准确的位置信息。

以下代码段演示了如何请求粗略位置信息权限:

    <manifest ... >
      <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    </manifest>
    
面向用户对话框的屏幕截图
图 1. 当应用请求位置信息时显示的对话框,其中包含选项:仅当该应用在使用中时才允许访问位置信息

在搭载 Android 10(API 级别 29)或更高版本的设备上,用户会看到图 1 所示的对话框,表明应用正在请求位置权限。如果用户在此对话框中允许应用访问设备的位置信息,则仅当用户正在与应用进行交互时,应用才能访问位置信息,而当它在后台运行时则不能访问。您可以声明一项前台服务,使应用能在用户将其置于后台后获取位置详细信息以继续执行用户发起的操作

注意:虽然当应用在 Android 10 或更高版本的系统上运行时可以请求在后台访问位置信息,但强烈建议您不要这样做。

获取最近一次的已知位置

设备最近一次的已知位置提供了一个方便的起点,可确保应用在开始定期位置信息更新之前具有已知位置。获取最近一次的已知位置一课将向您介绍如何通过调用 getLastLocation() 获取最近一次的已知位置。下面几部分中的代码段假定您的应用已检索到最近一次的已知位置,并将其作为 Location 对象存储在全局变量 mCurrentLocation 中。

请求位置信息更新

在请求位置信息更新之前,应用必须连接到位置信息服务并发出位置信息请求。更改位置信息设置一课将向您介绍如何执行此操作。发出位置信息请求后,即可通过调用 requestLocationUpdates() 开始定期更新。

根据请求的形式,一体化位置信息提供器要么调用 LocationCallback.onLocationResult() 回调方法并向其传递 Location 对象的列表,要么发出一个在扩展数据中包含位置信息的 PendingIntent。更新的准确性和频率受您已请求的位置信息权限以及您在位置信息请求对象中设置的选项的影响。

本课将介绍如何使用 LocationCallback 回调方法获取更新。调用 requestLocationUpdates(),并向其传递 LocationRequest 对象的实例和 LocationCallback。定义一个 startLocationUpdates() 方法,如以下代码示例所示:

Kotlin

    override fun onResume() {
        super.onResume()
        if (requestingLocationUpdates) startLocationUpdates()
    }

    private fun startLocationUpdates() {
        fusedLocationClient.requestLocationUpdates(locationRequest,
                locationCallback,
                null /* Looper */)
    }
    

Java

    @Override
    protected void onResume() {
        super.onResume();
        if (requestingLocationUpdates) {
            startLocationUpdates();
        }
    }

    private void startLocationUpdates() {
        fusedLocationClient.requestLocationUpdates(locationRequest,
                locationCallback,
                null /* Looper */);
    }
    

请注意,上面的代码段引用了布尔标记 requestingLocationUpdates,该标记用于跟踪用户已开启还是已关闭位置信息更新。如果用户已关闭位置信息更新,您可以告知他们您的应用要求访问位置信息。如需详细了解如何在 Activity 实例间保留该布尔标记的值,请参阅保存 Activity 的状态

定义位置信息更新回调

一体化位置信息提供器会调用 LocationCallback.onLocationResult() 回调方法。传入参数包含 Location 对象列表,其中包含位置的纬度和经度。以下代码段展示了如何实现 LocationCallback 接口并定义该方法,然后获取位置信息更新的时间戳,并在应用的界面上显示纬度、经度和时间戳:

Kotlin

    private lateinit var locationCallback: LocationCallback

    // ...

    override fun onCreate(savedInstanceState: Bundle?) {
        // ...

        locationCallback = object : LocationCallback() {
            override fun onLocationResult(locationResult: LocationResult?) {
                locationResult ?: return
                for (location in locationResult.locations){
                    // Update UI with location data
                    // ...
                }
            }
        }
    }
    

Java

    private LocationCallback locationCallback;

    // ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ...

        locationCallback = new LocationCallback() {
            @Override
            public void onLocationResult(LocationResult locationResult) {
                if (locationResult == null) {
                    return;
                }
                for (Location location : locationResult.getLocations()) {
                    // Update UI with location data
                    // ...
                }
            };
        };
    }
    

请求在后台访问位置信息

如果应用以 Android 10 或更高版本为目标平台,则您必须在应用的清单文件中声明 ACCESS_BACKGROUND_LOCATION 权限并接收用户权限,才能在应用位于后台时接收定期位置信息更新。

以下代码段展示了如何在应用中请求在后台访问位置信息:

    <manifest ... >
      <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
      <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
    </manifest>
    
面向用户对话框的屏幕截图
图 2. 当应用请求位置信息时显示的对话框,其中包含选项:始终允许访问位置信息(包括在后台时)

在搭载 Android 10(API 级别 29)或更高版本的设备上,用户会看到图 2 所示的对话框,表示应用正在请求位置信息权限,而且请求始终访问位置信息(包括在后台时)。此对话框包含的一个选项是只有正在使用应用时用户才允许该应用访问位置信息;如果用户选择此选项,则应用在后台时无法访问位置信息。如果应用的工作流要求始终都能访问位置信息,则您应告知用户应用需要在后台访问位置信息

注意:即使用户最初允许应用在后台访问位置信息,日后也可以在系统设置中撤消此权限。用户可以选择只有他们正在使用您的应用时才允许应用访问位置信息,也可以选择根本不允许应用访问位置信息。

因此,每当您的应用启动一项服务时,都要检查用户是否仍允许您的应用在后台访问位置信息。

提醒用户应用在后台访问位置信息

系统通知的屏幕截图
图 3. 提醒用户他们已授权某个应用“始终”访问设备位置信息的通知

用户可以选择允许您的应用始终都能访问设备位置信息。用户做出此选择后,当您的应用首次在后台访问设备位置信息时,系统会安排向用户发送一条通知。此通知旨在提醒用户他们已允许您的应用始终都能访问设备位置信息。示例通知如图 3 所示。

告知用户应用要求在后台访问位置信息

如果用户已要求只有他们正在使用您的应用时才允许应用访问位置信息,那么可以显示一个自定义对话框来提醒用户:如果不是始终都能访问位置信息,您的应用中的某个工作流便无法正常运行。

注意:用户可以选择既拒绝应用访问设备的位置信息,又阻止应用以后请求访问设备的位置信息。应用应尊重并处理用户的“拒绝,不要再询问”决定。

用户确认此对话框后,您可以请求在后台访问位置信息,此时会出现如图 4 所示的系统对话框:

面向用户对话框的屏幕截图
图 4. 请求用户同意让应用始终都能访问位置信息的对话框

以下代码段中显示了此权限检查逻辑的示例:

Kotlin

    val permissionAccessCoarseLocationApproved = ActivityCompat
        .checkSelfPermission(this, permission.ACCESS_COARSE_LOCATION) ==
        PackageManager.PERMISSION_GRANTED

    if (permissionAccessCoarseLocationApproved) {
       val backgroundLocationPermissionApproved = ActivityCompat
           .checkSelfPermission(this, permission.ACCESS_BACKGROUND_LOCATION) ==
           PackageManager.PERMISSION_GRANTED

       if (backgroundLocationPermissionApproved) {
           // App can access location both in the foreground and in the background.
           // Start your service that doesn't have a foreground service type
           // defined.
       } else {
           // App can only access location in the foreground. Display a dialog
           // warning the user that your app must have all-the-time access to
           // location in order to function properly. Then, request background
           // location.
           ActivityCompat.requestPermissions(this,
               arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
               your-permission-request-code
           )
       }
    } else {
       // App doesn't have access to the device's location at all. Make full request
       // for permission.
       ActivityCompat.requestPermissions(this,
           arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION,
                   Manifest.permission.ACCESS_BACKGROUND_LOCATION),
           your-permission-request-code
       )
    }
    

Java

    boolean permissionAccessCoarseLocationApproved =
        ActivityCompat.checkSelfPermission(this, permission.ACCESS_COARSE_LOCATION)
            == PackageManager.PERMISSION_GRANTED;

    if (permissionAccessCoarseLocationApproved) {
       boolean backgroundLocationPermissionApproved =
               ActivityCompat.checkSelfPermission(this,
                   permission.ACCESS_BACKGROUND_LOCATION)
                   == PackageManager.PERMISSION_GRANTED;

       if (backgroundLocationPermissionApproved) {
           // App can access location both in the foreground and in the background.
           // Start your service that doesn't have a foreground service type
           // defined.
       } else {
           // App can only access location in the foreground. Display a dialog
           // warning the user that your app must have all-the-time access to
           // location in order to function properly. Then, request background
           // location.
           ActivityCompat.requestPermissions(this, new String[] {
               Manifest.permission.ACCESS_BACKGROUND_LOCATION},
               your-permission-request-code);
       }
    } else {
       // App doesn't have access to the device's location at all. Make full request
       // for permission.
       ActivityCompat.requestPermissions(this, new String[] {
            Manifest.permission.ACCESS_COARSE_LOCATION,
            Manifest.permission.ACCESS_BACKGROUND_LOCATION
            },
            your-permission-request-code);
    }
    

继续执行用户发起的操作

您的应用可能会提供与位置信息有关的工作流,如在驾车时使用精细导航或在跑步时跟踪道路。当用户执行这些类型的任务时,您的应用通常需要在被置于后台后(如当用户按设备上的主屏幕按钮或关闭设备的显示屏时)访问设备的位置信息。

要在这种特定类型的用例中保留对设备位置信息的访问权,请启动您已在应用的清单中声明前台服务类型"location" 的前台服务:

    <service
        android:name="MyNavigationService"
        android:foregroundServiceType="location" ... >
        ...
    </service>
    

在启动该前台服务之前,请确保您的应用仍可访问设备的位置信息:

Kotlin

    val permissionAccessCoarseLocationApproved = ActivityCompat
        .checkSelfPermission(this, permission.ACCESS_COARSE_LOCATION) ==
        PackageManager.PERMISSION_GRANTED

    if (permissionAccessCoarseLocationApproved) {
       // App has permission to access location in the foreground. Start your
       // foreground service that has a foreground service type of "location".
    } else {
       // Make a request for foreground-only location access.
       ActivityCompat.requestPermissions(this,
           arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
           your-permission-request-code
       )
    }
    

Java

    boolean permissionAccessCoarseLocationApproved =
        ActivityCompat.checkSelfPermission(this,
            permission.ACCESS_COARSE_LOCATION) ==
            PackageManager.PERMISSION_GRANTED;

    if (permissionAccessCoarseLocationApproved) {
        // App has permission to access location in the foreground. Start your
        // foreground service that has a foreground service type of "location".
    } else {
       // Make a request for foreground-only location access.
       ActivityCompat.requestPermissions(this, new String[] {
            Manifest.permission.ACCESS_COARSE_LOCATION},
           your-permission-request-code);
    }
    

停止位置信息更新

您应考虑当 Activity 不再获得焦点时(如当用户切换到另一个应用或切换到同一应用中的另一个 Activity 时)是否要停止位置信息更新。这样便于减少耗电量,前提是即使应用在后台运行,也不需要收集信息。本部分介绍如何在 Activity 的 onPause() 方法中停止更新。

要停止位置信息更新,请调用 removeLocationUpdates(),并向其传递 LocationCallback,如以下代码示例所示:

Kotlin

    override fun onPause() {
        super.onPause()
        stopLocationUpdates()
    }

    private fun stopLocationUpdates() {
        fusedLocationClient.removeLocationUpdates(locationCallback)
    }
    

Java

    @Override
    protected void onPause() {
        super.onPause();
        stopLocationUpdates();
    }

    private void stopLocationUpdates() {
        fusedLocationClient.removeLocationUpdates(locationCallback);
    }
    

使用布尔属性 mRequestingLocationUpdates 跟踪当前是否开启了位置信息更新。在 Activity 的 onResume() 方法中,检查位置信息更新当前是否处于活跃状态,如果未处于活跃状态,请将其激活:

Kotlin

    override fun onResume() {
        super.onResume()
        if (requestingLocationUpdates) startLocationUpdates()
    }
    

Java

    @Override
    protected void onResume() {
        super.onResume();
        if (requestingLocationUpdates) {
            startLocationUpdates();
        }
    }
    

保存 Activity 的状态

设备配置的更改(如屏幕方向或语言的更改)可能会导致当前 Activity 被销毁。因此,应用必须存储重新创建该 Activity 所需的所有信息。要实现此目的,一种方法是使用存储在 Bundle 对象中的实例状态。

以下代码示例展示了如何使用 Activity 的 onSaveInstanceState() 回调保存实例状态:

Kotlin

    override fun onSaveInstanceState(outState: Bundle?) {
        outState?.putBoolean(REQUESTING_LOCATION_UPDATES_KEY, requestingLocationUpdates)
        super.onSaveInstanceState(outState)
    }
    

Java

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        outState.putBoolean(REQUESTING_LOCATION_UPDATES_KEY,
                requestingLocationUpdates);
        // ...
        super.onSaveInstanceState(outState);
    }
    

定义一个 updateValuesFromBundle() 方法,以从上一个 Activity 实例恢复保存的值(如果有)。通过 Activity 的 onCreate() 方法调用上述方法,如以下代码示例所示:

Kotlin

    override fun onCreate(savedInstanceState: Bundle?) {
        // ...
        updateValuesFromBundle(savedInstanceState)
    }

    private fun updateValuesFromBundle(savedInstanceState: Bundle?) {
        savedInstanceState ?: return

        // Update the value of requestingLocationUpdates from the Bundle.
        if (savedInstanceState.keySet().contains(REQUESTING_LOCATION_UPDATES_KEY)) {
            requestingLocationUpdates = savedInstanceState.getBoolean(
                    REQUESTING_LOCATION_UPDATES_KEY)
        }

        // ...

        // Update UI to match restored state
        updateUI()
    }
    

Java

    @Override
    public void onCreate(Bundle savedInstanceState) {
        // ...
        updateValuesFromBundle(savedInstanceState);
    }

    private void updateValuesFromBundle(Bundle savedInstanceState) {
        if (savedInstanceState == null) {
            return;
        }

        // Update the value of requestingLocationUpdates from the Bundle.
        if (savedInstanceState.keySet().contains(REQUESTING_LOCATION_UPDATES_KEY)) {
            requestingLocationUpdates = savedInstanceState.getBoolean(
                    REQUESTING_LOCATION_UPDATES_KEY);
        }

        // ...

        // Update UI to match restored state
        updateUI();
    }
    

如需详细了解如何保存实例状态,请参阅 Android Activity 类参考。

注意:为了实现更持久的存储,您可以将用户的偏好设置存储在应用的 SharedPreferences 中。您可以在 Activity 的 onPause() 方法中设置共享偏好设置,并在 onResume() 方法中检索偏好设置。如需详细了解如何保存偏好设置,请阅读保存键值集

下一课(即显示位置地址)将向您介绍如何显示给定位置的街道地址。

其他资源

要了解详情,请参考以下资源:

示例