initial commit: push everything
49
.gitignore
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# Claude AI settings
|
||||
.claude/
|
||||
|
||||
30
.metadata
Normal file
@@ -0,0 +1,30 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
|
||||
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
|
||||
- platform: web
|
||||
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
|
||||
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
16
README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# nearle
|
||||
|
||||
A new Flutter project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
32
analysis_options.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
analyzer:
|
||||
errors:
|
||||
file_names: ignore
|
||||
unused_field: ignore
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at https://dart.dev/lints.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
13
android/.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
.cxx/
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
|
||||
|
||||
122
android/app/build.gradle
Normal file
@@ -0,0 +1,122 @@
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
id "com.google.gms.google-services"
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file("local.properties")
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withReader("UTF-8") { reader ->
|
||||
localProperties.load(reader)
|
||||
}
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty("flutter.versionCode") ?: "1"
|
||||
def flutterVersionName = localProperties.getProperty("flutter.versionName") ?: "1.0"
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
def keystorePropertiesFile = rootProject.file("key.properties")
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
def googleMapsApiKey = localProperties.getProperty("GOOGLE_MAPS_API_KEY") ?: ""
|
||||
|
||||
android {
|
||||
namespace = "com.nearle.partner"
|
||||
compileSdk = 36
|
||||
|
||||
// Correct NDK version
|
||||
ndkVersion "27.0.12077973"
|
||||
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
useLegacyPackaging = true
|
||||
}
|
||||
// ❌ removed doNotStrip (this was breaking your AAB build)
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
coreLibraryDesugaringEnabled true
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.nearle.partner"
|
||||
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
targetSdkVersion 36
|
||||
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
|
||||
manifestPlaceholders = [
|
||||
GOOGLE_MAPS_API_KEY: googleMapsApiKey,
|
||||
applicationName: "android.app.Application"
|
||||
]
|
||||
|
||||
// Build only ARM64
|
||||
ndk {
|
||||
abiFilters "arm64-v8a"
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias keystoreProperties["keyAlias"]
|
||||
keyPassword keystoreProperties["keyPassword"]
|
||||
storeFile keystoreProperties["storeFile"] ? file(keystoreProperties["storeFile"]) : null
|
||||
storePassword keystoreProperties["storePassword"]
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled false
|
||||
debuggable true
|
||||
}
|
||||
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
|
||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
|
||||
// ❌ removed ndk { debugSymbolLevel } (this caused strip crash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.core:core:1.10.0"
|
||||
implementation "com.google.android.gms:play-services-location:21.0.1"
|
||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.5"
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
tasks.register("copyNotificationSounds", Copy) {
|
||||
def audioDir = file("$projectDir/../../assets/audio")
|
||||
from(audioDir)
|
||||
include("*.mp3")
|
||||
into("$projectDir/src/main/res/raw")
|
||||
|
||||
rename { String fileName ->
|
||||
fileName.replaceAll(/[^A-Za-z0-9_.]/, "_").toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("preBuild").configure {
|
||||
dependsOn("copyNotificationSounds")
|
||||
}
|
||||
59
android/app/build.gradle.kts
Normal file
@@ -0,0 +1,59 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.nearle"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.example.nearle"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
|
||||
packaging {
|
||||
jniLibs {
|
||||
doNotStrip.add("**/*.so")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
// android/app/build.gradle.kts (keep this task)
|
||||
tasks.register<Copy>("copyNotificationSounds") {
|
||||
from("$projectDir/../../assets/audio") {
|
||||
include("*.mp3")
|
||||
}
|
||||
into("$projectDir/src/main/res/raw")
|
||||
rename { it.replace(Regex("[^A-Za-z0-9_.]"), "_").lowercase() }
|
||||
}
|
||||
tasks.named("preBuild") { dependsOn("copyNotificationSounds") }
|
||||
163
android/app/google-services.json
Normal file
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "140444764229",
|
||||
"firebase_url": "https://nearle-gear-default-rtdb.firebaseio.com",
|
||||
"project_id": "nearle-gear",
|
||||
"storage_bucket": "nearle-gear.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:140444764229:android:a584c5e9127c3789283b2c",
|
||||
"android_client_info": {
|
||||
"package_name": "com.nearle.admin"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "140444764229-seu4nkl2k2hj6gebk3q20sv80k685ecp.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyBkzz2Yua74Q9YpzGmUPFP94fmJQqNMIiU"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "140444764229-seu4nkl2k2hj6gebk3q20sv80k685ecp.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
},
|
||||
{
|
||||
"client_id": "140444764229-m6l2v6eofrb9jgrno3qmsrjbtd4iccvd.apps.googleusercontent.com",
|
||||
"client_type": 2,
|
||||
"ios_info": {
|
||||
"bundle_id": "com.nearle.gear"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:140444764229:android:a99eaae950fa5ada283b2c",
|
||||
"android_client_info": {
|
||||
"package_name": "com.nearle.bond"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "140444764229-seu4nkl2k2hj6gebk3q20sv80k685ecp.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyBkzz2Yua74Q9YpzGmUPFP94fmJQqNMIiU"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "140444764229-seu4nkl2k2hj6gebk3q20sv80k685ecp.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
},
|
||||
{
|
||||
"client_id": "140444764229-m6l2v6eofrb9jgrno3qmsrjbtd4iccvd.apps.googleusercontent.com",
|
||||
"client_type": 2,
|
||||
"ios_info": {
|
||||
"bundle_id": "com.nearle.gear"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:140444764229:android:88df9627e2990ef4283b2c",
|
||||
"android_client_info": {
|
||||
"package_name": "com.nearle.gear"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "140444764229-5cllr5f8u28psf8ttc220f6h487u9vjq.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "com.nearle.gear",
|
||||
"certificate_hash": "1d8115901ded2af41250ffbfe99b9239a1953c05"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "140444764229-seu4nkl2k2hj6gebk3q20sv80k685ecp.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyBkzz2Yua74Q9YpzGmUPFP94fmJQqNMIiU"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "140444764229-seu4nkl2k2hj6gebk3q20sv80k685ecp.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
},
|
||||
{
|
||||
"client_id": "140444764229-m6l2v6eofrb9jgrno3qmsrjbtd4iccvd.apps.googleusercontent.com",
|
||||
"client_type": 2,
|
||||
"ios_info": {
|
||||
"bundle_id": "com.nearle.gear"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:140444764229:android:578383f5a1d3a05c283b2c",
|
||||
"android_client_info": {
|
||||
"package_name": "com.nearle.partner"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "140444764229-seu4nkl2k2hj6gebk3q20sv80k685ecp.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyBkzz2Yua74Q9YpzGmUPFP94fmJQqNMIiU"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "140444764229-seu4nkl2k2hj6gebk3q20sv80k685ecp.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
},
|
||||
{
|
||||
"client_id": "140444764229-m6l2v6eofrb9jgrno3qmsrjbtd4iccvd.apps.googleusercontent.com",
|
||||
"client_type": 2,
|
||||
"ios_info": {
|
||||
"bundle_id": "com.nearle.gear"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
BIN
android/app/nearlerider-keystore.jks
Normal file
89
android/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
##########################################
|
||||
## ✅ FLUTTER
|
||||
##########################################
|
||||
-keep class io.flutter.** { *; }
|
||||
-dontwarn io.flutter.**
|
||||
|
||||
##########################################
|
||||
## ✅ FIREBASE (Safe)
|
||||
##########################################
|
||||
-keep class com.google.firebase.** { *; }
|
||||
-keep class com.google.android.gms.** { *; }
|
||||
-dontwarn com.google.firebase.**
|
||||
-dontwarn com.google.android.gms.**
|
||||
|
||||
##########################################
|
||||
## ✅ FCM Background Messaging
|
||||
##########################################
|
||||
-keep class com.google.firebase.messaging.FirebaseMessagingService { *; }
|
||||
-keep class com.google.firebase.messaging.FirebaseMessaging { *; }
|
||||
-keep class com.google.firebase.messaging.RemoteMessage { *; }
|
||||
|
||||
##########################################
|
||||
## ✅ Gson / JSON Models (Correct way)
|
||||
##########################################
|
||||
-keepclassmembers class * {
|
||||
@com.google.gson.annotations.SerializedName <fields>;
|
||||
}
|
||||
|
||||
# Keep all model classes to prevent crashes
|
||||
-keep class **.model.** { *; }
|
||||
-keep class **.models.** { *; }
|
||||
|
||||
##########################################
|
||||
## ✅ Retrofit / OkHttp
|
||||
##########################################
|
||||
-dontwarn okhttp3.**
|
||||
-dontwarn okio.**
|
||||
-dontwarn retrofit2.**
|
||||
-keep class okhttp3.** { *; }
|
||||
-keep class okio.** { *; }
|
||||
-keep interface retrofit2.** { *; }
|
||||
|
||||
##########################################
|
||||
## ✅ Glide / image loading (avoid BigPicture FCM crash)
|
||||
##########################################
|
||||
-dontwarn com.bumptech.glide.**
|
||||
-keep class com.bumptech.glide.** { *; }
|
||||
|
||||
##########################################
|
||||
## ✅ Prevent crashes for reflection
|
||||
##########################################
|
||||
-keepattributes Signature
|
||||
-keepattributes Annotation
|
||||
|
||||
##########################################
|
||||
## ✅ KEEP ENUMS (Firebase uses it)
|
||||
##########################################
|
||||
-keepclassmembers enum * { *; }
|
||||
|
||||
##########################################
|
||||
## ✅ Do NOT break Kotlin (Important)
|
||||
##########################################
|
||||
-keep class kotlin.** { *; }
|
||||
-dontwarn kotlin.**
|
||||
|
||||
##########################################
|
||||
## ✅ GETX (State Management & Snackbars)
|
||||
##########################################
|
||||
# Keep all GetX classes and methods
|
||||
-keep class get.** { *; }
|
||||
-keep class * extends get.GetxController { *; }
|
||||
-keepclassmembers class * extends get.GetxController {
|
||||
<methods>;
|
||||
<fields>;
|
||||
}
|
||||
# Keep GetX navigation and routing
|
||||
-keep class get.Get { *; }
|
||||
-keep class get.GetMaterialApp { *; }
|
||||
-keep class get.GetNavigator { *; }
|
||||
-keep class get.GetSnackbar { *; }
|
||||
# Keep GetX observables and reactive variables
|
||||
-keep class get.Rx* { *; }
|
||||
-keep class get.Obs* { *; }
|
||||
-dontwarn get.**
|
||||
|
||||
##########################################
|
||||
## ✅ Reduce unnecessary warnings
|
||||
##########################################
|
||||
-ignorewarnings
|
||||
7
android/app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
144
android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,144 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
|
||||
<!-- ===== Permissions ===== -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<!-- For older Android versions -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.PICTURE_IN_PICTURE" />
|
||||
<!-- Android 13+ notifications -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<!-- Foreground service for background logging -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<!-- Optional: declare types for Android 14+ -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
|
||||
<!-- Location permissions -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<!-- Optional: needed for background tracking (break auto-refresh, etc.) -->
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
<!-- Phone call (for tap-to-call customer / pickup) -->
|
||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||
|
||||
<!-- Alarm permissions for shift end (works even when app is killed) -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<!-- ===== Application ===== -->
|
||||
<application
|
||||
android:label="Nearle Rider"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true"> <!-- allows http if any -->
|
||||
|
||||
<!-- Google Maps API Key -->
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q" />
|
||||
|
||||
<!-- Firebase notification channel -->
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||
android:value="Nearle" />
|
||||
|
||||
<!-- Foreground task notification channel (for flutter_foreground_task) -->
|
||||
<meta-data
|
||||
android:name="com.pravera.flutter_foreground_task.notification_channel_name"
|
||||
android:value="Background Service" />
|
||||
<meta-data
|
||||
android:name="com.pravera.flutter_foreground_task.notification_channel_description"
|
||||
android:value="Keeps Nearle online updates running in background." />
|
||||
|
||||
<!-- Foreground service component for flutter_foreground_task -->
|
||||
<!-- Using dataSync only - location is accessed separately and doesn't need to be part of foreground service type -->
|
||||
<service
|
||||
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync|location" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:resizeableActivity="true"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
<!-- Theme shown before Flutter UI loads -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Required for Flutter plugin registration -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
<!-- Shift End BroadcastReceiver (works even when app is killed) -->
|
||||
<receiver
|
||||
android:name=".ShiftEndReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.nearle.partner.SHIFT_END_ALARM" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- Optional: Legacy HTTP support (useful for older plugins) -->
|
||||
<uses-library
|
||||
android:name="org.apache.http.legacy"
|
||||
android:required="false" />
|
||||
</application>
|
||||
|
||||
<!-- ===== Package visibility (Android 11+) ===== -->
|
||||
<queries>
|
||||
<!-- Existing text processing visibility -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent>
|
||||
|
||||
<!-- Allow dialer / phone apps for tel: links (Android 11+) -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.DIAL" />
|
||||
<data android:scheme="tel" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="tel" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.CALL" />
|
||||
<data android:scheme="tel" />
|
||||
</intent>
|
||||
|
||||
<!-- Package visibility for common dialer apps -->
|
||||
<package android:name="com.android.dialer" />
|
||||
<package android:name="com.google.android.dialer" />
|
||||
<package android:name="com.samsung.android.dialer" />
|
||||
<package android:name="com.miui.dialer" />
|
||||
<package android:name="com.huawei.dialer" />
|
||||
<package android:name="com.oneplus.dialer" />
|
||||
<package android:name="com.coloros.dialer" />
|
||||
<package android:name="com.oppo.dialer" />
|
||||
<package android:name="com.vivo.dialer" />
|
||||
<package android:name="com.realme.dialer" />
|
||||
</queries>
|
||||
|
||||
</manifest>
|
||||
315
android/app/src/main/kotlin/com/nearle/gear/MainActivity.kt
Normal file
@@ -0,0 +1,315 @@
|
||||
package com.nearle.partner
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.util.Rational
|
||||
import androidx.core.view.WindowCompat
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.util.Calendar
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
private val channelName = "nearle/pip"
|
||||
private val shiftEndChannelName = "nearle/shift_end"
|
||||
|
||||
// PiP tracking for safe exit on resume
|
||||
private val PREFS = "app_prefs"
|
||||
private val KEY_WAS_IN_PIP = "was_in_pip"
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// No automatic PiP entry here – only from explicit Flutter calls
|
||||
}
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
// Enable edge-to-edge for Android 15+ (SDK 35+)
|
||||
if (Build.VERSION.SDK_INT >= 35) {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
}
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName)
|
||||
.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"enterPip" -> {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val aspect = Rational(16, 9)
|
||||
val params = PictureInPictureParams.Builder()
|
||||
.setAspectRatio(aspect)
|
||||
.build()
|
||||
enterPictureInPictureMode(params)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
enterPictureInPictureMode()
|
||||
}
|
||||
result.success(true)
|
||||
} catch (e: Exception) {
|
||||
result.error("PIP_ERROR", e.message, null)
|
||||
}
|
||||
}
|
||||
"moveToBack" -> {
|
||||
try {
|
||||
moveTaskToBack(true)
|
||||
result.success(true)
|
||||
} catch (e: Exception) {
|
||||
result.error("MOVE_BACK_ERROR", e.message, null)
|
||||
}
|
||||
}
|
||||
"hasActiveDeliveries" -> {
|
||||
try {
|
||||
val hasActive = checkActiveDeliveries()
|
||||
result.success(hasActive)
|
||||
} catch (e: Exception) {
|
||||
result.error("CHECK_ACTIVE_ERROR", e.message, null)
|
||||
}
|
||||
}
|
||||
"ensureExitPip" -> {
|
||||
try {
|
||||
ensureExitPip()
|
||||
result.success(true)
|
||||
} catch (e: Exception) {
|
||||
result.error("ENSURE_EXIT_PIP_ERROR", e.message, null)
|
||||
}
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
// Shift end alarm channel
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, shiftEndChannelName)
|
||||
.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"scheduleShiftEndAlarm" -> {
|
||||
try {
|
||||
val endTimeStr = call.argument<String>("endTime") ?: ""
|
||||
val startTimeStr = call.argument<String>("startTime") ?: ""
|
||||
if (endTimeStr.isNotEmpty()) {
|
||||
scheduleShiftEndAlarm(endTimeStr, startTimeStr)
|
||||
result.success(true)
|
||||
} else {
|
||||
result.error("INVALID_TIME", "End time is required", null)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
result.error("ALARM_ERROR", e.message, null)
|
||||
}
|
||||
}
|
||||
"cancelShiftEndAlarm" -> {
|
||||
try {
|
||||
cancelShiftEndAlarm()
|
||||
result.success(true)
|
||||
} catch (e: Exception) {
|
||||
result.error("CANCEL_ERROR", e.message, null)
|
||||
}
|
||||
}
|
||||
"handleShiftEnd" -> {
|
||||
try {
|
||||
// This is called by the BroadcastReceiver
|
||||
// We need to call Flutter code to create break log
|
||||
// Since we're in MainActivity, we can use the existing Flutter engine
|
||||
// But the receiver might not have access to it, so we'll handle it differently
|
||||
// The receiver will directly call the background service method
|
||||
result.success(true)
|
||||
} catch (e: Exception) {
|
||||
result.error("HANDLE_ERROR", e.message, null)
|
||||
}
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Let Flutter-side explicitly control when to enter PiP (navigation only)
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(
|
||||
isInPictureInPictureMode: Boolean,
|
||||
newConfig: Configuration?
|
||||
) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||
// Persist PiP state so we can safely exit it on resume
|
||||
val prefs = getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
prefs.edit().putBoolean(KEY_WAS_IN_PIP, isInPictureInPictureMode).apply()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// When user brings app back, force full-screen (no PiP)
|
||||
ensureExitPip()
|
||||
}
|
||||
|
||||
private fun ensureExitPip() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
|
||||
val prefs = getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
val wasInPip = prefs.getBoolean(KEY_WAS_IN_PIP, false)
|
||||
|
||||
// Clear the persisted flag so we don't auto-re-enter PiP
|
||||
// Note: Android doesn't provide a direct API to exit PiP programmatically,
|
||||
// but when user opens app from launcher, it naturally returns to full-screen.
|
||||
// We just ensure we don't auto-re-enter PiP on resume.
|
||||
if (wasInPip) {
|
||||
prefs.edit().putBoolean(KEY_WAS_IN_PIP, false).apply()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkActiveDeliveries(): Boolean {
|
||||
return try {
|
||||
val prefs: SharedPreferences = getSharedPreferences(
|
||||
"FlutterSharedPreferences",
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
// Only check has_live_deliveries (verified by API)
|
||||
val hasLive = prefs.getBoolean("flutter.has_live_deliveries", false)
|
||||
hasLive
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleShiftEndAlarm(endTimeStr: String, startTimeStr: String) {
|
||||
try {
|
||||
Log.d("MainActivity", "📅 Scheduling shift end alarm - endTime: $endTimeStr, startTime: $startTimeStr")
|
||||
|
||||
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
val intent = Intent(this, ShiftEndReceiver::class.java).apply {
|
||||
action = "com.nearle.partner.SHIFT_END_ALARM"
|
||||
}
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
this,
|
||||
1001, // Unique request code
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
// Parse end time (format: HH:mm:ss or HH:mm)
|
||||
val timeParts = endTimeStr.split(":")
|
||||
if (timeParts.size < 2) {
|
||||
Log.e("MainActivity", "❌ Invalid end time format: $endTimeStr")
|
||||
return
|
||||
}
|
||||
|
||||
val endHour = timeParts[0].toIntOrNull() ?: 0
|
||||
val endMinute = timeParts[1].toIntOrNull() ?: 0
|
||||
|
||||
val calendar = Calendar.getInstance().apply {
|
||||
set(Calendar.HOUR_OF_DAY, endHour)
|
||||
set(Calendar.MINUTE, endMinute)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}
|
||||
|
||||
// Handle overnight shifts (if start > end)
|
||||
if (startTimeStr.isNotEmpty()) {
|
||||
val startParts = startTimeStr.split(":")
|
||||
if (startParts.size >= 2) {
|
||||
val startHour = startParts[0].toIntOrNull() ?: 0
|
||||
val startMinute = startParts[1].toIntOrNull() ?: 0
|
||||
|
||||
if (startHour > endHour || (startHour == endHour && startMinute > endMinute)) {
|
||||
// Overnight shift - if current time is before end time, schedule for tomorrow
|
||||
val now = Calendar.getInstance()
|
||||
if (now.before(calendar)) {
|
||||
calendar.add(Calendar.DAY_OF_MONTH, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ CRITICAL: If shift end time has already passed today, trigger immediately
|
||||
val now = Calendar.getInstance()
|
||||
Log.d("MainActivity", "Current time: ${now.time}, Shift end time: ${calendar.time}")
|
||||
|
||||
if (calendar.before(now) || calendar.equals(now)) {
|
||||
// Shift end time has passed - trigger break log immediately
|
||||
Log.d("MainActivity", "⚡ Shift end time ($endTimeStr) has already passed - triggering break log immediately")
|
||||
|
||||
// Trigger the receiver immediately
|
||||
val immediateIntent = Intent(this, ShiftEndReceiver::class.java).apply {
|
||||
action = "com.nearle.partner.SHIFT_END_ALARM"
|
||||
}
|
||||
sendBroadcast(immediateIntent)
|
||||
Log.d("MainActivity", "✅ Sent immediate broadcast to ShiftEndReceiver")
|
||||
|
||||
// Also schedule for tomorrow to handle next shift
|
||||
calendar.add(Calendar.DAY_OF_MONTH, 1)
|
||||
Log.d("MainActivity", "📅 Also scheduled shift end alarm for tomorrow: ${calendar.time}")
|
||||
}
|
||||
|
||||
// Schedule exact alarm (Android 12+ requires SCHEDULE_EXACT_ALARM permission)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (alarmManager.canScheduleExactAlarms()) {
|
||||
alarmManager.setExactAndAllowWhileIdle(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
calendar.timeInMillis,
|
||||
pendingIntent
|
||||
)
|
||||
Log.d("MainActivity", "✅ Scheduled exact shift end alarm for: ${calendar.time} (${calendar.timeInMillis})")
|
||||
} else {
|
||||
// Fallback to inexact alarm
|
||||
alarmManager.setAndAllowWhileIdle(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
calendar.timeInMillis,
|
||||
pendingIntent
|
||||
)
|
||||
Log.w("MainActivity", "⚠️ Scheduled inexact shift end alarm (no permission) for: ${calendar.time}")
|
||||
}
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
alarmManager.setExactAndAllowWhileIdle(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
calendar.timeInMillis,
|
||||
pendingIntent
|
||||
)
|
||||
Log.d("MainActivity", "✅ Scheduled exact shift end alarm (Android M+) for: ${calendar.time}")
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
alarmManager.setExact(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
calendar.timeInMillis,
|
||||
pendingIntent
|
||||
)
|
||||
Log.d("MainActivity", "✅ Scheduled exact shift end alarm (legacy) for: ${calendar.time}")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "❌ Error scheduling shift end alarm: ${e.message}", e)
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelShiftEndAlarm() {
|
||||
try {
|
||||
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
val intent = Intent(this, ShiftEndReceiver::class.java).apply {
|
||||
action = "com.nearle.partner.SHIFT_END_ALARM"
|
||||
}
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
this,
|
||||
1001,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
alarmManager.cancel(pendingIntent)
|
||||
Log.d("MainActivity", "Cancelled shift end alarm")
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "Error cancelling shift end alarm: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
278
android/app/src/main/kotlin/com/nearle/gear/ShiftEndReceiver.kt
Normal file
@@ -0,0 +1,278 @@
|
||||
package com.nearle.partner
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.PowerManager
|
||||
import android.util.Log
|
||||
import java.io.OutputStreamWriter
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* BroadcastReceiver that triggers when shift end alarm fires
|
||||
* This works even when app is killed
|
||||
*
|
||||
* When app is killed, we directly call the API to create break log
|
||||
* since we can't reliably start Flutter engine
|
||||
*/
|
||||
class ShiftEndReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Log.d("ShiftEndReceiver", "🔔 Shift end alarm triggered at ${java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US).format(java.util.Calendar.getInstance().time)}")
|
||||
|
||||
// ✅ CRITICAL: Acquire WakeLock to keep device awake during API call
|
||||
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
val wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "ShiftEndReceiver::WakeLock")
|
||||
wakeLock.acquire(60 * 1000L) // Hold for 60 seconds max
|
||||
|
||||
// Execute API call in background thread
|
||||
Thread {
|
||||
try {
|
||||
// Check if rider is still on duty
|
||||
val prefs: SharedPreferences = context.getSharedPreferences(
|
||||
"FlutterSharedPreferences",
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
|
||||
// ✅ Try multiple key formats (Flutter SharedPreferences uses "flutter." prefix)
|
||||
val onduty = prefs.getInt("flutter.onduty", 0)
|
||||
Log.d("ShiftEndReceiver", "Checking onduty: $onduty")
|
||||
|
||||
if (onduty != 1) {
|
||||
Log.d("ShiftEndReceiver", "Rider already offline (onduty=$onduty), skipping break log creation")
|
||||
return@Thread
|
||||
}
|
||||
|
||||
// Get required IDs - try both flutter.userid and flutter.userId
|
||||
val userid = prefs.getInt("flutter.userid", 0).takeIf { it != 0 }
|
||||
?: prefs.getInt("flutter.userId", 0)
|
||||
val partnerid = prefs.getInt("flutter.partnerid", 0).takeIf { it != 0 }
|
||||
?: prefs.getInt("flutter.partnerId", 0)
|
||||
val shiftid = prefs.getInt("flutter.shiftid", 0).takeIf { it != 0 }
|
||||
?: prefs.getInt("flutter.shiftId", 0)
|
||||
val logid = prefs.getInt("flutter.logid", 0).takeIf { it != 0 }
|
||||
?: prefs.getInt("flutter.logId", 0)
|
||||
|
||||
Log.d("ShiftEndReceiver", "Retrieved IDs: userid=$userid, partnerid=$partnerid, shiftid=$shiftid, logid=$logid")
|
||||
|
||||
if (userid == 0) {
|
||||
Log.e("ShiftEndReceiver", "❌ Missing userid, cannot create break log. Available keys: ${prefs.all.keys}")
|
||||
return@Thread
|
||||
}
|
||||
|
||||
// Get API base URL (check if live or dev)
|
||||
// ✅ Match homepage logic: use createbreaklog (not createbreakriderlog)
|
||||
val mainRoute = prefs.getString("flutter.mainRoute", "dev") ?: "dev"
|
||||
val isLive = mainRoute == "live"
|
||||
Log.d("ShiftEndReceiver", "API Route: $mainRoute (isLive=$isLive)")
|
||||
val baseUrl = if (isLive) {
|
||||
"https://jupiter.nearle.app/live/api/v2/partners/createbreaklog"
|
||||
} else {
|
||||
"https://jupiter.nearle.app/dev/api/v2/partners/createbreaklog"
|
||||
}
|
||||
|
||||
// Create break log payload
|
||||
val now = Calendar.getInstance()
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
|
||||
val timeFormat = SimpleDateFormat("HH:mm:ss", Locale.US)
|
||||
val breakdate = dateFormat.format(now.time)
|
||||
val breakstart = timeFormat.format(now.time)
|
||||
val localBreakId = (System.currentTimeMillis() % 900).toInt() + 100
|
||||
|
||||
// ✅ Compact JSON (no extra whitespace) - matches Flutter format
|
||||
val payload = """{"breakid":$localBreakId,"logid":$logid,"breakdate":"$breakdate","userid":$userid,"partnerid":$partnerid,"shiftid":$shiftid,"breakstart":"$breakstart","breakend":"","breakhours":0.0,"latitude":"0","longitude":"0"}"""
|
||||
|
||||
Log.d("ShiftEndReceiver", "📤 Sending break log request to: $baseUrl")
|
||||
Log.d("ShiftEndReceiver", "📦 Payload: $payload")
|
||||
|
||||
// Make API call
|
||||
val url = URL(baseUrl)
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "POST"
|
||||
connection.setRequestProperty("Content-Type", "application/json")
|
||||
connection.setRequestProperty("Accept", "application/json")
|
||||
connection.doOutput = true
|
||||
connection.doInput = true
|
||||
connection.useCaches = false
|
||||
connection.connectTimeout = 15000 // Increased timeout
|
||||
connection.readTimeout = 15000
|
||||
|
||||
Log.d("ShiftEndReceiver", "🔌 Connecting to API...")
|
||||
|
||||
// Write payload
|
||||
val outputStream = connection.outputStream
|
||||
val writer = OutputStreamWriter(outputStream, "UTF-8")
|
||||
writer.write(payload)
|
||||
writer.flush()
|
||||
writer.close()
|
||||
|
||||
Log.d("ShiftEndReceiver", "📨 Request sent, waiting for response...")
|
||||
|
||||
val responseCode = connection.responseCode
|
||||
Log.d("ShiftEndReceiver", "📥 Break log API response: HTTP $responseCode")
|
||||
|
||||
// Read response body for debugging
|
||||
try {
|
||||
val responseStream = if (responseCode in 200..299) {
|
||||
connection.inputStream
|
||||
} else {
|
||||
connection.errorStream
|
||||
}
|
||||
if (responseStream != null) {
|
||||
val responseBody = responseStream.bufferedReader().use { it.readText() }
|
||||
Log.d("ShiftEndReceiver", "📄 Response body: $responseBody")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("ShiftEndReceiver", "Could not read response body: ${e.message}")
|
||||
}
|
||||
|
||||
if (responseCode in 200..299) {
|
||||
Log.d("ShiftEndReceiver", "✅ Break log created successfully (HTTP $responseCode)")
|
||||
|
||||
// Set offline locally
|
||||
prefs.edit()
|
||||
.putInt("flutter.onduty", 0)
|
||||
.putBoolean("flutter.online", false)
|
||||
.apply()
|
||||
|
||||
// Also update rider log to set onduty=0
|
||||
val updateUrl = if (isLive) {
|
||||
"https://jupiter.nearle.app/live/api/v2/partners/updateriderlog"
|
||||
} else {
|
||||
"https://jupiter.nearle.app/dev/api/v2/partners/updateriderlog"
|
||||
}
|
||||
|
||||
// ✅ Compact JSON (no extra whitespace)
|
||||
val updatePayload = """{"userid":$userid,"onduty":0,"latitude":"0","longitude":"0"}"""
|
||||
|
||||
Log.d("ShiftEndReceiver", "📤 Updating rider status to offline")
|
||||
|
||||
try {
|
||||
val updateConnection = URL(updateUrl).openConnection() as HttpURLConnection
|
||||
updateConnection.requestMethod = "POST"
|
||||
updateConnection.setRequestProperty("Content-Type", "application/json")
|
||||
updateConnection.doOutput = true
|
||||
updateConnection.connectTimeout = 5000
|
||||
updateConnection.readTimeout = 5000
|
||||
|
||||
val updateWriter = OutputStreamWriter(updateConnection.outputStream, "UTF-8")
|
||||
updateWriter.write(updatePayload)
|
||||
updateWriter.flush()
|
||||
updateWriter.close()
|
||||
|
||||
if (updateConnection.responseCode in 200..299) {
|
||||
Log.d("ShiftEndReceiver", "✅ Rider status updated to Offline")
|
||||
}
|
||||
updateConnection.disconnect()
|
||||
} catch (e: Exception) {
|
||||
Log.e("ShiftEndReceiver", "Error updating rider status: ${e.message}")
|
||||
}
|
||||
} else {
|
||||
// Read error response for debugging
|
||||
try {
|
||||
val errorStream = connection.errorStream
|
||||
if (errorStream != null) {
|
||||
val errorResponse = errorStream.bufferedReader().use { it.readText() }
|
||||
Log.e("ShiftEndReceiver", "❌ Failed to create break log: HTTP $responseCode\nError: $errorResponse")
|
||||
} else {
|
||||
Log.e("ShiftEndReceiver", "❌ Failed to create break log: HTTP $responseCode")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("ShiftEndReceiver", "❌ Failed to create break log: HTTP $responseCode (Error reading response: ${e.message})")
|
||||
}
|
||||
}
|
||||
|
||||
connection.disconnect()
|
||||
|
||||
// ✅ CRITICAL: Reschedule alarm for tomorrow (so it works every day automatically)
|
||||
// This ensures the alarm fires every day at shift end time even if app is killed
|
||||
try {
|
||||
val endTimeStr = prefs.getString("flutter.endtime", "") ?: ""
|
||||
val startTimeStr = prefs.getString("flutter.starttime", "") ?: ""
|
||||
Log.d("ShiftEndReceiver", "Rescheduling alarm - endTime: $endTimeStr, startTime: $startTimeStr")
|
||||
|
||||
if (endTimeStr.isNotEmpty()) {
|
||||
// Schedule alarm for tomorrow at the same time
|
||||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
val intent = Intent(context, ShiftEndReceiver::class.java).apply {
|
||||
action = "com.nearle.partner.SHIFT_END_ALARM"
|
||||
}
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
1001,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val timeParts = endTimeStr.split(":")
|
||||
if (timeParts.size >= 2) {
|
||||
val endHour = timeParts[0].toIntOrNull() ?: 0
|
||||
val endMinute = timeParts[1].toIntOrNull() ?: 0
|
||||
|
||||
val calendar = Calendar.getInstance().apply {
|
||||
add(Calendar.DAY_OF_MONTH, 1) // Tomorrow
|
||||
set(Calendar.HOUR_OF_DAY, endHour)
|
||||
set(Calendar.MINUTE, endMinute)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
|
||||
if (alarmManager.canScheduleExactAlarms()) {
|
||||
alarmManager.setExactAndAllowWhileIdle(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
calendar.timeInMillis,
|
||||
pendingIntent
|
||||
)
|
||||
Log.d("ShiftEndReceiver", "✅ Rescheduled alarm for tomorrow: ${calendar.time}")
|
||||
} else {
|
||||
alarmManager.setAndAllowWhileIdle(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
calendar.timeInMillis,
|
||||
pendingIntent
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
||||
alarmManager.setExactAndAllowWhileIdle(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
calendar.timeInMillis,
|
||||
pendingIntent
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
alarmManager.setExact(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
calendar.timeInMillis,
|
||||
pendingIntent
|
||||
)
|
||||
}
|
||||
Log.d("ShiftEndReceiver", "✅ Rescheduled alarm for tomorrow: ${calendar.time}")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("ShiftEndReceiver", "Error rescheduling alarm: ${e.message}", e)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("ShiftEndReceiver", "❌ Error handling shift end: ${e.message}", e)
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
// ✅ CRITICAL: Release WakeLock in finally block
|
||||
try {
|
||||
if (wakeLock.isHeld) {
|
||||
wakeLock.release()
|
||||
Log.d("ShiftEndReceiver", "🔓 WakeLock released")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("ShiftEndReceiver", "Error releasing WakeLock: ${e.message}")
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
BIN
android/app/src/main/res/drawable-hdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 153 B |
BIN
android/app/src/main/res/drawable-hdpi/splash.png
Normal file
|
After Width: | Height: | Size: 153 B |
BIN
android/app/src/main/res/drawable-mdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 121 B |
BIN
android/app/src/main/res/drawable-mdpi/splash.png
Normal file
|
After Width: | Height: | Size: 121 B |
BIN
android/app/src/main/res/drawable-night-hdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 153 B |
BIN
android/app/src/main/res/drawable-night-mdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 121 B |
|
After Width: | Height: | Size: 239 B |
|
After Width: | Height: | Size: 358 B |
|
After Width: | Height: | Size: 468 B |
BIN
android/app/src/main/res/drawable-v21/background.png
Normal file
|
After Width: | Height: | Size: 69 B |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
BIN
android/app/src/main/res/drawable-xhdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 239 B |
BIN
android/app/src/main/res/drawable-xhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 239 B |
BIN
android/app/src/main/res/drawable-xxhdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 358 B |
BIN
android/app/src/main/res/drawable-xxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 358 B |
BIN
android/app/src/main/res/drawable-xxxhdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 468 B |
BIN
android/app/src/main/res/drawable-xxxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 468 B |
BIN
android/app/src/main/res/drawable/background.png
Normal file
|
After Width: | Height: | Size: 69 B |
9
android/app/src/main/res/drawable/launch_background.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
android/app/src/main/res/raw/alert_1.mp3
Normal file
BIN
android/app/src/main/res/raw/alert_10.mp3
Normal file
BIN
android/app/src/main/res/raw/alert_2.mp3
Normal file
BIN
android/app/src/main/res/raw/alert_3.mp3
Normal file
BIN
android/app/src/main/res/raw/alert_4.mp3
Normal file
BIN
android/app/src/main/res/raw/alert_5.mp3
Normal file
BIN
android/app/src/main/res/raw/alert_6.mp3
Normal file
BIN
android/app/src/main/res/raw/alert_7.mp3
Normal file
BIN
android/app/src/main/res/raw/alert_8.mp3
Normal file
BIN
android/app/src/main/res/raw/alert_9.mp3
Normal file
BIN
android/app/src/main/res/raw/destination.mp3
Normal file
22
android/app/src/main/res/values-night-v31/styles.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowSplashScreenBackground">#FFFFFF</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
|
||||
<item name="android:windowSplashScreenIconBackgroundColor">#FFFFFF</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
22
android/app/src/main/res/values-night/styles.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
22
android/app/src/main/res/values-v31/styles.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowSplashScreenBackground">#FFFFFF</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
|
||||
<item name="android:windowSplashScreenIconBackgroundColor">#FFFFFF</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
6
android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
</style>
|
||||
</resources>
|
||||
7
android/app/src/profile/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
24
android/build.gradle.kts
Normal file
@@ -0,0 +1,24 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
val newBuildDir: Directory =
|
||||
rootProject.layout.buildDirectory
|
||||
.dir("../../build")
|
||||
.get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
7
android/gradle.properties
Normal file
@@ -0,0 +1,7 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||
android.builtInKotlin=false
|
||||
# This newDsl flag was added automatically by Flutter migrator
|
||||
android.newDsl=false
|
||||
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
|
||||
4
android/key.properties
Normal file
@@ -0,0 +1,4 @@
|
||||
storePassword=123456789
|
||||
keyPassword=123456789
|
||||
keyAlias= nearle
|
||||
storeFile=nearlerider-keystore.jks
|
||||
27
android/settings.gradle.kts
Normal file
@@ -0,0 +1,27 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath =
|
||||
run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.9.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||
id("com.google.gms.google-services") version "4.4.2" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
BIN
assets/audio/alert-1.mp3
Normal file
BIN
assets/audio/alert-10.mp3
Normal file
BIN
assets/audio/alert-2.mp3
Normal file
BIN
assets/audio/alert-3.mp3
Normal file
BIN
assets/audio/alert-4.mp3
Normal file
BIN
assets/audio/alert-5.mp3
Normal file
BIN
assets/audio/alert-6.mp3
Normal file
BIN
assets/audio/alert-7.mp3
Normal file
BIN
assets/audio/alert-8.mp3
Normal file
BIN
assets/audio/alert-9.mp3
Normal file
BIN
assets/audio/destination.mp3
Normal file
BIN
assets/fonts/ProximaNova/proximanova_bold.otf
Normal file
BIN
assets/fonts/ProximaNova/proximanova_regular.ttf
Normal file
BIN
assets/images/CreateMpin.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
assets/images/Gps.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
assets/images/Mpin.png
Normal file
|
After Width: | Height: | Size: 277 KiB |
BIN
assets/images/Nearle Bike.png
Normal file
|
After Width: | Height: | Size: 480 KiB |
BIN
assets/images/cancel.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
assets/images/customermap.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/images/deliveryicon.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
assets/images/gps-navigation.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
assets/images/homeicon.png
Normal file
|
After Width: | Height: | Size: 965 B |
BIN
assets/images/in.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/images/information-point.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
assets/images/intro1.png
Normal file
|
After Width: | Height: | Size: 350 KiB |
BIN
assets/images/intro2.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
assets/images/intro3.png
Normal file
|
After Width: | Height: | Size: 744 KiB |
BIN
assets/images/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
assets/images/map.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
assets/images/nearlelauncher.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
assets/images/nearleplaystore1.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
assets/images/nearlesplash2.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
assets/images/onlinebottom.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
assets/images/onlineoffline.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
assets/images/orderssample.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
assets/images/pending.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
assets/images/phone-call .png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
assets/images/profileicon.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
assets/images/selcart.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
assets/images/selecteddelivery.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/images/selectedprofile.png
Normal file
|
After Width: | Height: | Size: 17 KiB |