feat: add device authentication, place gates support, and PiP navigation features
@@ -3,6 +3,7 @@ import java.io.FileInputStream
|
||||
|
||||
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")
|
||||
}
|
||||
@@ -14,7 +15,7 @@ if (keystorePropertiesFile.exists()) {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.siro_map.siro_maps"
|
||||
namespace = "com.urukmap.app"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
@@ -23,9 +24,13 @@ android {
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.siro_map.siro_maps"
|
||||
minSdk = 23
|
||||
applicationId = "com.urukmap.app"
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
@@ -71,12 +76,6 @@ dependencies {
|
||||
implementation("androidx.car.app:app-projected:1.4.0")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
-keep class androidx.car.app.validation.** { *; }
|
||||
|
||||
# Keep our custom CarAppService, Session, Screen and State models
|
||||
-keep class com.siro_map.siro_maps.car.** { *; }
|
||||
-keepclassmembers class com.siro_map.siro_maps.car.** { *; }
|
||||
-keep class com.urukmap.app.car.** { *; }
|
||||
-keepclassmembers class com.urukmap.app.car.** { *; }
|
||||
|
||||
-dontwarn androidx.car.app.**
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
|
||||
<application
|
||||
android:label="خرائط سيرو"
|
||||
android:label="Uruk Map"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
@@ -51,6 +51,23 @@
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="geo" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Google Navigation Scheme: google.navigation:q=lat,lng -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="google.navigation" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Universal Web Deep Links: maps.siro.app & map-saas.intaleqapp.com -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" android:host="maps.siro.app" />
|
||||
<data android:scheme="https" android:host="map-saas.intaleqapp.com" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
package com.siro_map.siro_maps
|
||||
|
||||
import com.siro_map.siro_maps.car.CarNavigationState
|
||||
import com.siro_map.siro_maps.car.SiroCarAppService
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
private val CHANNEL = "com.siro.siro_maps/car_navigation"
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"isCarAppConnected" -> {
|
||||
result.success(SiroCarAppService.isConnected)
|
||||
}
|
||||
"updateNavState" -> {
|
||||
try {
|
||||
val lat = call.argument<Double>("lat") ?: 0.0
|
||||
val lng = call.argument<Double>("lng") ?: 0.0
|
||||
val bearing = call.argument<Double>("bearing") ?: 0.0
|
||||
val speed = call.argument<Double>("speed") ?: 0.0
|
||||
val instruction = call.argument<String>("instruction") ?: ""
|
||||
val distanceToStep = call.argument<Double>("distanceToStep") ?: 0.0
|
||||
val totalDistance = call.argument<Double>("totalDistance") ?: 0.0
|
||||
val eta = call.argument<Double>("eta") ?: 0.0
|
||||
val maneuver = call.argument<Int>("maneuver") ?: 0
|
||||
val isNavigating = call.argument<Boolean>("isNavigating") ?: false
|
||||
val isMapDarkMode = call.argument<Boolean>("isMapDarkMode") ?: false
|
||||
|
||||
val newState = CarNavigationState(
|
||||
lat = lat,
|
||||
lng = lng,
|
||||
bearing = bearing,
|
||||
speed = speed,
|
||||
instruction = instruction,
|
||||
distanceToStep = distanceToStep,
|
||||
totalDistance = totalDistance,
|
||||
eta = eta,
|
||||
maneuver = maneuver,
|
||||
isNavigating = isNavigating,
|
||||
isMapDarkMode = isMapDarkMode
|
||||
)
|
||||
SiroCarAppService.updateNavState(newState)
|
||||
result.success(true)
|
||||
} catch (e: Exception) {
|
||||
result.error("UPDATE_FAILED", e.localizedMessage, null)
|
||||
}
|
||||
}
|
||||
"stopNavigation" -> {
|
||||
SiroCarAppService.stopNavigation()
|
||||
result.success(true)
|
||||
}
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package com.urukmap.app
|
||||
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Rational
|
||||
import com.urukmap.app.car.CarNavigationState
|
||||
import com.urukmap.app.car.SiroCarAppService
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
private val CAR_CHANNEL = "com.siro.siro_maps/car_navigation"
|
||||
private val PIP_CHANNEL = "com.siro.siro_maps/pip"
|
||||
private val DEEP_LINK_CHANNEL = "com.siro.siro_maps/deep_link"
|
||||
|
||||
private var pipMethodChannel: MethodChannel? = null
|
||||
private var deepLinkMethodChannel: MethodChannel? = null
|
||||
private var pendingDeepLink: String? = null
|
||||
private var isNavigating: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
handleIntent(intent)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
handleIntent(intent)
|
||||
}
|
||||
|
||||
private fun handleIntent(intent: Intent?) {
|
||||
if (intent?.action == Intent.ACTION_VIEW) {
|
||||
val dataString = intent.dataString
|
||||
if (!dataString.isNullOrEmpty()) {
|
||||
pendingDeepLink = dataString
|
||||
deepLinkMethodChannel?.invokeMethod("onDeepLink", mapOf("url" to dataString))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
// 1. Car Navigation Channel
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CAR_CHANNEL).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"isCarAppConnected" -> {
|
||||
result.success(SiroCarAppService.isConnected)
|
||||
}
|
||||
"updateNavState" -> {
|
||||
try {
|
||||
val lat = call.argument<Double>("lat") ?: 0.0
|
||||
val lng = call.argument<Double>("lng") ?: 0.0
|
||||
val bearing = call.argument<Double>("bearing") ?: 0.0
|
||||
val speed = call.argument<Double>("speed") ?: 0.0
|
||||
val instruction = call.argument<String>("instruction") ?: ""
|
||||
val distanceToStep = call.argument<Double>("distanceToStep") ?: 0.0
|
||||
val totalDistance = call.argument<Double>("totalDistance") ?: 0.0
|
||||
val eta = call.argument<Double>("eta") ?: 0.0
|
||||
val maneuver = call.argument<Int>("maneuver") ?: 0
|
||||
val isNav = call.argument<Boolean>("isNavigating") ?: false
|
||||
val isMapDarkMode = call.argument<Boolean>("isMapDarkMode") ?: false
|
||||
|
||||
updateNavigationState(isNav)
|
||||
|
||||
val newState = CarNavigationState(
|
||||
lat = lat,
|
||||
lng = lng,
|
||||
bearing = bearing,
|
||||
speed = speed,
|
||||
instruction = instruction,
|
||||
distanceToStep = distanceToStep,
|
||||
totalDistance = totalDistance,
|
||||
eta = eta,
|
||||
maneuver = maneuver,
|
||||
isNavigating = isNav,
|
||||
isMapDarkMode = isMapDarkMode
|
||||
)
|
||||
SiroCarAppService.updateNavState(newState)
|
||||
result.success(true)
|
||||
} catch (e: Exception) {
|
||||
result.error("UPDATE_FAILED", e.localizedMessage, null)
|
||||
}
|
||||
}
|
||||
"stopNavigation" -> {
|
||||
updateNavigationState(false)
|
||||
SiroCarAppService.stopNavigation()
|
||||
result.success(true)
|
||||
}
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Deep Link Channel
|
||||
deepLinkMethodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, DEEP_LINK_CHANNEL).apply {
|
||||
setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"getInitialLink" -> {
|
||||
val link = pendingDeepLink
|
||||
pendingDeepLink = null
|
||||
result.success(link)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deliver pending deep link if arrived before engine configuration
|
||||
pendingDeepLink?.let { link ->
|
||||
deepLinkMethodChannel?.invokeMethod("onDeepLink", mapOf("url" to link))
|
||||
}
|
||||
|
||||
// 3. Picture-in-Picture (PiP) Channel
|
||||
pipMethodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, PIP_CHANNEL).apply {
|
||||
setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"enterPictureInPicture" -> {
|
||||
val success = enterPipMode()
|
||||
result.success(success)
|
||||
}
|
||||
"isPipSupported" -> {
|
||||
val supported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
result.success(supported)
|
||||
}
|
||||
"setNavigating" -> {
|
||||
val navigating = call.argument<Boolean>("isNavigating") ?: false
|
||||
updateNavigationState(navigating)
|
||||
result.success(true)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Device Hardware Channel
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.siro.siro_maps/device_hardware").setMethodCallHandler { call, result ->
|
||||
if (call.method == "getHardwareInfo") {
|
||||
val androidId = android.provider.Settings.Secure.getString(contentResolver, android.provider.Settings.Secure.ANDROID_ID) ?: ""
|
||||
val info = mapOf(
|
||||
"hardwareId" to androidId,
|
||||
"brand" to Build.BRAND,
|
||||
"manufacturer" to Build.MANUFACTURER,
|
||||
"model" to Build.MODEL,
|
||||
"device" to Build.DEVICE,
|
||||
"board" to Build.BOARD,
|
||||
"hardware" to Build.HARDWARE
|
||||
)
|
||||
result.success(info)
|
||||
} else {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNavigationState(navigating: Boolean) {
|
||||
isNavigating = navigating
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
try {
|
||||
val pipParams = PictureInPictureParams.Builder()
|
||||
.setAutoEnterEnabled(navigating)
|
||||
.setAspectRatio(Rational(3, 4))
|
||||
.build()
|
||||
setPictureInPictureParams(pipParams)
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun enterPipMode(): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
|
||||
return try {
|
||||
val pipParams = PictureInPictureParams.Builder()
|
||||
.setAspectRatio(Rational(3, 4))
|
||||
.build()
|
||||
enterPictureInPictureMode(pipParams)
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
// Auto-enter PiP on Android 8.0 - 11 when user presses Home while navigating
|
||||
if (isNavigating && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
enterPipMode()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||
pipMethodChannel?.invokeMethod("onPipChanged", mapOf("isInPip" to isInPictureInPictureMode))
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.siro_map.siro_maps.car
|
||||
package com.urukmap.app.car
|
||||
|
||||
data class CarNavigationState(
|
||||
val lat: Double = 0.0,
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.siro_map.siro_maps.car
|
||||
package com.urukmap.app.car
|
||||
|
||||
import androidx.car.app.CarAppService
|
||||
import androidx.car.app.Session
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.siro_map.siro_maps.car
|
||||
package com.urukmap.app.car
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.car.app.Screen
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.siro_map.siro_maps.car
|
||||
package com.urukmap.app.car
|
||||
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 56 KiB |
@@ -2,4 +2,5 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "9.0.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
|
||||
id("com.android.application") version "8.11.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
|
||||