AndroidアプリでForeground Serviceを使って、画面スリープ状態でも位置情報を定期取得する
Android 8.0から、バックグラウンドアプリが位置情報を取得する回数に制限が制限されています。 アプリがフォアグラウンドにいる場合は問題ありませんが、この制限を回避する方法の一つは、Foreground Serviceを使うことです。
アプリがバックグラウンドいるとき(スリープ状態のとき)でも位置情報を取得したくなったため、サンプルアプリを作って試してみました。
環境
- Android Studio 3.6
- Pixcel 3a (Android 10)
サンプルアプリを作ってみる
プロジェクトの新規作成
Empty Activity
を選択して作成します。
Google Play Serviceライブラリの追加
app/build.gradle
にcom.google.android.gms:play-services-location:17.0.0
を追加します。
dependencies { ... implementation 'com.google.android.gms:play-services-location:17.0.0' }
Google Play Servicesの最新バージョンは下記で確認できます。
パーミッションの付与
AndroidManifest.xml
に下記3つのパーミッションを追加します。
- ACCESS_COARSE_LOCATION
- ACCESS_FINE_LOCATION
- ACCESS_BACKGROUND_LOCATION
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="jp.test.tryforegroundservicesample"> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <application ... </application> </manifest>
位置情報のパーミッションをリクエストする
ユーザに対して、アプリが位置情報を利用する旨を確認するため、requestPermission()
などを追加します。
class MainActivity : AppCompatActivity() { companion object { private const val PERMISSION_REQUEST_CODE = 1234 } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) requestPermission() } private fun requestPermission() { val permissionAccessCoarseLocationApproved = ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED if (permissionAccessCoarseLocationApproved) { val backgroundLocationPermissionApproved = ActivityCompat .checkSelfPermission(this, Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_GRANTED if (backgroundLocationPermissionApproved) { // フォアグラウンドとバックグランドのバーミッションがある } else { // フォアグラウンドのみOKなので、バックグラウンドの許可を求める ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION), PERMISSION_REQUEST_CODE ) } } else { // 位置情報の権限が無いため、許可を求める ActivityCompat.requestPermissions(this, arrayOf( Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION ), PERMISSION_REQUEST_CODE ) } } }
Foreground Serviceとして動くServiceを実装
ForegroundServiceとして動くLocationService.kt
を新規作成します。サービスをフォアグラウンドで実行するstartForeground()
を呼んでいます。その際に通知情報を渡す必要があるため、通知情報も作成しています。
import android.app.Service import android.content.Intent import android.os.IBinder import android.util.Log import androidx.core.app.NotificationCompat import com.google.android.gms.location.* class LocationService : Service() { companion object { const val CHANNEL_ID = "777" } private lateinit var fusedLocationClient: FusedLocationProviderClient private lateinit var locationCallback: LocationCallback override fun onCreate() { super.onCreate() fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { var updatedCount = 0 locationCallback = object : LocationCallback() { override fun onLocationResult(locationResult: LocationResult?) { locationResult ?: return for (location in locationResult.locations){ updatedCount++ Log.d(this.javaClass.name, "[${updatedCount}] ${location.latitude} , ${location.longitude}") } } } val openIntent = Intent(this, MainActivity::class.java).let { PendingIntent.getActivity(this, 0, it, 0) } val notification = NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.ic_launcher_foreground) .setContentTitle("位置情報テスト") .setContentText("位置情報を取得しています...") .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setContentIntent(openIntent) .build() startForeground(9999, notification) startLocationUpdates() return START_STICKY } override fun onBind(p0: Intent?): IBinder? { return null } override fun stopService(name: Intent?): Boolean { return super.stopService(name) stopLocationUpdates() } override fun onDestroy() { super.onDestroy() stopLocationUpdates() stopSelf() } private fun startLocationUpdates() { val locationRequest = createLocationRequest() ?: return fusedLocationClient.requestLocationUpdates( locationRequest, locationCallback, null) } private fun stopLocationUpdates() { fusedLocationClient.removeLocationUpdates(locationCallback) } private fun createLocationRequest(): LocationRequest? { return LocationRequest.create()?.apply { interval = 10000 fastestInterval = 5000 priority = LocationRequest.PRIORITY_HIGH_ACCURACY } } }
AndroidManifestファイルにForeground Service権限とサービスクラス名を追加
AndroidManifest.xml
に下記を追加します。
FOREGROUND_SERVICE
の権限(APIレベル28以上で必要)- 作成したサービスクラス名
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="jp.test.tryforegroundservicesample"> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <application <activity android:name=".MainActivity"> ... </activity> <service android:name=".LocationService" /> </application> </manifest>
通知先チャンネルを作成
Foreground Serviceが動いていることをユーザに示すための通知を表示しなければなりません。この通知先チャンネルを作成します。(APIレベル26以上で必要)
MainActivity.kt
にcreateNotificationChannel()
を実装し、onCreate()
から呼び出します。
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) requestPermission() createNotificationChannel() } private fun requestPermission() { ... } private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( LocationService.CHANNEL_ID, "お知らせ", NotificationManager.IMPORTANCE_DEFAULT).apply { description = "お知らせを通知します。" } val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannel(channel) } }
開始ボタンと終了ボタンを作成
activity_main.xml
に開始ボタンと終了ボタンを作成します。
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/startButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Start" app:layout_constraintBottom_toTopOf="@id/finishButton" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/finishButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Finish" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/startButton" /> </androidx.constraintlayout.widget.ConstraintLayout>
開始&終了ボタンの選択時にForeground Serviceを開始&終了する
MainActivity.kt
に開始ボタンと終了ボタン選択時の動作を追加します。
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) requestPermission() createNotificationChannel() startButton.setOnClickListener { val intent = Intent(this, LocationService::class.java) startForegroundService(intent) } finishButton.setOnClickListener { val intent = Intent(this, LocationService::class.java) stopService(intent) } }
動作確認
ビルド&起動
アプリをビルドして起動します。起動すると位置情報を取得するか聞かれるため、「常に許可」を選択します。
作成した「START」ボタンと「FINISH」が表示されています。
位置情報の定期取得を開始する
「START」ボタンを押して、Foreground Serviceで位置情報の定期取得を開始します。 通知画面には、位置情報を取得している旨の通知が常時表示されています。
そして、次のようなログがバッチリ出力されています!
[1] 35.xxxx , 139.yyyy [2] 35.xxxx , 139.yyyy [3] 35.xxxx , 139.yyyy ... [30] 35.xxxx , 139.yyyy [31] 35.xxxx , 139.yyyy [32] 35.xxxx , 139.yyyy
Androidアプリを次の状態にしても、ログは定期的に表示されていました。
- 画面に作成したアプリが表示されている状態
- 画面に別のアプリが表示されている状態
- 画面スリープ状態
位置情報の定期取得を終了する
「FINISH」ボタンを押すだけです。通知画面から表示も消えます。
さいごに
Foreground Serviceの作り方が分かりました。 公式ドキュメントの読解方法もなんとなく掴めてきたような気がします……。 どなたかの参考になれば幸いです。
参考
- バックグラウンド位置情報の制限 | Android デベロッパー | Android Developers
- Set Up Google Play Services | Android 用 Google API | Google Developers
- 最新の位置情報を取得する | Android デベロッパー | Android Developers
- サービスの概要 | Android デベロッパー | Android Developers
- 通知の概要 | Android デベロッパー | Android Developers
- 定期的に現在地の更新情報を受け取る | Android デベロッパー | Android Developers
- 位置情報の設定の変更 | Android デベロッパー | Android Developers
- Androidアプリの通知音をアプリ内蔵音源に変更してみた | Developers.IO
- Foreground Serviceの基本 - Qiita