iOS, Android 백그라운드 위치 추적 방법

여러분이 서비스하는 앱/웹이 사용자의 위치를 사용자 기기의 화면이 꺼진 상황에서도 추적해야 할 필요가 있나요? 물론 불법이 아닌 범위 내에서요. 안타깝게도 아직까지 모바일 웹 서비스로는 이 기능이 불가능합니다. 따라서 앱을 직접 만들어서 백그라운드 위치 추적 기능을 넣는 방법이 최선의 방법입니다. 이제 각 플랫폼 별로 어떻게 해야 하는지를 알려드리도록 하겠습니다.


1. Android(API 28 이하까지 적용됨. 29 이상은 차차 알아봐야 함...)

AndroidManifest.xml

        </activity>

        <service android:name=".Background" />

    </application>

    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

</manifest>

Background.java

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;

public class Background extends Service {
// private LocationListener mLocationListener;
// private LocationManager mLocationManager;

@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public void onCreate() {
super.onCreate();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent.getAction().equals("Start")) {
Log.i("location", "Received Start Foreground Intent ");
startForeground((int) System.currentTimeMillis() % 10000, getNotification());
// your start service code
}
else if (intent.getAction().equals("Stop")) {
Log.i("location", "Received Stop Foreground Intent");
//your end servce code
stopForeground(true);
stopSelf();
}
return START_STICKY;
}

@Override
public void onDestroy() {
super.onDestroy();
}

private Notification getNotification() {
if (Build.VERSION.SDK_INT >= 26) { // API 26 이상에선 알림을 통해 위치 서비스를 받고 있다는 것을 알려야 함
NotificationChannel channel = new NotificationChannel("channel_01", "My Channel", NotificationManager.IMPORTANCE_DEFAULT);

NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);

Notification.Builder builder = new Notification.Builder(getApplicationContext(), "channel_01").setAutoCancel(false).setContentTitle("백그라운드에서 위치 데이터 사용중").setContentText("백그라운드에서 위치 데이터 사용중입니다.").setOngoing(true);

return builder.build();
} else {
Notification.Builder builder = new Notification.Builder(getApplicationContext()).setAutoCancel(false).setContentTitle("백그라운드에서 위치 데이터 사용중").setContentText("백그라운드에서 위치 데이터 사용중입니다.").setOngoing(true);

return builder.build();
}
}
}

MainActivity.java

import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "assistanceapp.2020irc.org";
LocationManager locationManager;

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == 1) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);

LocationListener locationListener = new LocationListener() {
public void onLocationChanged(Location location) {
new MethodChannel(getFlutterView(), CHANNEL).invokeMethod("background", location.getLatitude() + "," + location.getLongitude());
Log.d("location", "" + location.getLatitude());
Log.d("location", "" + location.getLongitude());
}

public void onStatusChanged(String provider, int status, Bundle extras) {
Log.d("location", "onStatusChanged");
}

public void onProviderEnabled(String provider) {
Log.d("location", "onProviderEnabled");
}

public void onProviderDisabled(String provider) {
Log.d("location", "onProviderDisabled");
}
};
try {
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 2000, 1, locationListener);
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000, 1, locationListener);
} catch(SecurityException e) {
Log.d("location", "SecurityException: permission required");
}

new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
(call, result) -> {
if (call.method.equals("initLocation")) {
initLocation();
} else if (call.method.equals("stop")) {
locationManager.removeUpdates(locationListener);
final Intent intent = new Intent(this.getApplication(), Background.class);
intent.setAction("Stop");
this.getApplication().startService(intent);
} else {
result.notImplemented();
}
});
}
}
}

@Override
protected void onCreate(Bundle savedInstanceState) {
FlutterMain.startInitialization(this);
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);

if(Build.VERSION.SDK_INT >= 23)
{
if (checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED &&
checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED) {
locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);

LocationListener locationListener = new LocationListener() {
public void onLocationChanged(Location location) {
new MethodChannel(getFlutterView(), CHANNEL).invokeMethod("background", location.getLatitude() + "," + location.getLongitude());
Log.d("location", "" + location.getLatitude());
Log.d("location", "" + location.getLongitude());
}

public void onStatusChanged(String provider, int status, Bundle extras) {
Log.d("location", "onStatusChanged");
}

public void onProviderEnabled(String provider) {
Log.d("location", "onProviderEnabled");
}

public void onProviderDisabled(String provider) {
Log.d("location", "onProviderDisabled");
}
};

locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 2000, 1, locationListener);
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000, 1, locationListener);

