Android avançado em Kotlin 04.2: Adicionando geofencing ao seu mapa

1. Bem-vindo

Este tutorial faz parte do curso Android avançado em Kotlin. Você obterá o máximo valor deste curso se trabalhar com os tutoriais em sequência, mas isso não é obrigatório. Todos os tutoriais do curso estão listados na página de destino Android avançado em Kotlin.

Introdução

Bem-vindo à lição Android avançado em Kotlin sobre cercas geográficas!

A API de geofencing permite definir perímetros, também conhecidos como geofences, que circundam áreas de interesse. Seu aplicativo recebe uma notificação quando o dispositivo cruza uma geocerca, o que permite que você forneça uma experiência relevante quando os usuários estão dentro da área "cercada".

Por exemplo, um aplicativo de companhia aérea pode definir uma geocerca em torno de um aeroporto quando uma reserva de voo está perto do horário de embarque. Quando o dispositivo cruza a geocerca, o aplicativo pode enviar uma notificação que leva os usuários a uma atividade que lhes permite obter o cartão de embarque.

A API Geofencing usa sensores de dispositivo para detectar com precisão a localização do dispositivo de uma forma que economize bateria. O dispositivo pode estar em um de três estados, ou tipos de transição, relacionados à geocerca.

Tipos de transição de geocerca:

Enter: Indica que o dispositivo entrou na (s) geocerca (s).

Dwell: Indica que o dispositivo entrou e está residindo dentro da (s) geocerca (s) por um determinado período de tempo.

Exit: Indica que o dispositivo saiu da (s) geocerca (s).

A cerca geográfica tem muitas aplicações, incluindo:

  • Aplicativos de lembrete, onde você pode obter um lembrete quando você se aproxima de um destino. Por exemplo, você recebe um lembrete para pegar uma receita quando chega perto de sua farmácia.
  • Serviços de localização de crianças, em que um pai pode ser notificado se uma criança deixar uma área designada por uma cerca virtual.
  • Registro de presença, onde um empregador pode saber quando seus funcionários chegam no momento em que entram em uma geocerca.
  • Um aplicativo de caça ao tesouro que usa cercas geográficas para marcar o local onde um tesouro está escondido. Ao entrar nesse perímetro, você será notificado de que ganhou. Este é o aplicativo que você criará neste tutorial!

A imagem abaixo mostra os locais da geocerca denotados por marcadores e os raios ao redor deles.

O que você precisará

  • A versão mais recente do Android Studio.
  • Um mínimo de SDK API 29 em seu dispositivo ou emulador. (O aplicativo ainda deve funcionar em níveis de API inferiores, mas pode ter uma aparência diferente.)

O que você já deveria saber

O que você aprenderá

  • Como verificar as permissões do usuário.
  • Como verificar as configurações do dispositivo.
  • Como adicionar receptores de transmissão.
  • Como adicionar geocercas.
  • Como tratar com as transições da geocerca.
  • Como simular locais no emulador.

2. Visão geral do aplicativo

O aplicativo que você criará neste tutorial é um jogo de caça ao tesouro. Este aplicativo é uma caça ao tesouro que dá ao usuário uma pista, e quando o usuário inserir o local correto, o aplicativo irá avisá-lo com a próxima pista, ou uma tela de vitória se ele tiver terminado a caça.

As imagens abaixo mostram uma pista e a tela de vitória.

Observe que o código do jogo atual tem locais de São Francisco codificados, mas você aprenderá como personalizar o jogo criando suas próprias cercas geográficas para levar as pessoas a lugares em sua área.

3. Começando

Para começar, baixe o código:

Baixe o Zip

Como alternativa, você pode clonar o repositório GitHub para o código e alternar para o branch starter-code:

$ git clone https://github.com/googlecodelabs/android-kotlin-geo-fences

4. Tarefa: familiarizando-se com o código

Etapa 1: execute o aplicativo inicial

  1. Execute o aplicativo inicializador em um emulador ou em seu próprio dispositivo. Você deverá ver uma tela inicial com um Android segurando um mapa do tesouro.

Etapa 2: familiarize-se com o código

O aplicativo inicial contém código para ajudá-lo a começar e economizar algum trabalho. Ele contém recurso, layouts, uma atividade, um modelo de vista e um receptor de transmissão que você completará durante esta lição.

