Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ android {

dependencies {
coreLibraryDesugaring(libs.desugar)
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar", "*.aar"))))
implementation(platform(libs.firebase.bom))
implementation(libs.activity)
implementation(libs.browser)
Expand Down
Empty file added app/libs/aa_sdk_v105
Empty file.
Binary file added app/libs/android-support-car.aar
Binary file not shown.
Binary file added app/libs/gearhead-sdk.aar
Binary file not shown.
14 changes: 14 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

<queries>
<package android:name="com.nianticlabs.pokemongo" />
Expand Down Expand Up @@ -60,6 +61,16 @@
<data android:queryPrefix="daddr=" />
</intent-filter>
</activity>
<service
android:name=".auto.MainService"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="com.google.android.gms.car.category.CATEGORY_PROJECTION" />
<category android:name="com.google.android.gms.car.category.CATEGORY_PROJECTION_OEM" />
</intent-filter>
</service>
<receiver
android:name=".follower.BackgroundLocationReceiver"
android:directBootAware="true"
Expand All @@ -72,6 +83,9 @@
</intent-filter>
</receiver>

<meta-data android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc" />

<service
android:name="androidx.work.impl.background.systemalarm.SystemAlarmService"
android:directBootAware="true"
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/java/be/mygod/reactmap/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.getSystemService
import androidx.work.Configuration
import androidx.work.WorkManager
import be.mygod.reactmap.auto.CarSiteController
import be.mygod.reactmap.follower.BackgroundLocationReceiver
import be.mygod.reactmap.follower.LocationSetter
import be.mygod.reactmap.util.DeviceStorageApp
Expand Down Expand Up @@ -86,6 +87,11 @@ class App : Application() {
lockscreenVisibility = Notification.VISIBILITY_SECRET
setShowBadge(false)
},
NotificationChannel(CarSiteController.CHANNEL_ID,
getText(R.string.notification_channel_car_site_controller), NotificationManager.IMPORTANCE_LOW).apply {
lockscreenVisibility = Notification.VISIBILITY_SECRET
setShowBadge(false)
},
NotificationChannel(LocationSetter.CHANNEL_ID, getText(R.string.notification_channel_webhook_updating),
NotificationManager.IMPORTANCE_LOW).apply {
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
Expand Down
51 changes: 0 additions & 51 deletions app/src/main/java/be/mygod/reactmap/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,10 @@ package be.mygod.reactmap
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants
import android.view.WindowManager
import android.webkit.WebView
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.core.content.edit
import androidx.core.content.res.use
Expand All @@ -23,7 +18,6 @@ import androidx.lifecycle.repeatOnLifecycle
import be.mygod.reactmap.App.Companion.app
import be.mygod.reactmap.util.AlertDialogFragment
import be.mygod.reactmap.util.Empty
import be.mygod.reactmap.util.UnblockCentral
import be.mygod.reactmap.util.UpdateChecker
import be.mygod.reactmap.util.readableMessage
import be.mygod.reactmap.webkit.ReactMapFragment
Expand All @@ -34,21 +28,13 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.io.FileDescriptor
import java.io.IOException
import java.net.InetSocketAddress

class MainActivity : FragmentActivity() {
companion object {
const val ACTION_CONFIGURE = "be.mygod.reactmap.action.CONFIGURE"
const val ACTION_RESTART_GAME = "be.mygod.reactmap.action.RESTART_GAME"
private const val KEY_WELCOME = "welcome"

private val setInt by lazy { FileDescriptor::class.java.getDeclaredMethod("setInt$", Int::class.java) }
@get:RequiresApi(29)
private val os by lazy { Class.forName("libcore.io.Libcore").getDeclaredField("os").get(null) }
private val nullFd by lazy { Os.open("/dev/null", OsConstants.O_RDONLY, 0) }
}

override fun onCreate(savedInstanceState: Bundle?) {
Expand All @@ -68,43 +54,6 @@ class MainActivity : FragmentActivity() {
AlertDialogFragment.setResultListener<ConfigDialogFragment, Empty>(this) { which, _ ->
if (which != DialogInterface.BUTTON_POSITIVE) return@setResultListener
currentFragment?.terminate()
try {
for (file in File("/proc/self/fd").listFiles() ?: emptyArray()) try {
val fdInt = file.name.toInt()
val fd = FileDescriptor().apply { setInt(this, fdInt) }
val endpoint = try {
Os.getsockname(fd)
} catch (e: ErrnoException) {
if (e.errno == OsConstants.EBADF || e.errno == OsConstants.ENOTSOCK) continue else throw e
}
if (endpoint !is InetSocketAddress) continue
val isTcp = when (val type = UnblockCentral.getsockoptInt(null, fd, OsConstants.SOL_SOCKET,
OsConstants.SO_TYPE)) {
OsConstants.SOCK_STREAM -> true
OsConstants.SOCK_DGRAM -> false
else -> {
Timber.w(Exception("Unknown $type to $endpoint"))
continue
}
}
val ownerTag = if (Build.VERSION.SDK_INT >= 29) try {
UnblockCentral.fdsanGetOwnerTag(os, fd) as Long
} catch (e: Exception) {
Timber.w(e)
0
} else 0
Timber.d("Resetting $fdInt owned by $ownerTag if is 0 -> $endpoint $isTcp")
if (ownerTag != 0L) continue
if (isTcp) {
UnblockCentral.setsockoptLinger(null, fd, OsConstants.SOL_SOCKET,
OsConstants.SO_LINGER, UnblockCentral.lingerReset)
} else Os.dup2(nullFd, fdInt)
} catch (e: Exception) {
Timber.w(e)
}
} catch (e: IOException) {
Timber.d(e)
}
reactMapFragment()
}
supportFragmentManager.setFragmentResultListener("ReactMapFragment", this) { _, _ ->
Expand Down
62 changes: 62 additions & 0 deletions app/src/main/java/be/mygod/reactmap/auto/CarKeyboard.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package be.mygod.reactmap.auto

import android.webkit.JavascriptInterface
import android.webkit.WebView
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import be.mygod.reactmap.R
import com.google.android.apps.auto.sdk.CarActivity
import com.google.android.apps.auto.sdk.SearchCallback
import com.google.android.apps.auto.sdk.SearchItem
import kotlinx.coroutines.launch
import org.json.JSONStringer

class CarKeyboard(private val web: WebView, private val lifecycleOwner: LifecycleOwner) : SearchCallback() {
companion object {
private val string by lazy {
JSONStringer::class.java.getDeclaredMethod("string", String::class.java).apply { isAccessible = true }
}
}

private val activity = (web.context as CarActivity).also {
it.carUiController.searchController.setSearchCallback(this)
web.addJavascriptInterface(this, "_autoKeyboard")
}
private val jsSetup = activity.resources.openRawResource(R.raw.setup_keyboard).bufferedReader().readText()
fun setup() {
isActive = false
web.evaluateJavascript(jsSetup, null)
}

private var isActive = false

@JavascriptInterface
fun request(text: String?, hint: CharSequence?): Boolean {
if (isActive) return false
isActive = true
lifecycleOwner.lifecycleScope.launch {
activity.carUiController.searchController.setSearchHint(hint)
// searchController.setSearchItems()
activity.carUiController.searchController.startSearch(text)
}
return true
}

override fun onSearchTextChanged(searchTerm: String?) { }

override fun onSearchSubmitted(searchTerm: String?) = true.also {
isActive = false
val value = searchTerm?.let {
JSONStringer().apply { string(this, it) }.toString()
}.toString()
web.evaluateJavascript("window._autoKeyboardCallback.valueReady($value)", null)
}

override fun onSearchItemSelected(searchItem: SearchItem?) { }

override fun onSearchStop() {
if (!isActive) return
isActive = false
web.evaluateJavascript("window._autoKeyboardCallback.dismiss()", null)
}
}
75 changes: 75 additions & 0 deletions app/src/main/java/be/mygod/reactmap/auto/CarReactMapFragment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package be.mygod.reactmap.auto

import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.webkit.JsResult
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
import be.mygod.reactmap.App.Companion.app
import be.mygod.reactmap.R
import be.mygod.reactmap.webkit.BaseReactMapFragment
import com.google.android.material.snackbar.Snackbar
import timber.log.Timber

class CarReactMapFragment : BaseReactMapFragment() {
private val mainActivity by lazy { context as MainCarActivity }
private lateinit var carKeyboard: CarKeyboard
private lateinit var siteController: CarSiteController

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
carKeyboard = CarKeyboard(web, this)
siteController = CarSiteController(this)
}

override fun onPageStarted() = carKeyboard.setup()

override fun onShowFileChooser(filePathCallback: ValueCallback<Array<Uri>>?,
fileChooserParams: WebChromeClient.FileChooserParams) {
Snackbar.make(web, R.string.car_toast_complicated_action, Snackbar.LENGTH_SHORT).show()
mainActivity.killMap()
}
override fun onDownloadStart(url: String?, userAgent: String?, contentDisposition: String?, mimetype: String?,
contentLength: Long) =
Snackbar.make(web, R.string.car_toast_complicated_action, Snackbar.LENGTH_SHORT).show()

override fun onReceiveTitle(title: String?) {
siteController.title = title
}

override fun onRenderProcessGone() = mainActivity.killMap()

override fun requestLocationPermissions() = requireContext().checkSelfPermission(
Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED

override fun onUnsupportedUri(uri: Uri) {
val s = uri.toString()
if (s.startsWith("https://maps.google.com/maps/place/")) {
return mainActivity.startCarActivity(Intent(Intent.ACTION_VIEW,
Uri.parse("google.navigation:q=${s.substring(35)}")))
}
// support ACTION_DIAL tel:URL or starting other car apps?
try {
app.startActivity(Intent(app.customTabsIntent.intent).apply {
data = uri
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}, app.customTabsIntent.startAnimationBundle)
} catch (e: RuntimeException) {
Timber.d(e)
return Snackbar.make(web, s, Snackbar.LENGTH_SHORT).show()
}
Snackbar.make(web, R.string.car_toast_unsupported_url, Snackbar.LENGTH_SHORT).show()
}

override fun onJsAlert(message: String?, result: JsResult) = true.also {
Snackbar.make(web, message.toString(), Snackbar.LENGTH_INDEFINITE).apply {
addCallback(object : Snackbar.Callback() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) = result.cancel()
})
}.show()
}
}
47 changes: 47 additions & 0 deletions app/src/main/java/be/mygod/reactmap/auto/CarSiteController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package be.mygod.reactmap.auto

import android.app.Notification
import android.app.PendingIntent
import android.content.Intent
import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import be.mygod.reactmap.App.Companion.app
import be.mygod.reactmap.R

class CarSiteController(private val fragment: Fragment) : DefaultLifecycleObserver {
companion object {
const val CHANNEL_ID = "carcontrol"
private const val NOTIFICATION_ID = 2
}
init {
fragment.lifecycle.addObserver(this)
}

private fun postNotification() = fragment.requireContext().let { context ->
app.nm.notify(NOTIFICATION_ID, Notification.Builder(context, CHANNEL_ID).apply {
setWhen(0)
setCategory(Notification.CATEGORY_SERVICE)
setContentTitle(title ?: context.getText(R.string.title_loading))
setContentText(context.getText(R.string.notification_car_site_controller_message))
setColor(context.getColor(R.color.main_blue))
setGroup(CHANNEL_ID)
setSmallIcon(R.drawable.ic_maps_directions_car)
setOngoing(true)
setVisibility(Notification.VISIBILITY_SECRET)
setContentIntent(PendingIntent.getBroadcast(context, 0,
Intent(MainCarActivity.ACTION_CLOSE).setPackage(context.packageName),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
}.build())
}

var title: CharSequence? = null
set(value) {
field = value
if (fragment.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) postNotification()
}

override fun onStart(owner: LifecycleOwner) = postNotification()
override fun onStop(owner: LifecycleOwner) = app.nm.cancel(NOTIFICATION_ID)
}
Loading