如何使用Kotlin在Android中实现地理围栏

596 阅读3分钟

使用Kotlin在Android中实现地理围栏

Geofence是一个模仿的变量,它描述了一个真实的感兴趣的地理区域。Geofencing API允许你定义一个特定区域的轮廓或限制。当用户越过Geofence时,他们会收到通知提醒。

地理围栏API采用了设备传感器的使用,以节省电池的方式检测用户的位置。

地理围栏包括三种过渡类型。

  • 进入- 这表明用户已经进入地理围栏。
  • 居住--表明用户在一定时期内存在于地理围栏内。
  • 退出- 这表明用户已经移出了地理围栏。

前提条件

要跟上这个教程,你应该。

  • 在你的机器上安装最新版本的[Android Studio]。
  • 拥有谷歌地图的基本知识。
  • 拥有[Kotlin]编程语言的基本知识。
  • 能够使用[ViewBinding]。

开始吧

第1步 - 创建一个Android项目

在这一步,我们将创建一个带有谷歌地图活动的Android Studio项目。

确保你已经选择了Google Maps Activity 模板。

New Project

第2步 - 包括所需的依赖项

在你的应用级build.gradle 文件中包括以下依赖项。

implementation 'com.google.android.gms:play-services-maps:17.0.1'
implementation 'com.google.android.gms:play-services-location:18.0.0'

第3步 - 添加所需的权限

要开始使用Geofencing API,用户必须首先允许定位权限。

Android manifest 文件中,添加以下权限。

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

检查权限

在声明该功能之前,确保该应用程序有在前台和后台运行的权限。查一下设备的Android API版本是很有用的。

在你的MainActivity 文件中添加以下代码。

private val gadgetQ = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q

为了确定权限是否已被授予,创建以下函数。

@TargetApi(29)
private fun approveForegroundAndBackgroundLocation(): Boolean {
    val foregroundLocationApproved = (
            PERMISSION_GRANTED == ActivityCompat.checkSelfPermission(
                this, Manifest.permission.ACCESS_FINE_LOCATION
            ))
    val backgroundPermissionApproved =
        if (gadgetQ) {
            PERMISSION_GRANTED == ActivityCompat.checkSelfPermission(
                this, Manifest.permission.ACCESS_BACKGROUND_LOCATION
            )
        } else {
            true
        }
    return foregroundLocationApproved && backgroundPermissionApproved
}

如果设备运行的是Android Q(API 29),确保权限ACCESS_BACKGROUND_LOCATIONACCESS_FINE_LOCATION 被启用。

如果设备运行的是旧的安卓版本,你不需要权限就可以在后台访问用户的位置。

private fun authorizedLocation(): Boolean {
    val formalizeForeground = (
            PERMISSION_GRANTED == ActivityCompat.checkSelfPermission(
                this, Manifest.permission.ACCESS_FINE_LOCATION
            ))
    val formalizeBackground =
        if (gadgetQ) {
            PERMISSION_GRANTED == ActivityCompat.checkSelfPermission(
                this, Manifest.permission.ACCESS_BACKGROUND_LOCATION
            )
        } else {
            true
        }
    return formalizeForeground && formalizeBackground
}

请求后台和精细的位置权限

这是你向用户请求权限的地方,如果没有被授予的话,你可以访问他们的位置。

global scopecompanion object 中添加以下变量。

private val REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE = 3 // random unique value
private val REQUEST_FOREGROUND_ONLY_PERMISSIONS_REQUEST_CODE = 4
private val REQUEST_TURN_DEVICE_LOCATION_ON = 5
private fun askLocationPermission() {
    if (authorizedLocation())
        return
    var grantingPermission = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
    val customResult = when {
        gadgetQ -> {
            grantingPermission += Manifest.permission.ACCESS_BACKGROUND_LOCATION
            REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE
        }
        else -> REQUEST_FOREGROUND_ONLY_PERMISSIONS_REQUEST_CODE
    }
    Log.d(TAG, "askLocationPermission")

    ActivityCompat.requestPermissions(
        this, grantingPermission, customResult
    )
}