Abra as seguintes classes importantes fornecidas para você e familiarize-se com o código:

  • HuntMainActivity.kt é a classe principal na qual você trabalhará. Esta classe contém o código básico para funções que tratam de permissões e para adicionar e remover cercas geográficas.
  • GeofenceViewModel.kt é o ViewModel associado a HuntMainActivity.kt. Esta classe trata com o GeofenceIndex LiveData e determina qual dica deve ser mostrada na tela.
  • NotificationUtils.kt: Quando você entra em uma geocerca, uma notificação é exibida. Esta classe cria e estiliza essa notificação.
  • activity_main.xml atualmente exibe uma imagem de um Android, mas você irá implementá-la para exibir uma dica para levar seus jogadores ao próximo local.
  • GeofenceBroadcastReceiver.kt contém o código de esqueleto para o método onReceive() do BroadcastReceiver. Você irá atualizar o onReceive() método neste tutorial.

5. Tarefa: Solicitando permissões

A primeira coisa que seu aplicativo precisa fazer é obter permissões de localização do usuário. Isso envolve as seguintes etapas de alto nível e será o mesmo para qualquer aplicativo que você criar e que precise de permissões.

  1. Adicione as permissões ao manifesto do Android.
  2. Crie um método que verifique as permissões.
  3. Solicite essas permissões chamando esse método.
  4. Lide com o resultado da solicitação de permissões ao usuário.

Etapa 1: adicionar permissões ao AndroidManifest

A API Geofencing exige que o local seja compartilhado o tempo todo. Se você estiver no Android versão Q ou posterior, precisará solicitar especificamente ao usuário essa permissão.

  1. Abra AndroidManifest.xml.
  2. Adicione permissões para ACCESS_FINE_LOCATION e ACCESS_BACKGROUND_LOCATION acima da etiquetaapplication.
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

Etapa 2: verifique se o dispositivo está executando o Android Q (API 29) ou posterior

Verifique se o dispositivo está executando o Android Q ou posterior. Para dispositivos que executam o Android Q (API 29) ou posterior, você terá que solicitar uma permissão adicional de localização em segundo plano.

  1. Abra HuntMainActivity.kt.
  2. Acima do método onCreate(), adicione uma variável de membro chamada runningQOrLater. Isso verificará qual API o dispositivo está executando.
private val runningQOrLater = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q

Etapa 3: crie um método para verificar as permissões

Em seu aplicativo, você precisa verificar se as permissões foram concedidas e, em caso negativo, solicite-as.

  1. Em HuntMainActivity, substitua o código no método foregroundAndBackgroundLocationPermissionApproved() pelo código abaixo, que é explicado posteriormente.
@TargetApi(29)
private fun foregroundAndBackgroundLocationPermissionApproved(): Boolean {
   val foregroundLocationApproved = (
           PackageManager.PERMISSION_GRANTED ==
           ActivityCompat.checkSelfPermission(this,
               Manifest.permission.ACCESS_FINE_LOCATION))
   val backgroundPermissionApproved =
       if (runningQOrLater) {
           PackageManager.PERMISSION_GRANTED ==
           ActivityCompat.checkSelfPermission(
               this, Manifest.permission.ACCESS_BACKGROUND_LOCATION
           )
       } else {
           true
       }
   return foregroundLocationApproved && backgroundPermissionApproved
}
  • Primeiro, você deve verificar se a permissão ACCESS_FINE_LOCATION foi concedida.
val foregroundLocationApproved = (
           PackageManager.PERMISSION_GRANTED ==
           ActivityCompat.checkSelfPermission(this,
               Manifest.permission.ACCESS_FINE_LOCATION))
  • Se o dispositivo estiver executando o Android Q (API 29) ou superior, verifique se a permissão ACCESS_BACKGROUND_LOCATION foi concedida. Retorne true se o dispositivo estiver executando uma versão inferior a Q, onde você não precisa de permissão para acessar o local em segundo plano.
val backgroundPermissionApproved =
   if (runningQOrLater) {
       PackageManager.PERMISSION_GRANTED ==
       ActivityCompat.checkSelfPermission(
           this, Manifest.permission.ACCESS_BACKGROUND_LOCATION
       )
   } else {
       true
   }
  • Retorne true se as permissões foram concedidas e false se não.
return foregroundLocationApproved && backgroundPermissionApproved

