Initial commit: LogiStream Android WebView App
44
android/app/build.gradle
Normal file
@@ -0,0 +1,44 @@
|
||||
apply plugin: "com.android.application"
|
||||
|
||||
def enableProguardInReleaseBuilds = false
|
||||
|
||||
android {
|
||||
compileSdkVersion 34
|
||||
buildToolsVersion "34.0.0"
|
||||
|
||||
namespace "com.logistream"
|
||||
defaultConfig {
|
||||
applicationId "com.logistream.mobile"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 34
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled false
|
||||
}
|
||||
release {
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.webkit:webkit:1.8.0'
|
||||
|
||||
// Google Play Services for Location
|
||||
implementation 'com.google.android.gms:play-services-location:21.0.1'
|
||||
}
|
||||
|
||||
67
android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- 인터넷 권한 -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<!-- 위치 권한 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
|
||||
<!-- 포그라운드 서비스 권한 (Android 9+) -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
|
||||
<!-- 알림 권한 (Android 13+) -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- 웨이크락 권한 (백그라운드 실행) -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<!-- 파일 접근 권한 (웹뷰 파일 업로드 등) -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
|
||||
<!-- 카메라 권한 (웹뷰에서 카메라 사용 시) -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<!-- 위치 기능 필수 -->
|
||||
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.location.network" android:required="false" />
|
||||
|
||||
<application
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:allowBackup="false"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:largeHeap="true">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Foreground Location Service -->
|
||||
<service
|
||||
android:name=".LocationService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="location">
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
178
android/app/src/main/java/com/logistream/LocationService.java
Normal file
@@ -0,0 +1,178 @@
|
||||
package com.logistream;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.location.Location;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import com.google.android.gms.location.FusedLocationProviderClient;
|
||||
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;
|
||||
import com.google.android.gms.location.Priority;
|
||||
|
||||
public class LocationService extends Service {
|
||||
private static final String CHANNEL_ID = "LocationServiceChannel";
|
||||
private static final int NOTIFICATION_ID = 1;
|
||||
|
||||
private FusedLocationProviderClient fusedLocationClient;
|
||||
private LocationCallback locationCallback;
|
||||
private final IBinder binder = new LocalBinder();
|
||||
private LocationUpdateListener locationUpdateListener;
|
||||
|
||||
// 위치 업데이트 리스너 인터페이스
|
||||
public interface LocationUpdateListener {
|
||||
void onLocationUpdate(Location location);
|
||||
}
|
||||
|
||||
public class LocalBinder extends Binder {
|
||||
LocationService getService() {
|
||||
return LocationService.this;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
|
||||
createNotificationChannel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
// Foreground Service 시작
|
||||
startForeground(NOTIFICATION_ID, createNotification());
|
||||
startLocationUpdates();
|
||||
|
||||
// 서비스가 종료되어도 자동으로 재시작
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return binder;
|
||||
}
|
||||
|
||||
private void createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"위치 추적 서비스",
|
||||
NotificationManager.IMPORTANCE_LOW // 소리 없이 표시
|
||||
);
|
||||
channel.setDescription("앱이 백그라운드에서 위치를 추적하고 있습니다");
|
||||
|
||||
NotificationManager manager = getSystemService(NotificationManager.class);
|
||||
if (manager != null) {
|
||||
manager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Notification createNotification() {
|
||||
// 알림 클릭 시 MainActivity로 이동
|
||||
Intent notificationIntent = new Intent(this, MainActivity.class);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
notificationIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
);
|
||||
|
||||
return new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("LogiStream 위치 추적 중")
|
||||
.setContentText("위치 정보를 실시간으로 전송하고 있습니다")
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true) // 사용자가 스와이프로 제거할 수 없음
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void startLocationUpdates() {
|
||||
// 권한 체크
|
||||
if (ActivityCompat.checkSelfPermission(this,
|
||||
android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
||||
stopSelf();
|
||||
return;
|
||||
}
|
||||
|
||||
// 위치 요청 설정
|
||||
LocationRequest locationRequest = new LocationRequest.Builder(
|
||||
Priority.PRIORITY_HIGH_ACCURACY, 10000) // 10초마다 업데이트
|
||||
.setMinUpdateIntervalMillis(5000) // 최소 5초 간격
|
||||
.build();
|
||||
|
||||
locationCallback = new LocationCallback() {
|
||||
@Override
|
||||
public void onLocationResult(@NonNull LocationResult locationResult) {
|
||||
Location location = locationResult.getLastLocation();
|
||||
if (location != null && locationUpdateListener != null) {
|
||||
locationUpdateListener.onLocationUpdate(location);
|
||||
updateNotification(location);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fusedLocationClient.requestLocationUpdates(
|
||||
locationRequest,
|
||||
locationCallback,
|
||||
Looper.getMainLooper()
|
||||
);
|
||||
}
|
||||
|
||||
private void updateNotification(Location location) {
|
||||
// 알림 내용 업데이트 (선택사항)
|
||||
NotificationManager manager = getSystemService(NotificationManager.class);
|
||||
if (manager != null) {
|
||||
Intent notificationIntent = new Intent(this, MainActivity.class);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
notificationIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
);
|
||||
|
||||
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("LogiStream 위치 추적 중")
|
||||
.setContentText(String.format("위도: %.6f, 경도: %.6f",
|
||||
location.getLatitude(), location.getLongitude()))
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build();
|
||||
|
||||
manager.notify(NOTIFICATION_ID, notification);
|
||||
}
|
||||
}
|
||||
|
||||
public void setLocationUpdateListener(LocationUpdateListener listener) {
|
||||
this.locationUpdateListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (fusedLocationClient != null && locationCallback != null) {
|
||||
fusedLocationClient.removeLocationUpdates(locationCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
415
android/app/src/main/java/com/logistream/MainActivity.java
Normal file
@@ -0,0 +1,415 @@
|
||||
package com.logistream;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.provider.Settings;
|
||||
import android.webkit.GeolocationPermissions;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private static final int LOCATION_PERMISSION_REQUEST_CODE = 1;
|
||||
private static final int NOTIFICATION_PERMISSION_REQUEST_CODE = 2;
|
||||
private static final String WEBSITE_URL = "https://logistream.kpslp.kr";
|
||||
private static final long BACK_PRESS_INTERVAL = 2000; // 2초
|
||||
|
||||
private WebView webView;
|
||||
private Handler handler = new Handler(Looper.getMainLooper());
|
||||
private long backPressedTime = 0;
|
||||
private Toast backToast;
|
||||
|
||||
// Foreground Service 관련
|
||||
private LocationService locationService;
|
||||
private boolean serviceBound = false;
|
||||
|
||||
private ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
LocationService.LocalBinder binder = (LocationService.LocalBinder) service;
|
||||
locationService = binder.getService();
|
||||
serviceBound = true;
|
||||
|
||||
// 위치 업데이트 리스너 설정
|
||||
locationService.setLocationUpdateListener(new LocationService.LocationUpdateListener() {
|
||||
@Override
|
||||
public void onLocationUpdate(Location location) {
|
||||
sendLocationToWebView(location);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
serviceBound = false;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
// 앱 시작 시 모든 캐시 및 세션 데이터 삭제
|
||||
clearAllCache();
|
||||
|
||||
webView = findViewById(R.id.webview);
|
||||
|
||||
setupWebView();
|
||||
|
||||
// 권한 확인 및 요청
|
||||
checkAndRequestPermissions();
|
||||
|
||||
webView.loadUrl(WEBSITE_URL);
|
||||
}
|
||||
|
||||
private void clearAllCache() {
|
||||
// 쿠키 완전 삭제
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
android.webkit.CookieManager.getInstance().removeAllCookies(null);
|
||||
android.webkit.CookieManager.getInstance().flush();
|
||||
} else {
|
||||
android.webkit.CookieManager cookieManager = android.webkit.CookieManager.getInstance();
|
||||
cookieManager.removeAllCookie();
|
||||
cookieManager.removeSessionCookie();
|
||||
}
|
||||
|
||||
// WebView 데이터 디렉토리 삭제
|
||||
try {
|
||||
// 캐시 디렉토리 삭제
|
||||
deleteDir(getCacheDir());
|
||||
|
||||
// WebView 관련 데이터 디렉토리 삭제
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
deleteDir(new java.io.File(getApplicationInfo().dataDir, "app_webview"));
|
||||
deleteDir(new java.io.File(getApplicationInfo().dataDir, "databases"));
|
||||
deleteDir(new java.io.File(getApplicationInfo().dataDir, "app_databases"));
|
||||
deleteDir(new java.io.File(getApplicationInfo().dataDir, "shared_prefs"));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean deleteDir(java.io.File dir) {
|
||||
if (dir != null && dir.isDirectory()) {
|
||||
String[] children = dir.list();
|
||||
if (children != null) {
|
||||
for (String child : children) {
|
||||
boolean success = deleteDir(new java.io.File(dir, child));
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dir.delete();
|
||||
} else if (dir != null && dir.isFile()) {
|
||||
return dir.delete();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void setupWebView() {
|
||||
// WebView 캐시 및 히스토리 삭제
|
||||
webView.clearCache(true);
|
||||
webView.clearHistory();
|
||||
webView.clearFormData();
|
||||
|
||||
WebSettings webSettings = webView.getSettings();
|
||||
|
||||
// JavaScript 활성화
|
||||
webSettings.setJavaScriptEnabled(true);
|
||||
webSettings.setGeolocationEnabled(true);
|
||||
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
|
||||
|
||||
// 캐시 설정 - 캐시 사용 안함
|
||||
webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);
|
||||
|
||||
// DOM Storage와 Database 활성화 (로그인 세션 유지를 위해 필요하지만 앱 재시작시 삭제됨)
|
||||
webSettings.setDomStorageEnabled(true);
|
||||
webSettings.setDatabaseEnabled(true);
|
||||
|
||||
// 줌 설정
|
||||
webSettings.setSupportZoom(true);
|
||||
webSettings.setBuiltInZoomControls(true);
|
||||
webSettings.setDisplayZoomControls(false);
|
||||
|
||||
// 기타 설정
|
||||
webSettings.setLoadWithOverviewMode(true);
|
||||
webSettings.setUseWideViewPort(true);
|
||||
|
||||
webView.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
view.loadUrl(url);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
webView.setWebChromeClient(new WebChromeClient() {
|
||||
@Override
|
||||
public void onGeolocationPermissionsShowPrompt(String origin,
|
||||
GeolocationPermissions.Callback callback) {
|
||||
callback.invoke(origin, true, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void checkAndRequestPermissions() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (!checkLocationPermission()) {
|
||||
// 권한 설명 다이얼로그 표시
|
||||
showPermissionRationaleDialog();
|
||||
} else {
|
||||
// 위치 권한이 있으면 알림 권한 확인
|
||||
checkNotificationPermission();
|
||||
}
|
||||
} else {
|
||||
startLocationService();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkNotificationPermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
// Android 13 이상에서는 알림 권한 필요
|
||||
if (ContextCompat.checkSelfPermission(this,
|
||||
android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[]{android.Manifest.permission.POST_NOTIFICATIONS},
|
||||
NOTIFICATION_PERMISSION_REQUEST_CODE);
|
||||
} else {
|
||||
startLocationService();
|
||||
}
|
||||
} else {
|
||||
startLocationService();
|
||||
}
|
||||
}
|
||||
|
||||
private void showPermissionRationaleDialog() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("위치 권한 필요")
|
||||
.setMessage(R.string.permission_rationale)
|
||||
.setPositiveButton("허용", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
requestLocationPermission();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("거부", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Toast.makeText(MainActivity.this,
|
||||
R.string.permission_denied,
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
})
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
}
|
||||
|
||||
private boolean checkLocationPermission() {
|
||||
return ContextCompat.checkSelfPermission(this,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
private void requestLocationPermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// Android 10 이상: 백그라운드 위치 권한 별도 요청
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[]{
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
},
|
||||
LOCATION_PERMISSION_REQUEST_CODE);
|
||||
} else {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[]{
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
},
|
||||
LOCATION_PERMISSION_REQUEST_CODE);
|
||||
}
|
||||
}
|
||||
|
||||
private void requestBackgroundLocationPermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("백그라운드 위치 권한")
|
||||
.setMessage("앱이 백그라운드에서도 위치를 추적하려면 '항상 허용'을 선택해주세요.")
|
||||
.setPositiveButton("설정", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
ActivityCompat.requestPermissions(MainActivity.this,
|
||||
new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION},
|
||||
LOCATION_PERMISSION_REQUEST_CODE + 1);
|
||||
}
|
||||
})
|
||||
.setNegativeButton("나중에", null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||
@NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
||||
if (requestCode == LOCATION_PERMISSION_REQUEST_CODE) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
Toast.makeText(this, "위치 권한이 허용되었습니다", Toast.LENGTH_SHORT).show();
|
||||
|
||||
// 백그라운드 위치 권한 요청
|
||||
requestBackgroundLocationPermission();
|
||||
|
||||
// 알림 권한 확인
|
||||
checkNotificationPermission();
|
||||
} else {
|
||||
// 권한 거부됨
|
||||
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION)) {
|
||||
// 사용자가 권한을 거부했지만 다시 요청 가능
|
||||
Toast.makeText(this, R.string.permission_denied, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
// 사용자가 "다시 묻지 않음"을 선택
|
||||
showSettingsDialog();
|
||||
}
|
||||
}
|
||||
} else if (requestCode == LOCATION_PERMISSION_REQUEST_CODE + 1) {
|
||||
// 백그라운드 위치 권한 결과
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
Toast.makeText(this, "백그라운드 위치 권한이 허용되었습니다", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
|
||||
// 알림 권한 결과
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
Toast.makeText(this, "알림 권한이 허용되었습니다", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
startLocationService();
|
||||
}
|
||||
}
|
||||
|
||||
private void showSettingsDialog() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("권한 필요")
|
||||
.setMessage("설정에서 위치 권한을 허용해주세요")
|
||||
.setPositiveButton("설정으로 이동", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
Uri uri = Uri.fromParts("package", getPackageName(), null);
|
||||
intent.setData(uri);
|
||||
startActivity(intent);
|
||||
}
|
||||
})
|
||||
.setNegativeButton("취소", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void startLocationService() {
|
||||
if (!checkLocationPermission()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Foreground Service 시작
|
||||
Intent serviceIntent = new Intent(this, LocationService.class);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(serviceIntent);
|
||||
} else {
|
||||
startService(serviceIntent);
|
||||
}
|
||||
|
||||
// 서비스에 바인딩
|
||||
bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
private void sendLocationToWebView(Location location) {
|
||||
String javascript = String.format(
|
||||
"javascript:(function() {" +
|
||||
" if (window.updateLocation) {" +
|
||||
" window.updateLocation(%f, %f, %f, %f, %f);" +
|
||||
" }" +
|
||||
"})();",
|
||||
location.getLatitude(),
|
||||
location.getLongitude(),
|
||||
location.getAccuracy(),
|
||||
location.getSpeed(),
|
||||
location.getBearing()
|
||||
);
|
||||
|
||||
handler.post(() -> webView.evaluateJavascript(javascript, null));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
// 서비스 바인딩 해제
|
||||
if (serviceBound) {
|
||||
unbindService(serviceConnection);
|
||||
serviceBound = false;
|
||||
}
|
||||
|
||||
// 앱 종료 시 서비스도 중지
|
||||
Intent serviceIntent = new Intent(this, LocationService.class);
|
||||
stopService(serviceIntent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (webView.canGoBack()) {
|
||||
// 웹뷰에서 뒤로 갈 수 있으면 웹뷰 뒤로가기
|
||||
webView.goBack();
|
||||
} else {
|
||||
// 첫 화면에서 뒤로가기 - 2번 누르면 종료
|
||||
if (backPressedTime + BACK_PRESS_INTERVAL > System.currentTimeMillis()) {
|
||||
// 2초 이내에 다시 누름 - 종료
|
||||
if (backToast != null) {
|
||||
backToast.cancel();
|
||||
}
|
||||
super.onBackPressed();
|
||||
finish();
|
||||
} else {
|
||||
// 첫 번째 뒤로가기 - 토스트 표시
|
||||
backPressedTime = System.currentTimeMillis();
|
||||
backToast = Toast.makeText(this, R.string.exit_message, Toast.LENGTH_SHORT);
|
||||
backToast.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
// 설정에서 돌아왔을 때 권한 재확인
|
||||
if (checkLocationPermission() && !serviceBound) {
|
||||
startLocationService();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
// 액티비티가 백그라운드로 가더라도 서비스는 계속 실행됨
|
||||
}
|
||||
}
|
||||
|
||||
13
android/app/src/main/res/drawable/ic_notification.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 -1.25,7 -3,9h20c-1.75,-2 -3,-3.75 -3,-9 0,-3.87 -3.13,-7 -7,-7zM12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zM12,6c2.21,0 4,1.79 4,4 0,1.66 0.67,3.33 2,5H6c1.33,-1.67 2,-3.34 2,-5 0,-2.21 1.79,-4 4,-4z"/>
|
||||
</vector>
|
||||
|
||||
|
||||
|
||||
|
||||
18
android/app/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
0
android/app/src/main/res/mipmap-hdpi/.gitkeep
Normal file
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 870 B |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 630 B |
0
android/app/src/main/res/mipmap-mdpi/.gitkeep
Normal file
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 606 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 418 B |
0
android/app/src/main/res/mipmap-xhdpi/.gitkeep
Normal file
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 846 B |
0
android/app/src/main/res/mipmap-xxhdpi/.gitkeep
Normal file
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
0
android/app/src/main/res/mipmap-xxxhdpi/.gitkeep
Normal file
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
8
android/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">GIS기반 공차중계</string>
|
||||
<string name="exit_message">뒤로 버튼을 한번 더 누르면 종료됩니다</string>
|
||||
<string name="permission_rationale">차량 위치 추적을 위해 위치 권한이 필요합니다</string>
|
||||
<string name="permission_denied">위치 권한이 거부되었습니다. 설정에서 권한을 허용해주세요</string>
|
||||
</resources>
|
||||
|
||||
12
android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- 액션바(헤더) 없는 테마 -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="colorPrimary">#0033A0</item>
|
||||
<item name="colorPrimaryDark">#002080</item>
|
||||
<item name="colorAccent">#E31E24</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
31
android/build.gradle
Normal file
@@ -0,0 +1,31 @@
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "34.0.0"
|
||||
minSdkVersion = 23
|
||||
compileSdkVersion = 34
|
||||
targetSdkVersion = 34
|
||||
kotlinVersion = "1.8.0"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.1.0'
|
||||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url "$rootDir/../node_modules/react-native/android"
|
||||
}
|
||||
maven {
|
||||
url "$rootDir/../node_modules/react-native-webview/android"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
46
android/gradle.properties
Normal file
@@ -0,0 +1,46 @@
|
||||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
org.gradle.parallel=true
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
|
||||
# Version of flipper SDK to use with React Native
|
||||
FLIPPER_VERSION=0.182.0
|
||||
|
||||
# Use this property to specify which architecture you want to build.
|
||||
# You can also override it from the CLI using
|
||||
# ./gradlew <task> -PreactNativeArchitectures=x86_64
|
||||
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
||||
|
||||
# Use this property to enable support to the new architecture.
|
||||
# This will allow you to use TurboModules and the Fabric render in
|
||||
# your application. You should enable this flag either if you want
|
||||
# to write custom TurboModules/Fabric components OR use libraries that
|
||||
# are providing them.
|
||||
newArchitectureEnabled=false
|
||||
|
||||
# Use this property to enable or disable the Hermes JS engine.
|
||||
# If set to false, you will be using JSC instead.
|
||||
hermesEnabled=true
|
||||
|
||||
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
91
android/gradlew.bat
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
|
||||
3
android/settings.gradle
Normal file
@@ -0,0 +1,3 @@
|
||||
rootProject.name = 'LogiStream'
|
||||
include ':app'
|
||||
|
||||