앱이 종료가 되더라도 사용자의 GPS 좌표를 계속 받아올 수 있는 백그라운드 서비스를 구현해보자
1. Permission (권한) 부여
- AndroidManifest.xml
: permission 4개 추가
( + LocationService.java 파일 생성 후 아래 <service> 태그 추가 )
<manifest xmlns:...>
...
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<application
...
<service
android:name=".LocationService"
android:enabled="true"
android:exported="false" />
</application>
</manifest>
>> Android 10(API 수준 29) 이상에서 개발자는 런타임 시 백그라운드 위치 정보 엑세스 권한을 요청하기 위해 앱 매니페스트에서 ACCESS_BACKGROUND_LOCATION 권한을 선언해야 합니다.
>> 또한 앱에서 Android 12 이상을 타겟팅하면 ACCESS_FINE_LOCATION 권한만 요청할 수 없습니다. ACCESS_COARSE_LOCATION 권한도 요청해야 하며 단일 런타임 요청에 두 권한을 모두 포함해야 합니다.
% 참조 : https://developer.android.com/training/location/permissions?hl=ko (안드로이드 개발문서)
2. gradle 추가
- build.gradle (:app)
dependencies {
...
// implementation('com.google.android.gms:play-services-location:<최신버전>')
implementation('com.google.android.gms:play-services-location:18.0.0') // 저는 18.0.0 버전입니다.
}
3. 레이아웃 생성
- 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/buttonStartLocationUpdates"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/start_location_update"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/buttonStopLocationUpdates"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/stop_location_updates"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/buttonStartLocationUpdates" />
</androidx.constraintlayout.widget.ConstraintLayout>
- strings.xml ( 참고 )
<resources>
<string name="app_name">Example_GPS</string>
<string name="start_location_update">Start_Location_Updates</string>
<string name="stop_location_updates">Stop_Location_Updates</string>
</resources>
4. 서비스 클래스 생성
- LocationService.java (생성)
package com.example.example_gps;
import android.Manifest;
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.content.pm.PackageManager;
import android.os.Build;
import android.os.IBinder;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;
import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;
public class LocationService extends Service {
private LocationCallback mLocationCallback = new LocationCallback() {
@Override
public void onLocationResult(LocationResult locationResult) {
super.onLocationResult(locationResult);
if (locationResult != null && locationResult.getLastLocation() != null) {
double latitude = locationResult.getLastLocation().getLatitude();
double longitude = locationResult.getLastLocation().getLongitude();
Log.v("LOCATION_UPDATE", latitude + ", " + longitude);
}
}
};
@Nullable
@Override
public IBinder onBind(Intent intent) {
throw new UnsupportedOperationException("Not yet implemented");
}
private void startLocationService() {
String channelId = "location_notification_channel";
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
Intent resultIntent = new Intent();
PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), 0, resultIntent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), channelId);
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setContentTitle("Location Service");
builder.setDefaults(NotificationCompat.DEFAULT_ALL);
builder.setContentText("Running");
builder.setContentIntent(pendingIntent);
builder.setAutoCancel(false);
builder.setPriority(NotificationCompat.PRIORITY_MAX);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (notificationManager != null && notificationManager.getNotificationChannel(channelId) == null) {
NotificationChannel notificationChannel = new NotificationChannel(channelId, "Location Service", NotificationManager.IMPORTANCE_HIGH);
notificationChannel.setDescription("This channel is used by location service");
notificationManager.createNotificationChannel(notificationChannel);
}
}
LocationRequest locationRequest = LocationRequest.create();
locationRequest.setInterval(4000);
locationRequest.setFastestInterval(2000);
locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return;
}
LocationServices.getFusedLocationProviderClient(this).requestLocationUpdates(locationRequest, mLocationCallback, Looper.getMainLooper());
startForeground(Constants.LOCATION_SERVICE_ID, builder.build());
}
private void stopLocationService() {
LocationServices.getFusedLocationProviderClient(this).removeLocationUpdates(mLocationCallback);
stopForeground(true);
stopSelf();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null) {
String action = intent.getAction();
if (action != null) {
if (action.equals(Constants.ACTION_START_LOCATION_SERVICE)) {
startLocationService();
} else if (action.equals(Constants.ACTION_STOP_LOCATION_SERVICE)) {
stopLocationService();
}
}
}
return super.onStartCommand(intent, flags, startId);
}
}
- Constants.java (생성)
package com.example.example_gps;
public class Constants {
static final int LOCATION_SERVICE_ID = 175;
static final String ACTION_START_LOCATION_SERVICE = "startLocationService";
static final String ACTION_STOP_LOCATION_SERVICE = "stopLocationService";
}
5. 버튼 동작하기 ( 메인 액티비티 )
- MainActivity.java
package com.example.example_gps;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.Manifest;
import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private static final int REQUEST_CODE_LOCATION_PERMISSION = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.buttonStartLocationUpdates).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_CODE_LOCATION_PERMISSION);
} else {
startLocationService();
}
}
});
findViewById(R.id.buttonStopLocationUpdates).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
stopLocationService();
}
});
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_LOCATION_PERMISSION && grantResults.length > 0) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startLocationService();
} else {
Toast.makeText(this, "Permission denied!", Toast.LENGTH_SHORT).show();
}
}
}
private boolean isLocationServiceRunning() {
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
if (activityManager != null) {
for (ActivityManager.RunningServiceInfo service : activityManager.getRunningServices(Integer.MAX_VALUE)) {
if (LocationService.class.getName().equals(service.service.getClassName())) {
if (service.foreground) {
return true;
}
}
}
return false;
}
return false;
}
private void startLocationService() {
if (!isLocationServiceRunning()) {
Intent intent = new Intent(getApplicationContext(), LocationService.class);
intent.setAction(Constants.ACTION_START_LOCATION_SERVICE);
startService(intent);
Toast.makeText(this, "Location service started", Toast.LENGTH_SHORT).show();
}
}
private void stopLocationService() {
if (isLocationServiceRunning()) {
Intent intent = new Intent(getApplicationContext(), LocationService.class);
intent.setAction(Constants.ACTION_STOP_LOCATION_SERVICE);
startService(intent);
Toast.makeText(this, "Location service stopped", Toast.LENGTH_SHORT).show();
}
}
}
- 파일 리스트
** 그대로 실행시켜서 Logcat을 보면 앱을 꺼도 GPS가 잡히는 것을 볼 수 있다.
* 중요 ★★★★★
- 안드로이드 11버전은 권한 요청에서 '항상 허용' 부분이 사라졌다. 그래서 백그라운드에서 일시적으로 작동하고 멈춘다.
- 정상적으로 작동시키려면 사용자가 직접 앱 꾹 눌러서 '앱정보 > 권한 > 위치 > 항상 허용' 체크해줘야한다.
// https://www.youtube.com/watch?v=4_RK_5bCoOY&t=1095s 참고
끝
'앱 개발 > 안드로이드(Java)' 카테고리의 다른 글
ListView 구현 (0) | 2023.03.10 |
---|---|
ViewBinding 적용하기 (0) | 2023.03.10 |
glide를 사용하여 GIF 로딩 화면 구현하기 (4) | 2021.08.17 |
이미지 회전 로딩 화면 구현 (0) | 2021.08.17 |
ViewPager2와 Fragment 예제 (0) | 2021.08.16 |