Etapa 4: solicitar permissões

  1. Copie o seguinte código para o método requestForegroundAndBackgroundLocationPermissions(). É aqui que você pede ao usuário para conceder permissões de localização. Cada etapa é explicada nos pontos abaixo.
@TargetApi(29 )
private fun requestForegroundAndBackgroundLocationPermissions() {
   if (foregroundAndBackgroundLocationPermissionApproved())
       return
   var permissionsArray = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
   val resultCode = when {
       runningQOrLater -> {
           permissionsArray += Manifest.permission.ACCESS_BACKGROUND_LOCATION
           REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE
       }
       else -> REQUEST_FOREGROUND_ONLY_PERMISSIONS_REQUEST_CODE
   }
   Log.d(TAG, "Request foreground only location permission")
   ActivityCompat.requestPermissions(
       this@HuntMainActivity,
       permissionsArray,
       resultCode
   )
}
  • Se as permissões já foram concedidas, você não precisa perguntar novamente, então você pode return fora do método.
if (foregroundAndBackgroundLocationPermissionApproved())
   return
  • O permissionsArray contém as permissões a serem solicitadas. Inicialmente, adicione ACCESS_FINE_LOCATION uma vez que é necessário para todos os níveis de API.
var permissionsArray = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
  • Em seguida, você precisa de um resultCode. Este código é diferente se o dispositivo está executando Q (API 29) ou posterior e determina se você precisa verificar se há uma permissão (localização precisa) ou várias permissões (localização fina e em segundo plano) quando o usuário retorna da tela de solicitação de permissão.
  • Adicione uma instrução when para verificar a versão em execução e atribua resultCode a REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE se o dispositivo estiver executando Q (API 29) ou posterior, e REQUEST_FOREGROUND_ONLY_PERMISSIONS_REQUEST_CODE, se não.
val resultCode = when {
   runningQOrLater -> {
       permissionsArray += Manifest.permission.ACCESS_BACKGROUND_LOCATION
       REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE
   }
   else -> REQUEST_FOREGROUND_ONLY_PERMISSIONS_REQUEST_CODE
}
  • Por fim, solicite permissões passando na atividade atual, a matriz de permissões e o código de resultado.
ActivityCompat.requestPermissions(
   this@HuntMainActivity,
   permissionsArray,
   resultCode
)

Etapa 5: manipular permissões

Depois que o usuário responde à solicitação de permissão, você precisa tratar com sua resposta em onRequestPermissionsResult().

  1. Copie este código para o método onRequestPermissionsResult().
override fun onRequestPermissionsResult(
   requestCode: Int,
   permissions: Array<String>,
   grantResults: IntArray
) {
   Log.d(TAG, "onRequestPermissionResult")

   if (
       grantResults.isEmpty() ||
       grantResults[LOCATION_PERMISSION_INDEX] == PackageManager.PERMISSION_DENIED ||
       (requestCode == REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE &&
               grantResults[BACKGROUND_LOCATION_PERMISSION_INDEX] ==
               PackageManager.PERMISSION_DENIED))
   {
       Snackbar.make(
           binding.activityMapsMain,
           R.string.permission_denied_explanation, 
           Snackbar.LENGTH_INDEFINITE
       )
           .setAction(R.string.settings) {
               startActivity(Intent().apply {
                   action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
                   data = Uri.fromParts("package", BuildConfig.APPLICATION_ID, null)
                   flags = Intent.FLAG_ACTIVITY_NEW_TASK
               })
           }.show()
   } else {
       checkDeviceLocationSettingsAndStartGeofence()
   }
}
  • As permissões podem ser negadas de algumas maneiras:
  1. Se a matriz grantResults estiver vazia, a interação foi interrompida e a solicitação de permissão cancelada.
  2. Se o valor do array grantResults em LOCATION_PERMISSION_INDEX tiver PERMISSION_DENIED, isso significa que o usuário negou as permissões de primeiro plano.
  3. Se o código de solicitação for igual a REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE e BACKGROUND_LOCATION_PERMISSION_INDEX for negado, isso significa que o dispositivo está executando Q (API 29) ou superior e que as permissões de segundo plano foram negadas.