if (call.method.equals("initLocation")) { // 백그라운드 위치 추적 시작 조건
initLocation();
} else if (call.method.equals("stop")) { // 백그라운드 위치 추적 끝 조건
locationManager.removeUpdates(locationListener);
final Intent intent = new Intent(this.getApplication(), Background.class);
intent.setAction("Stop");
this.getApplication().startService(intent);
}

} else {
requestPermissions(new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION}, 1);
}
}
else
{
locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);

LocationListener locationListener = new LocationListener() {
public void onLocationChanged(Location location) {
new MethodChannel(getFlutterView(), CHANNEL).invokeMethod("background", location.getLatitude() + "," + location.getLongitude());
Log.d("location", "" + location.getLatitude());
Log.d("location", "" + location.getLongitude());
}

public void onStatusChanged(String provider, int status, Bundle extras) {
Log.d("location", "onStatusChanged");
}

public void onProviderEnabled(String provider) {
Log.d("location", "onProviderEnabled");
}

public void onProviderDisabled(String provider) {
Log.d("location", "onProviderDisabled");
}
};

locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 2000, 1, locationListener);
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000, 1, locationListener);
if (call.method.equals("initLocation")) { // 백그라운드 위치 추적 시작 조건
initLocation();
} else if (call.method.equals("stop")) { // 백그라운드 위치 추적 끝 조건
locationManager.removeUpdates(locationListener);
final Intent intent = new Intent(this.getApplication(), Background.class);
intent.setAction("Stop");
this.getApplication().startService(intent);
}
}


}

private void initLocation() {
final Intent intent = new Intent(this.getApplication(), Background.class);
if (Build.VERSION.SDK_INT >= 26) {
intent.setAction("Start");
this.getApplication().startForegroundService(intent);
} else {
intent.setAction("Start");
this.getApplication().startService(intent);
}
}
}

관련 문서: 

https://developer.android.com/training/location/background?hl=ko

https://developer.android.com/training/location/permissions?hl=ko

그 외 안드로이드 공식 문서


2. iOS

AppDelegate.swift

import UIKit

import CoreLocation


@UIApplicationMain

@objc class AppDelegateCLLocationManagerDelegate {

    let locationManager = CLLocationManager()

    override func application(

        _ application: UIApplication,

        didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?

        ) -> Bool {

        locationManager.requestAlwaysAuthorization()

        locationManager.delegate = self

        locationManager.allowsBackgroundLocationUpdates = true

        if #available(iOS 11.0, *) {

            locationManager.showsBackgroundLocationIndicator = true

        else {

            // Fallback on earlier versions

        }

        locationManager.pausesLocationUpdatesAutomatically = false

        locationManager.startUpdatingLocation()

        

        GeneratedPluginRegistrant.register(with: self)

        return super.application(application, didFinishLaunchingWithOptions: launchOptions)

    }


    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {

        // 여기서 location 설정을 맞춰주면 됨(위도: locations.last?.coordinate.latitude, 경도: locations.last?.coordinate.longitude). 

        // 이 함수는 기기 내에서 주기적으로 호출됨

    }

    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {

        if let error = error as? CLError, error.code == .denied {

            // Location updates are not authorized.

            return

        }

        // Notify the user of any errors.

    }

}

그리고 info.plist에 NSLocationAlwaysAndWhenInUseUsageDescription, NSLocationAlwaysUsageDescription, NSLocationWhenInUseUsageDescription 추가하기, Capability Tab에 Background Mode 안 Location Update 체크표시

관련 문서: 

https://developer.apple.com/documentation/corelocation/getting_the_user_s_location/using_the_standard_location_service

https://developer.apple.com/documentation/corelocation/getting_the_user_s_location/handling_location_events_in_the_background

그 외 애플 개발자 공식 문서


놀랍게도, 제가 소개해드린 방법들은 Flutter에도 플랫폼별 코드 작성을 이용하여 구현이 가능합니다. 제가 일전에 이 기능을 써야 할 상황이 생겼었습니다. 근데 아무리 구글링을 해봐도 만족하는 무료 Flutter 패키지가 없어서 포기를 해야 하나 고민하고 있던 와중에, 이 사이트를 참고하여 이것마저 안 되면 포기하자는 마음으로 코드를 작성하여 시도해보았습니다. 다행히도, 성공하더군요. 그래서 다른 분들께 칭찬을 받고 무난하게 기능을 도입했던 기억이 납니다.

댓글

이 블로그의 인기 게시물

개인정보처리방침

앱 출시는 어떤 과정으로 진행되는 것일까? (Android, iOS)

React Native 화면 전환법