一旦用户对权限请求做出回应,你应该在onRequestPermissionsResult() 方法中处理他们的回应,如下图所示。

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)

    if (requestCode == REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE || 
        requestCode == REQUEST_FOREGROUND_ONLY_PERMISSIONS_REQUEST_CODE) {
        if (grantResults.size > 0 && (grantResults[0] == PackageManager.PERMISSION_GRANTED)){
                validateGadgetAreaInitiateGeofence()
            }
    }
}

第4步 - 检查小工具的位置。

如果用户的设备位置被停用,授予的权限将毫无价值。为了验证设备的位置是否被启用,添加以下代码。

检查设备位置设置并启动地理围栏

private fun validateGadgetAreaInitiateGeofence(resolve: Boolean = true) {

    // create a location request that request for the quality of service to update the location
    val locationRequest = LocationRequest.create().apply {
        priority = LocationRequest.PRIORITY_LOW_POWER
    }
    val builder = LocationSettingsRequest.Builder().addLocationRequest(locationRequest)

    // check if the client location settings are satisfied
    val client = LocationServices.getSettingsClient(this)
    
    // create a location response that acts as a listener for the device location if enabled
    val locationResponses = client.checkLocationSettings(builder.build())

    locationResponses.addOnFailureListener { exception ->
        if (exception is ResolvableApiException && resolve) {
            try {
                exception.startResolutionForResult(
                    this, REQUEST_TURN_DEVICE_LOCATION_ON
                )
            } catch (sendEx: IntentSender.SendIntentException) {
                Log.d(TAG, "Error getting location settings resolution: ${sendEx.message}")
            }
        } else {
            Toast.makeText(this, "Enable your location", Toast.LENGTH_SHORT).show()
        }
    }

    locationResponses.addOnCompleteListener {it ->
        if (it.isSuccessful) {
            addGeofence()
        }
    }
}

检查用户是否接受或拒绝了onActivityResult() 方法中的请求。如果他们没有,再次提示他们。

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        validateGadgetAreaInitiateGeofence(false)
    }

第5步 - 添加和删除地理围栏

添加地理围栏 你需要一个继承自PendingIntent 的方法来管理地理围栏的转换。

一个PendingIntent ,既描述了一个intent ,也描述了应该对其进行的action

我们将为一个BroadcastReceiver 定义一个待定意图,以控制Geofence的转换。

 private val geofenceIntent: PendingIntent by lazy {
        val intent = Intent(this, GeofenceBroadcastReceiver::class.java)
        PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
    }

GeofencingClient 是与Geofencing APIs互动的最基本方式。

因此,创建一个GeofencingClient 的实例。

private lateinit var geoClient: GeofencingClient

onCreate() 方法中,初始化geofencingClient

geoClient = LocationServices.getGeofencingClient(this)

还是在onCreate 方法中,添加一个持有geofencesgeofenceList

在这一步中,我们将添加一个地理围栏,但你可以根据自己的意愿来添加。

val latitude = 0.616016
val longitude = 34.521816
val radius = 100f

geofenceList.add(Geofence.Builder()
            .setRequestId("entry.key")
            .setCircularRegion(latitude,longitude,radius)
            .setExpirationDuration(Geofence.NEVER_EXPIRE)
            .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
            .build())

创建一个指定地理围栏的函数,如下图所示。

private fun seekGeofencing(): GeofencingRequest {
    return GeofencingRequest.Builder().apply {
        setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
        addGeofences(geofenceList)
    }.build()
}

要将地理围栏与pendingIntent ,创建一个地理围栏函数,并在其中包括以下实现。