if (grantResults.isEmpty() ||
   grantResults[LOCATION_PERMISSION_INDEX] == PackageManager.PERMISSION_DENIED ||
   (requestCode == REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE &&
           grantResults[BACKGROUND_LOCATION_PERMISSION_INDEX] ==
           PackageManager.PERMISSION_DENIED))
  • Este aplicativo tem muito pouco uso se as permissões não forem concedidas, então apresente uma snackbar explicando ao usuário que o aplicativo precisa de permissões de localização para que eles possam jogar.
Snackbar.make(
   binding.activityMapsMain,
   R.string.permission_denied_explanation,
   Snackbar.LENGTH_INDEFINITE
)
   .setAction(R.string.settings) {
       startActivity(Intent().apply {
           action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
           data = Uri.fromParts("package", BuildConfig.APPLICATION_ID, null)
           flags = Intent.FLAG_ACTIVITY_NEW_TASK
       })
   }.show()
  • Caso contrário, as permissões foram concedidas e você pode chamar o método checkDeviceLocationSettingsAndStartGeofence().
else {
   checkDeviceLocationSettingsAndStartGeofence()
}
  1. Execute seu aplicativo! Você deverá ver um pop-up solicitando que você conceda permissões. Escolha Allow all the time ou Allow se você estiver executando uma API inferior a 29.

6. Tarefa: Verificar a localização do dispositivo

Seu código agora pede ao usuário para dar permissões.

No entanto, se a localização do dispositivo do usuário estiver desativada, essa permissão não significará nada.

A próxima coisa a verificar é se a localização do dispositivo está ativada. Nesta etapa, você adicionará um código para verificar se o local do dispositivo de um usuário está habilitado e, se não estiver, exibirá uma atividade onde ele pode ativá-lo usando uma solicitação de localização.

  1. Copie esse código para o método checkDeviceLocationSettingsAndStartGeofence() em HuntMainActivity.kt. As etapas são explicadas nos pontos abaixo.
private fun checkDeviceLocationSettingsAndStartGeofence(resolve:Boolean = true) {
   val locationRequest = LocationRequest.create().apply {
       priority = LocationRequest.PRIORITY_LOW_POWER
   }
   val builder = LocationSettingsRequest.Builder().addLocationRequest(locationRequest)
   val settingsClient = LocationServices.getSettingsClient(this)
   val locationSettingsResponseTask =
       settingsClient.checkLocationSettings(builder.build())
   locationSettingsResponseTask.addOnFailureListener { exception ->
       if (exception is ResolvableApiException && resolve){
           try {
               exception.startResolutionForResult(this@HuntMainActivity,
                   REQUEST_TURN_DEVICE_LOCATION_ON)
           } catch (sendEx: IntentSender.SendIntentException) {
               Log.d(TAG, "Error getting location settings resolution: " + sendEx.message)
           }
       } else {
           Snackbar.make(
               binding.activityMapsMain,
               R.string.location_required_error, Snackbar.LENGTH_INDEFINITE
           ).setAction(android.R.string.ok) {
               checkDeviceLocationSettingsAndStartGeofence()
           }.show()
       }
   }
   locationSettingsResponseTask.addOnCompleteListener {
       if ( it.isSuccessful ) {
           addGeofenceForClue()
       }
   }
}
  • Primeiro, crie um LocationRequest e use-o com um LocationSettingsRequest Builder.
   val locationRequest = LocationRequest.create().apply {
       priority = LocationRequest.PRIORITY_LOW_POWER
   }
   val builder = LocationSettingsRequest.Builder().addLocationRequest(locationRequest)
  • Em seguida, use LocationServices para obter o SettingsClient. Crie um val chamado locationSettingsResponseTask e use-o para verificar as configurações de localização.
val settingsClient = LocationServices.getSettingsClient(this)
val locationSettingsResponseTask =
   settingsClient.checkLocationSettings(builder.build())
  • Como o caso em que você está mais interessado é descobrir se as configurações de localização não estão satisfeitas, adicione um onFailureListener() ao locationSettingsResponseTask.
locationSettingsResponseTask.addOnFailureListener { exception ->
}
  • Verifique se a exceção é do tipo ResolvableApiException e, em caso afirmativo, tente chamar o método startResolutionForResult() para solicitar ao usuário que ligue localização do dispositivo.
