Initial commit: LogiStream Android WebView App

This commit is contained in:
2025-12-08 11:54:34 +09:00
commit 4f7aa593a1
78 changed files with 17896 additions and 0 deletions

44
android/app/build.gradle Normal file
View 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'
}

View 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>

View 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);
}
}
}

View 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();
// 액티비티가 백그라운드로 가더라도 서비스는 계속 실행됨
}
}

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 870 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View 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>

View 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
View 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
View 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

Binary file not shown.

View 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
View 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
View File

@@ -0,0 +1,3 @@
rootProject.name = 'LogiStream'
include ':app'