private fun addGeofence(){
    if (ActivityCompat.checkSelfPermission(
            this, Manifest.permission.ACCESS_FINE_LOCATION
        ) != PackageManager.PERMISSION_GRANTED
    ) {
        return
    }
    geofencingClient?.addGeofences(getGeofencingRequest(), geofenceIntent)?.run {
        addOnSuccessListener {
            Toast.makeText(this@MapsActivity, "Geofence(s) added", Toast.LENGTH_SHORT).show()
        }
        addOnFailureListener {
            Toast.makeText(this@MapsActivity, "Failed to add geofence(s)", Toast.LENGTH_SHORT).show()
        }
    }
}

删除一个地理围栏

当不使用时,删除与PendingIntent 相关的任何地理围栏是一个好的做法。我们使用下面的方法来这样做。

private fun removeGeofence(){
    geofencingClient?.removeGeofences(geofenceIntent)?.run {
        addOnSuccessListener {
            Toast.makeText(this@MapsActivity, "Geofences removed", Toast.LENGTH_SHORT).show()
        }
        addOnFailureListener {
            Toast.makeText(this@MapsActivity, "Failed to remove geofences", Toast.LENGTH_SHORT).show()
        }
    }
}

onDestroy 方法中,调用removeGeofence() 函数。

override fun onDestroy() {
    super.onDestroy()
    removeGeofence()
}

第6步 - 创建一个BroadcastReceiver类

Android应用程序可以在设备上发送和接收广播信息。

BroadcastReceiver 侦听Geofence的转换,并在设备进入一个特定的geofence区域时提供一个通知。

BroadcastReceiver的实现方法如下。

class GeofenceBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        val geofencingEvent = GeofencingEvent.fromIntent(intent)
        if (geofencingEvent.hasError()) {
            val errorMessage = GeofenceStatusCodes.getStatusCodeString(geofencingEvent.errorCode)
            Log.e(TAG, errorMessage)
            return
        }

        val geofenceTransition = geofencingEvent.geofenceTransition
        if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER) {
            val triggeringGeofences = geofencingEvent.triggeringGeofences

            // Creating and sending notification
            val notificationManager = ContextCompat.getSystemService(
                context!!, NotificationManager::class.java
            ) as NotificationManager

            notificationManager.sendGeofenceEnteredNotification(context)
        } else {
            Log.e(TAG, "Invalid type transition $geofenceTransition")
        }
    }
}

在你的清单中,添加以下代码来注册BroadCastReceiver。

<application>
 ...
<receiver android:name=".GeofenceBroadcastReceiver"/>
</application>

设置通知

我们使用以下代码设置了一个通知。

private const val NOTIFICATION_ID = 33
private const val CHANNEL_ID = "GeofenceChannel"

fun createChannel(context: Context) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val notificationChannel =
            NotificationChannel(CHANNEL_ID, "Channel1", NotificationManager.IMPORTANCE_HIGH)
        val notificationManager = context.getSystemService(NotificationManager::class.java)
        notificationManager.createNotificationChannel(notificationChannel)
    }
}
// extension function
fun NotificationManager.sendGeofenceEnteredNotification(context: Context) {

    // Opening the notification
    val contentIntent = Intent(context, MapsActivity::class.java)
    val contentPendingIntent = PendingIntent.getActivity(
        context,
        NOTIFICATION_ID,
        contentIntent,
        PendingIntent.FLAG_UPDATE_CURRENT
    )

    // Building the notification
    val builder = NotificationCompat.Builder(context, CHANNEL_ID)
        .setContentTitle(context.getString(R.string.app_name))
        .setContentText("You have entered a geofence area")
        .setSmallIcon(R.drawable.ic_baseline_notifications_24)
        .setPriority(NotificationCompat.PRIORITY_HIGH)
        .setContentIntent(contentPendingIntent)
        .build()

    this.notify(NOTIFICATION_ID, builder)
}

结语

在本文中,我们了解了什么是地理围栏,如何添加和删除地理围栏,使用广播接收器监听地理围栏事件,以及在有人进入地理围栏时显示通知。