if (exception is ResolvableApiException && resolve){
   try {
       exception.startResolutionForResult(this@HuntMainActivity,
           REQUEST_TURN_DEVICE_LOCATION_ON)
   }
  • Se a chamada de startResolutionForResult entrar no bloco catch, imprima um log.
catch (sendEx: IntentSender.SendIntentException) {
   Log.d(TAG, "Error getting location settings resolution: " + sendEx.message)
}
  • Se a exceção não for do tipo ResolvableApiException, apresente uma snackbar que alerte o usuário de que o local precisa ser habilitado para jogar a caça ao tesouro.
else {
   Snackbar.make(
       binding.activityMapsMain,
       R.string.location_required_error, Snackbar.LENGTH_INDEFINITE
   ).setAction(android.R.string.ok) {
       checkDeviceLocationSettingsAndStartGeofence()
   }.show()
}
  • Se a locationSettingsResponseTask for concluída, verifique se ela foi bem-sucedida e adicione uma geocerca como pista.
locationSettingsResponseTask.addOnCompleteListener {
   if ( it.isSuccessful ) {
       addGeofenceForClue()
   }
}
  1. Em onActivityResult(), substitua o código existente pelo código abaixo. Depois que o usuário escolhe se aceita ou negar as permissões de localização do dispositivo, verifica se o usuário optou por aceitar as permissões. Se não, pergunte novamente.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
   super.onActivityResult(requestCode, resultCode, data)
   if (requestCode == REQUEST_TURN_DEVICE_LOCATION_ON) {
       checkDeviceLocationSettingsAndStartGeofence(false)
   }
}
  1. Para testar isso, desligue a localização do dispositivo e execute o aplicativo. Você deverá ver um pop-up conforme mostrado abaixo. Pressione OK.

7. Tarefa: Adicionando geofences

Agora que você concluiu a verificação de que as permissões apropriadas foram concedidas, adicione algumas geocerca!

Etapa 1: criar uma intenção pendente

Você precisa de uma maneira de tratar com as transições da geocerca, o que é feito com um PendingIntent. Um PendingIntent é uma descrição de um Intent e uma ação alvo a ser executada com ele. Você criará uma intenção pendente para um BroadcastReceiver para tratar com as transições da geocerca.

  1. Em HuntMainActivity.kt, acima de onCreate(), adicione uma variável privada chamada geofencePendingIntent do tipo PendingIntent para tratar com o transições de geocerca. Conecte geofencePendingIntent ao GeofenceTransitionsBroadcastReceiver.
private val geofencePendingIntent: PendingIntent by lazy {
   val intent = Intent(this, GeofenceBroadcastReceiver::class.java)
   intent.action = ACTION_GEOFENCE_EVENT
   PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}

Etapa 2: Adicionar um cliente de cerca geográfica

Um GeofencingClient é o principal ponto de entrada para interagir com as APIs de geofencing.

  1. No método onCreate(), instancie o geofencingClient que já está declarado no código inicial.
geofencingClient = LocationServices.getGeofencingClient(this)

Etapa 3: Adicionar geocerca

  1. Copie este código para o método addGeofenceForClue(). Cada etapa é explicada nos pontos abaixo.
private fun addGeofenceForClue() {
   if (viewModel.geofenceIsActive()) return
   val currentGeofenceIndex = viewModel.nextGeofenceIndex()
   if(currentGeofenceIndex >= GeofencingConstants.NUM_LANDMARKS) {
       removeGeofences()
       viewModel.geofenceActivated()
       return
   }
   val currentGeofenceData = GeofencingConstants.LANDMARK_DATA[currentGeofenceIndex]

   val geofence = Geofence.Builder()
       .setRequestId(currentGeofenceData.id)
       .setCircularRegion(currentGeofenceData.latLong.latitude,
           currentGeofenceData.latLong.longitude,
           GeofencingConstants.GEOFENCE_RADIUS_IN_METERS
       )
       .setExpirationDuration(GeofencingConstants.GEOFENCE_EXPIRATION_IN_MILLISECONDS)
       .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
       .build()

   val geofencingRequest = GeofencingRequest.Builder()
       .setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
       .addGeofence(geofence)
       .build()

   geofencingClient.removeGeofences(geofencePendingIntent)?.run {
       addOnCompleteListener {
           geofencingClient.addGeofences(geofencingRequest, geofencePendingIntent)?.run {
               addOnSuccessListener {
                   Toast.makeText(this@HuntMainActivity, R.string.geofences_added,
                       Toast.LENGTH_SHORT)
                       .show()
                   Log.e("Add Geofence", geofence.requestId)
                   viewModel.geofenceActivated()
               }
               addOnFailureListener {
                   Toast.makeText(this@HuntMainActivity, R.string.geofences_not_added,
                       Toast.LENGTH_SHORT).show()
                   if ((it.message != null)) {
                       Log.w(TAG, it.message)
                   }
               }
           }
       }
   }
}
  • Primeiro, verifique se você tem alguma geocerca ativa para sua caça ao tesouro. Se já o faz, não deve adicionar outro, pois somente quer que eles procurem um tesouro de cada vez.
