🌎

ロック画面でも位置情報を取得する

2024/09/27に公開

はじめに

以前AndroidでGPSを取得するアプリを作成しましたが、アプリを閉じたりロック画面にすると取得できないものでした
以前作ったもの↓
https://zenn.dev/nenfa/articles/d9258d1f92573a
そこで今回は以前作ったものを改良してフォアグラウンドサービスにしていきます

フォアグラウンドサービスとは

簡単に言うとユーザーが明示的に認識できる状態で実行されるサービスです
通知を表示することでサービスが動作中と認識できるようになっています

詳しくはAndroidDevelopersへ↓
https://developer.android.com/develop/background-work/services/foreground-services?hl=ja

サービスを作る

サービスを定義する

サービス化するにあたり必要なパーミッションが増えたので追記していきます
AndroidManifest.xmlに以下の権限をを追加しておきます

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とします
マニフェストにサービスとして定義する必要があるので次のコードを追記します

AndroidManifest.xml
    <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クラスを呼び出すことで取得していきます

GPSForegroundService.kt
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等そのままですがサービスで取得した値はログに表示するようにしてあるため今回は使用しません手抜きでごめんなさい

MainActivity.kt
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のタイミングでサービスの停止を行なっています

実際に動かすとこんな感じです
https://youtu.be/lV1hVuUQGyw?si=soGaOiv1dOWuHnIL

エミュレータで実行しているので位置情報は固定値ですが実機で動かすと実際の位置情報を取得できます

今回のコードです
https://github.com/NenfuAT/GetGPS

おわりに

今回は前回のアプリをフォアグラウンドサービスにしました
フォアグラウンドサービスで取得した値をUIなどに反映させるのは少しめんどくさいので今回はUIがお飾りになっています(気が向いたら更新するかも...?)
裏で位置情報を取得するユースケースは意外とあると思うので参考になったら幸いです

Discussion