ロック画面でも位置情報を取得する
はじめに
以前AndroidでGPSを取得するアプリを作成しましたが、アプリを閉じたりロック画面にすると取得できないものでした
以前作ったもの↓
そこで今回は以前作ったものを改良してフォアグラウンドサービスにしていきます
フォアグラウンドサービスとは
簡単に言うとユーザーが明示的に認識できる状態で実行されるサービスです
通知を表示することでサービスが動作中と認識できるようになっています
詳しくはAndroidDevelopersへ↓
サービスを作る
サービスを定義する
サービス化するにあたり必要なパーミッションが増えたので追記していきます
AndroidManifest.xml
に以下の権限をを追加しておきます
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
FOREGROUND_SERVICE
でフォアグラウンドサービスの権限
POST_NOTIFICATIONS
が通知の権限権限
FOREGROUND_SERVICE_LOCATION
がフォアグラウンドサービスで位置情報を扱う権限です
次にサービスを記述するファイルを作成します
今回はGPSForegroundService.kt
とします
マニフェストにサービスとして定義する必要があるので次のコードを追記します
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.GetGPS"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.GetGPS">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!--これを追加-->
<service
android:name=".GPSForegroundService"
android:foregroundServiceType="location"/>
</application>
サービスを実装する
実際にサービスを定義していきます今回のGPSを取得する部分は前回作成したGPSLocationManager
クラスを呼び出すことで取得していきます
package com.nenfuat.getgps
import GPSLocationManager
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.location.Location
import android.util.Log
import androidx.core.app.NotificationCompat
class GPSForegroundService : Service() {
private lateinit var gpsLocationManager: GPSLocationManager
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// GPSLocationManagerの初期化
gpsLocationManager = GPSLocationManager(this)
gpsLocationManager.startLocationUpdates(object : GPSLocationManager.MyLocationCallback {
override fun onLocationResult(location: Location?) {
location?.let {
// 必要に応じて、位置情報を他のコンポーネントやサーバーに送信
Log.d("GPS","Location: ${it.latitude}, ${it.longitude}")
}
}
override fun onLocationError(error: String) {
println("Location Error: $error")
}
})
// フォアグラウンドサービスとして通知を表示
startForegroundServiceWithNotification()
return START_STICKY
}
private fun startForegroundServiceWithNotification() {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channelId = "GPSServiceChannel"
val channelName = "GPS Service Channel"
// 通知チャンネルを作成
val channel = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW)
notificationManager.createNotificationChannel(channel)
// メインアクティビティを起動するPendingIntent
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)
// 通知の作成
val notification = NotificationCompat.Builder(this, channelId)
.setContentTitle("GPS 位置情報サービス")
.setContentText("位置情報を取得しています")
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentIntent(pendingIntent)
.build()
// フォアグラウンドサービスとして通知を表示
startForeground(1, notification)
}
override fun onDestroy() {
super.onDestroy()
// GPSの位置情報取得を停止
gpsLocationManager.stopLocationUpdates()
stopForeground(true) // 通知の削除
stopSelf() // サービスの停止
}
override fun onBind(intent: Intent?) = null
}
通知に
// メインアクティビティを起動するPendingIntent
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)
を入れることで通知をタップした際にアプリに遷移することができます
次にMainActivity.kt
でサービスを起動します
通知の権限を求めるついでに位置情報の権限もこっちに移しちゃいましょう
前回の使い回しなのでUI等そのままですがサービスで取得した値はログに表示するようにしてあるため今回は使用しません手抜きでごめんなさい
package com.nenfuat.getgps
import GPSLocationManager
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.location.Location
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.core.app.ActivityCompat
import com.nenfuat.getgps.ui.theme.GetGPSTheme
class MainActivity : ComponentActivity() {
private lateinit var gpsLocationManager: GPSLocationManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
gpsLocationManager = GPSLocationManager(this)
// 位置情報を取得するためのPermissionチェック
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
) {
// 必要な場合、パーミッションを要求
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION,Manifest.permission.POST_NOTIFICATIONS), 1000)
}else {
// 権限が既にある場合はサービスを開始する
startGPSService()
}
enableEdgeToEdge()
setContent {
GetGPSTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
GPSViewer(
modifier = Modifier.padding(innerPadding),
gpsLocationManager
)
}
}
}
}
override fun onDestroy() {
super.onDestroy()
gpsLocationManager.stopLocationUpdates()
// サービスを停止
stopGPSService()
}
private fun startGPSService() {
val intent = Intent(this, GPSForegroundService::class.java)
startService(intent) // サービスの起動
}
private fun stopGPSService() {
val intent = Intent(this, GPSForegroundService::class.java)
stopService(intent) // サービスの停止
}
}
@Composable
fun GPSViewer(modifier: Modifier = Modifier, gpsLocationManager: GPSLocationManager) {
var latitude by remember { mutableStateOf("") }
var longitude by remember { mutableStateOf("") }
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "緯度: $latitude\n経度: $longitude",
modifier = modifier
)
Button(onClick = {
gpsLocationManager.startLocationUpdates(object : GPSLocationManager.MyLocationCallback {
override fun onLocationResult(location: Location?) {
if (location != null) {
latitude = location.latitude.toString()
longitude = location.longitude.toString()
}
}
override fun onLocationError(error: String) {
// エラー処理
}
})
}) {
Text(text = "位置情報取得")
}
}
}
変更点としては権限を求める部分を追加したのとCreate時にサービスの実行、Destroyのタイミングでサービスの停止を行なっています
実際に動かすとこんな感じです
エミュレータで実行しているので位置情報は固定値ですが実機で動かすと実際の位置情報を取得できます
今回のコードです
おわりに
今回は前回のアプリをフォアグラウンドサービスにしました
フォアグラウンドサービスで取得した値をUIなどに反映させるのは少しめんどくさいので今回はUIがお飾りになっています(気が向いたら更新するかも...?)
裏で位置情報を取得するユースケースは意外とあると思うので参考になったら幸いです
Discussion