if (viewModel.geofenceIsActive()) return
  • Encontre o currentGeofenceIndex em viewModel. Remova quaisquer geofences existentes, chame geofenceActivated no viewModel e retorne.
val currentGeofenceIndex = viewModel.nextGeofenceIndex()
if(currentGeofenceIndex >= GeofencingConstants.NUM_LANDMARKS){
   removeGeofences()
   viewModel.geofenceActivated()
   return
}
  • Depois de ter o índice da geocerca e saber se ele é válido, obtenha os dados em torno da geocerca, que incluem o id e as coordenadas de latitude e longitude.
val currentGeofenceData = GeofencingConstants.LANDMARK_DATA [currentGeofenceIndex]
  • Construa a geofence usando o construtor da geofence e as informações em currentGeofenceData. Defina a duração da expiração usando a constante definida em GeofencingConstants. Defina o tipo de transição para GEOFENCE_TRANSITION_ENTER. Finalmente, construa a geocerca.
val geofence = Geofence.Builder()
   .setRequestId(currentGeofenceData.id)
   .setCircularRegion(currentGeofenceData.latLong.latitude,
       currentGeofenceData.latLong.longitude,
       GeofencingConstants.GEOFENCE_RADIUS_IN_METERS
   )
   .setExpirationDuration(GeofencingConstants.GEOFENCE_EXPIRATION_IN_MILLISECONDS)
   .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
   .build()
  • Crie a solicitação de geocerca. Defina o gatilho inicial para INITIAL_TRIGGER_ENTER, adicione a geocerca que acabou de construir e, em seguida, construa.
val geofencingRequest = GeofencingRequest.Builder()
   .setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
   .addGeofence(geofence)
   .build()
  • Chame removeGeofences() no geofencingClient para remover quaisquer geofences já associadas ao PendingIntent.
geofencingClient.removeGeofences(geofencePendingIntent)?.run {
}
  • Quando removeGeofences() for concluído, independentemente de seu sucesso ou falha, adicione as novas geofences. Você pode desconsiderar o sucesso ou o fracasso da remoção das geocerca porque, mesmo que a remoção falhe, não afetará a adição de outra geocerca.
addOnCompleteListener {
   geofencingClient.addGeofences(geofencingRequest, geofencePendingIntent)?.run {
          }
}
  • Se a adição das cercas geográficas for bem-sucedida, avise o usuário com um brinde.
addOnSuccessListener {
   Toast.makeText(this@HuntMainActivity, R.string.geofences_added,
       Toast.LENGTH_SHORT)
       .show()
   Log.e("Add Geofence", geofence.requestId)
   viewModel.geofenceActivated()
}
  • Se a adição das geocercas falhar, apresente um brinde diferente, informando ao usuário que houve um problema ao adicionar as geocerca.
addOnFailureListener {
   Toast.makeText(this@HuntMainActivity, R.string.geofences_not_added,
       Toast.LENGTH_SHORT).show()
   if ((it.message != null)) {
       Log.w(TAG, it.message)
   }
}
  1. Execute seu aplicativo. Sua tela deve exibir uma pista e um brinde que informa que a geocerca foi adicionada.

8. Tarefa: Atualizar o receptor de transmissão

Seu aplicativo agora adiciona geofences. No entanto, tente navegar até a Golden Gate Bridge (o local correto para a primeira pista padrão). Nada acontece. Por que é que?

Quando o usuário entra em uma geocerca estabelecida por uma pista, no caso a Ponte Golden Gate, você quer ser avisado, para que possa apresentar a próxima pista. Você pode fazer isso usando um receptor de transmissão que pode receber detalhes sobre os eventos de transição da geocerca.

