updated the sheet

This commit is contained in:
2026-08-28 18:16:28 +05:30
parent 69f4f3e909
commit 074cc0eccf
43 changed files with 2617 additions and 1201 deletions

View File

@@ -33,7 +33,7 @@
<!-- ===== Application ===== -->
<!-- usesCleartextTraffic was `true`, commented "allows http if any". There
is no http:// endpoint left in the app: every host it talks to —
api.doormile.com, queue.workolik.com, the OSM tile server and the OSRM
api.doormile.com, the OSM tile server and the OSRM
router — is https, and all of them present valid publicly trusted
certificates.
@@ -44,7 +44,7 @@
the certificate bypass in MyHttpOverrides, which was the other half
of the same hole. -->
<application
android:label="Doormile Rider"
android:label="Miler"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="false">

View File

@@ -1,278 +1,232 @@
package com.doormile.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.doormile.app/live/api/v2/partners/createbreaklog"
} else {
"https://jupiter.doormile.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.doormile.app/live/api/v2/partners/updateriderlog"
} else {
"https://jupiter.doormile.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.doormile.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()
}
}
package com.doormile.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)
if (userid == 0) {
Log.e("ShiftEndReceiver", "❌ Missing userid, cannot create break log. Available keys: ${prefs.all.keys}")
return@Thread
}
// ── v1, with the session, and no ids in the body ──
//
// This block hand-posted two legacy routes over HttpURLConnection:
// partners/createbreaklog and partners/updateriderlog, on
// the retired pre-v1 backend,
// with `userid`, `partnerid`, `shiftid` and `logid` in the body and
// NO Authorization header — an unauthenticated write keyed on ids
// read out of SharedPreferences.
//
// The v1 equivalents key on the bearer token instead: the server
// knows which rider the session names, so the ids are neither sent
// nor needed. Same two effects, one host, one auth model as the
// rest of the app.
//
// No token means no shift to end. That is the correct outcome —
// a signed-out handset must not be able to close somebody's duty.
val token = prefs.getString("flutter.authtoken", "") ?: ""
if (token.isEmpty()) {
Log.e("ShiftEndReceiver", "No session token; cannot end shift")
return@Thread
}
val breakOk = callV1(
path = "/miler/breaks/start",
method = "POST",
token = token,
body = """{"breaktype":"Shift_End"}"""
)
if (breakOk) {
Log.d("ShiftEndReceiver", "Break log created successfully")
prefs.edit()
.putInt("flutter.onduty", 0)
.putBoolean("flutter.online", false)
.apply()
// `PUT /miler/duty/end` takes no body and closes whichever duty
// log is open for the token's rider.
if (callV1("/miler/duty/end", "PUT", token, null)) {
Log.d("ShiftEndReceiver", "Rider status updated to Offline")
}
} else {
Log.e("ShiftEndReceiver", "Failed to create break log")
}
// ✅ 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.doormile.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()
}
/**
* One authenticated call to the v1 backend. Returns true on 2xx.
*
* The single place this file talks to a network, so there is one base URL,
* one auth model and one timeout policy rather than a copy per endpoint.
*/
private fun callV1(path: String, method: String, token: String, body: String?): Boolean {
var connection: HttpURLConnection? = null
return try {
connection = URL(V1_BASE + path).openConnection() as HttpURLConnection
connection.requestMethod = method
connection.setRequestProperty("Content-Type", "application/json")
connection.setRequestProperty("Accept", "application/json")
connection.setRequestProperty("Authorization", "Bearer $token")
connection.connectTimeout = 15000
connection.readTimeout = 15000
if (body != null) {
connection.doOutput = true
OutputStreamWriter(connection.outputStream, "UTF-8").use {
it.write(body)
it.flush()
}
}
val code = connection.responseCode
Log.d("ShiftEndReceiver", "$method $path -> HTTP $code")
code in 200..299
} catch (e: Exception) {
Log.e("ShiftEndReceiver", "$method $path failed: ${e.message}")
false
} finally {
connection?.disconnect()
}
}
companion object {
/** The one backend this app talks to. */
private const val V1_BASE = "https://api.doormile.com/api/v1"
}
}