Os aplicativos Android podem enviar ou receber mensagens de broadcast do sistema Android e outros aplicativos usando Broadcast Receivers. Eles usam o padrão de design publicar-assinar, onde as transmissões são enviadas e os aplicativos podem se registrar para receber transmissões específicas. Quando uma transmissão assinada é enviada, o aplicativo é notificado.

Etapa 1: substituir o método onReceive()

  1. Em GeofenceBroadcastReceiver.kt, encontre a função onReceive() e copie este código para a classe. Cada etapa é explicada nos pontos abaixo.
override fun onReceive(context: Context, intent: Intent) {
   if (intent.action == ACTION_GEOFENCE_EVENT) {
       val geofencingEvent = GeofencingEvent.fromIntent(intent)

       if (geofencingEvent.hasError()) {
           val errorMessage = errorMessage(context, geofencingEvent.errorCode)
           Log.e(TAG, errorMessage)
           return
       }

       if (geofencingEvent.geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER) {
           Log.v(TAG, context.getString(R.string.geofence_entered))
           val fenceId = when {
               geofencingEvent.triggeringGeofences.isNotEmpty() ->
                   geofencingEvent.triggeringGeofences[0].requestId
               else -> {
                   Log.e(TAG, "No Geofence Trigger Found! Abort mission!")
                   return
               }
           }
           val foundIndex = GeofencingConstants.LANDMARK_DATA.indexOfFirst {
               it.id == fenceId
           }
           if ( -1 == foundIndex ) {
               Log.e(TAG, "Unknown Geofence: Abort Mission")
               return
           }
           val notificationManager = ContextCompat.getSystemService(
               context,
               NotificationManager::class.java
           ) as NotificationManager

           notificationManager.sendGeofenceEnteredNotification(
               context, foundIndex
           )
       }
   }
}
  • Um Broadcast Receiver pode receber muitos tipos de ações. Para este aplicativo, você somente precisa saber quando a geocerca foi inserida. Verifique se a ação da intenção é do tipo ACTION_GEOFENCE_EVENT.
if (intent.action == ACTION_GEOFENCE_EVENT) {
}
  • Crie uma variável chamada geofencingEvent e inicialize-a para GeofencingEvent com a intenção passada.
val geofencingEvent = GeofencingEvent.fromIntent(intent)
  • Se houver um erro, você precisa entender o que deu errado. Salve uma variável com a mensagem de erro obtida através do código de erro das cercas geográficas. Registre essa mensagem e retorne do método.
if (geofencingEvent.hasError()) {
   val errorMessage = errorMessage(context, geofencingEvent.errorCode)
   Log.e(TAG, errorMessage)
   return
}
  • Verifique se o tipo geofenceTransition é ENTER.
if (geofencingEvent.geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER) {}
  • Se o array triggeringGeofences não estiver vazio, defina o fenceID para o requestId da primeira geocerca. Você teria apenas uma geocerca ativa por vez, portanto, se a matriz não estivesse vazia, haveria apenas uma para interagir. Se a matriz estiver vazia, registre uma mensagem e return.
val fenceId = when {
   geofencingEvent.triggeringGeofences.isNotEmpty() ->
       geofencingEvent.triggeringGeofences[0].requestId
   else -> {
       Log.e(TAG, "No Geofence Trigger Found! Abort mission!")
       return
   }
}
  • Verifique se a geocerca é consistente com as constantes listadas em GeofenceUtil.kt. Caso contrário, imprima um registro e return.
val foundIndex = GeofencingConstants.LANDMARK_DATA.indexOfFirst {
   it.id == fenceId
}

if ( -1 == foundIndex ) {
   Log.e(TAG, "Unknown Geofence: Abort Mission")
   return
}
  • Se sua execução de código chegou até aqui, o usuário inseriu uma geocerca válida. Envie uma notificação contando a eles as boas notícias!
val notificationManager = ContextCompat.getSystemService(
   context,
   NotificationManager::class.java
) as NotificationManager

notificationManager.sendGeofenceEnteredNotification(
   context, foundIndex
)
  1. Tente você mesmo entrando em uma geocerca ou emulando sua localização para estar na geocerca (instruções na próxima etapa). Quando você entra, uma notificação deve aparecer.

9. Tarefa: Simular um local no emulador

Pule esta seção se você não estiver usando um emulador.

Como o teste deste tutorial depende de uma caminhada, pode ser mais conveniente usar um local simulado no emulador. Nesta tarefa, você aprenderá como simular a localização em seu emulador.

Etapa 1: simule sua localização

  1. Na barra de menus ao lado do emulador, toque nos três pontos (...) na parte inferior para abrir o plano de Extended controls.

  1. Selecione Location.

  1. Na barra de pesquisa do mapa, insira um local, como a Ponte Golden Gate. O marcador de local é exibido no local que você inseriu.

  1. No canto inferior direito do painel, pressione o botão Set Location.

  1. Vá para o aplicativo do Google Maps e a notificação deve aparecer. Isso pode demorar alguns segundos.

10. Tarefa: Removendo geocerca

Quando você não precisar mais de cercas geográficas, é uma prática recomendada removê-las, o que interrompe o monitoramento, para economizar bateria e ciclos de CPU.

Etapa 1: remover geofences

  1. Em HuntMainActivity.kt, copie este código para o método removeGeofences(). Cada etapa é explicada nos pontos abaixo.
private fun removeGeofences() {
   if (!foregroundAndBackgroundLocationPermissionApproved()) {
       return
   }
   geofencingClient.removeGeofences(geofencePendingIntent)?.run {
       addOnSuccessListener {
           Log.d(TAG, getString(R.string.geofences_removed))
           Toast.makeText(applicationContext, R.string.geofences_removed, Toast.LENGTH_SHORT)
               .show()
       }
       addOnFailureListener {
           Log.d(TAG, getString(R.string.geofences_not_removed))
       }
   }
}
  • Inicialmente, verifique se as permissões de primeiro plano foram aprovadas. Se não, volte.
if (!foregroundAndBackgroundLocationPermissionApproved()) {
       return
   }
  • Chame removeGeofences() no geofencingClient e passe o geofencePendingIntent.
geofencingClient.removeGeofences(geofencePendingIntent)?.run {
}
  • Adicione um onSuccessListener() e informe ao usuário com um brinde que as cercas geográficas foram removidas com sucesso.
addOnSuccessListener {
   Log.d(TAG, getString(R.string.geofences_removed))
   Toast.makeText(applicationContext, R.string.geofences_removed, Toast.LENGTH_SHORT)
       .show()
}
  • Adicione um onFailureListener() onde você registra se as cercas geográficas não foram removidas.
addOnFailureListener {
   Log.d(TAG, getString(R.string.geofences_not_removed))
}
  1. O método removeGeofences() é chamado no método onDestroy() incluído no código inicial.

11. Tarefa: Navegar até o local vencedor

Agora que tudo está configurado, resta apenas uma coisa a fazer. Ganhar o jogo!

Etapa 1: Ganhe o jogo!

Navegue até o local vencedor zombando do local em seu emulador ou caminhando fisicamente até lá! Parabéns, você ganhou este tutorial!

12. Desafio de codificação

Você pode adicionar pontos de referência para personalizar sua caça ao tesouro e adicionar mais cercas geográficas para fazer a caça ao tesouro durar mais tempo.

  1. Em strings.xml, adicione sua dica personalizada e local.
<!-- Geofence Hints -->
<string name="lombard_street_hint">Go to the most crooked street in the City</string>
<!-- Geofence Locations -->
<string name="lombard_street_location">at Lombard Street</string>
  1. Em GeofenceUtils.kt, personalize os pontos de referência criando um LandmarkDataObject com um ID de destino, dica de destino, localização de destino e latitude e longitude de destino. Adicione-o ao array LANDMARK_DATA com seus próprios objetos de referência.
val LANDMARK_DATA = arrayOf(
   LandmarkDataObject(
       "Lombard street",
       R.string.lombard_street_hint,
       R.string.lombard_street_location,
       LatLng(37.801205, -122.426752))
)

13. Resumo

Neste tutorial você aprendeu como:

  • Adicione permissões, solicite permissões, verifique as permissões e controle as permissões.
  • Verifique a localização do dispositivo usando o cliente de configurações.
  • Adicione geofences usando uma intenção pendente e um cliente de geofencing.
  • Integre um receptor de transmissão para detectar quando uma geocerca é inserida substituindo o método onReceive().
  • Remova geofences usando o cliente de geofencing.

14. Saiba mais

Cursos Udacity:

Documentação do desenvolvedor Android:

Outros recursos:

15. Próximo tutorial

Para obter links para outros tutoriais neste curso, consulte a página inicial de tutoriais do Android avançado em Kotlin.