Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ Other features:

* See map even behind your status bar and navigation bar!
* More conveniently refresh the webpage when fullscreen. (During network congestion, Chrome HTTP/3 scheduler retries requests at its own pace. Doing a manual refresh overrides this.)
* Login button is clicked automatically.
* Login button is clicked automatically (Discord login only).
* You could make alerts follow location in background.
8 changes: 5 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ plugins {
alias(libs.plugins.firebaseCrashlytics)
alias(libs.plugins.googleServices)
alias(libs.plugins.kotlinAndroid)
id("kotlin-parcelize")
}

android {
Expand All @@ -13,8 +14,8 @@ android {
applicationId = "be.mygod.reactmap"
minSdk = 24
targetSdk = 34
versionCode = 16
versionName = "0.4.5"
versionCode = 52
versionName = "0.5.1"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down Expand Up @@ -46,10 +47,11 @@ dependencies {
implementation(libs.core.ktx)
implementation(libs.firebase.analytics)
implementation(libs.firebase.crashlytics)
implementation(libs.fragment) // update to 1.3+ to suppress lint
implementation(libs.fragment.ktx)
implementation(libs.play.services.location)
implementation(libs.lifecycle.common)
implementation(libs.timber)
implementation(libs.work.ktx)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.espresso.core)
Expand Down
66 changes: 66 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

<application
android:name=".App"
Expand All @@ -30,6 +32,70 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver
android:name=".follower.BackgroundLocationReceiver"
android:directBootAware="true"
android:enabled="false"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>

<service
android:name="androidx.work.impl.background.systemalarm.SystemAlarmService"
android:directBootAware="true"
tools:replace="android:directBootAware"/>
<service
android:name="androidx.work.impl.background.systemjob.SystemJobService"
android:directBootAware="true"
tools:replace="android:directBootAware"/>
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:directBootAware="true"
android:foregroundServiceType="dataSync"
tools:replace="android:directBootAware"
tools:node="merge"/>
<receiver
android:name="androidx.work.impl.utils.ForceStopRunnable$BroadcastReceiver"
android:directBootAware="true"
tools:replace="android:directBootAware"/>
<receiver
android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$BatteryChargingProxy"
android:directBootAware="true"
tools:replace="android:directBootAware"/>
<receiver
android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$BatteryNotLowProxy"
android:directBootAware="true"
tools:replace="android:directBootAware"/>
<receiver
android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$StorageNotLowProxy"
android:directBootAware="true"
tools:replace="android:directBootAware"/>
<receiver
android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$NetworkStateProxy"
android:directBootAware="true"
tools:replace="android:directBootAware"/>
<receiver
android:name="androidx.work.impl.background.systemalarm.RescheduleReceiver"
android:directBootAware="true"
tools:replace="android:directBootAware"/>
<receiver
android:name="androidx.work.impl.background.systemalarm.ConstraintProxyUpdateReceiver"
android:directBootAware="true"
tools:replace="android:directBootAware"/>
<receiver
android:name="androidx.work.impl.diagnostics.DiagnosticsReceiver"
android:directBootAware="true"
tools:replace="android:directBootAware"/>
<provider
android:name="androidx.startup.InitializationProvider"
tools:node="remove"/>
<provider
android:name="com.google.firebase.provider.FirebaseInitProvider"
tools:node="remove"/>
</application>

</manifest>
53 changes: 51 additions & 2 deletions app/src/main/java/be/mygod/reactmap/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,57 @@ package be.mygod.reactmap

import android.annotation.SuppressLint
import android.app.Application
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.UserManager
import android.os.ext.SdkExtensions
import android.util.Log
import android.webkit.WebView
import android.widget.Toast
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.getSystemService
import androidx.work.WorkManager
import be.mygod.reactmap.follower.BackgroundLocationReceiver
import be.mygod.reactmap.follower.LocationSetter
import be.mygod.reactmap.util.DeviceStorageApp
import com.google.android.gms.location.LocationServices
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.ktx.Firebase
import com.google.firebase.ktx.initialize
import kotlinx.coroutines.DEBUG_PROPERTY_NAME
import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON
import timber.log.Timber

class App : Application() {
companion object {
private const val PREF_NAME = "reactmap"
const val KEY_ACTIVE_URL = "url.active"
const val URL_DEFAULT = "https://www.reactmap.dev"

lateinit var app: App
}

lateinit var deviceStorage: Application
lateinit var work: WorkManager
val pref by lazy { deviceStorage.getSharedPreferences(PREF_NAME, MODE_PRIVATE) }
val fusedLocation by lazy { LocationServices.getFusedLocationProviderClient(deviceStorage) }
val nm by lazy { getSystemService<NotificationManager>()!! }
val userManager by lazy { getSystemService<UserManager>()!! }

val activeUrl get() = pref.getString(KEY_ACTIVE_URL, URL_DEFAULT) ?: URL_DEFAULT

override fun onCreate() {
super.onCreate()
app = this
deviceStorage = DeviceStorageApp(this)
deviceStorage.moveSharedPreferencesFrom(this, PREF_NAME)
// overhead of debug mode is minimal: https://github.com/Kotlin/kotlinx.coroutines/blob/f528898/docs/debugging.md#debug-mode
System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)
Firebase.initialize(deviceStorage)
FirebaseCrashlytics.getInstance().apply {
setCustomKey("build", Build.DISPLAY)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) setCustomKey("extension_s",
Expand All @@ -42,7 +73,25 @@ class App : Application() {
}
}
})
if (BuildConfig.DEBUG) WebView.setWebContentsDebuggingEnabled(true)

if (Build.VERSION.SDK_INT >= 26) nm.createNotificationChannels(listOf(
NotificationChannel(SiteController.CHANNEL_ID, "Full screen site controls",
NotificationManager.IMPORTANCE_LOW).apply {
lockscreenVisibility = Notification.VISIBILITY_SECRET
},
NotificationChannel(LocationSetter.CHANNEL_ID, "Background location updating",
NotificationManager.IMPORTANCE_LOW).apply {
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
},
NotificationChannel(LocationSetter.CHANNEL_ID_SUCCESS, "Background location updated",
NotificationManager.IMPORTANCE_MIN),
NotificationChannel(LocationSetter.CHANNEL_ID_ERROR, "Background location update failed",
NotificationManager.IMPORTANCE_HIGH).apply {
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
},
))
work = WorkManager.getInstance(deviceStorage)
BackgroundLocationReceiver.setup()
}

private val customTabsIntent by lazy {
Expand Down
93 changes: 93 additions & 0 deletions app/src/main/java/be/mygod/reactmap/ConfigDialogFragment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package be.mygod.reactmap

import android.Manifest
import android.app.AlertDialog
import android.content.DialogInterface
import android.content.pm.PackageManager
import android.os.Parcelable
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.LinearLayout
import android.widget.Switch
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.core.view.isGone
import be.mygod.reactmap.App.Companion.app
import be.mygod.reactmap.follower.BackgroundLocationReceiver
import be.mygod.reactmap.util.AlertDialogFragment
import be.mygod.reactmap.util.Empty
import be.mygod.reactmap.util.readableMessage
import kotlinx.parcelize.Parcelize

class ConfigDialogFragment : AlertDialogFragment<Empty, ConfigDialogFragment.Ret>() {
companion object {
private const val KEY_HISTORY_URL = "url.history"
}

@Parcelize
data class Ret(val hostname: String?) : Parcelable

private lateinit var historyUrl: Set<String?>
private lateinit var urlEdit: AutoCompleteTextView
private lateinit var followerSwitch: Switch

private val requestPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (!it) followerSwitch.isChecked = false
}

override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
historyUrl = app.pref.getStringSet(KEY_HISTORY_URL, null) ?: setOf(App.URL_DEFAULT)
val context = requireContext()
urlEdit = AutoCompleteTextView(context).apply {
setAdapter(ArrayAdapter(context, android.R.layout.select_dialog_item, historyUrl.toTypedArray()))
setText(app.activeUrl)
}
followerSwitch = Switch(context).apply {
text = "Make alerts follow location in background\n(beware that you would be sharing your location with the map)"
isChecked = BackgroundLocationReceiver.enabled
isGone = true
setOnCheckedChangeListener { _, isChecked ->
if (isChecked) requestPermission.launch(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
}
}
setView(LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
addView(urlEdit, LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT))
addView(followerSwitch, LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT))
})
setTitle("ReactMap URL:")
setMessage("You can return to this dialog later by clicking on the notification.")
setPositiveButton(android.R.string.ok, listener)
setNegativeButton(android.R.string.cancel, null)
}

override fun onResume() {
super.onResume()
val context = requireContext()
followerSwitch.isGone = context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) !=
PackageManager.PERMISSION_GRANTED ||
context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) !=
PackageManager.PERMISSION_GRANTED
}

override val ret get() = Ret(try {
val (uri, host) = urlEdit.text!!.toString().toUri().let {
require("https".equals(it.scheme, true)) { "Only HTTPS is allowed" }
it.toString() to it.host!!
}
val oldApiUrl = ReactMapHttpEngine.apiUrl
app.pref.edit {
putString(App.KEY_ACTIVE_URL, uri)
putStringSet(KEY_HISTORY_URL, historyUrl + uri)
}
if (oldApiUrl != ReactMapHttpEngine.apiUrl) BackgroundLocationReceiver.onApiChanged()
host
} catch (e: Exception) {
Toast.makeText(requireContext(), e.readableMessage, Toast.LENGTH_LONG).show()
null
}).also { BackgroundLocationReceiver.enabled = followerSwitch.isChecked }
}
15 changes: 7 additions & 8 deletions app/src/main/java/be/mygod/reactmap/Glocation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import androidx.annotation.RequiresPermission
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import be.mygod.reactmap.App.Companion.app
import com.google.android.gms.location.LocationAvailability
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
import timber.log.Timber

Expand Down Expand Up @@ -54,7 +54,6 @@ class Glocation(private val web: WebView) : DefaultLifecycleObserver {
it.context as ComponentActivity
}.also { it.lifecycle.addObserver(this) }
private val jsSetup = activity.resources.openRawResource(R.raw.setup).bufferedReader().readText()
private val client = LocationServices.getFusedLocationProviderClient(activity)
private val pendingRequests = mutableSetOf<Long>()
private var pendingWatch = false
private val activeListeners = mutableSetOf<Long>()
Expand Down Expand Up @@ -119,7 +118,7 @@ class Glocation(private val web: WebView) : DefaultLifecycleObserver {

private fun getCurrentPosition(granted: Boolean, ids: String) {
@SuppressLint("MissingPermission")
if (granted) client.lastLocation.addOnCompleteListener { task ->
if (granted) app.fusedLocation.lastLocation.addOnCompleteListener { task ->
val location = task.result
web.evaluateJavascript(if (location == null) {
"navigator.geolocation._getCurrentPositionError([$ids], ${task.exception.toGeolocationPositionError()})"
Expand Down Expand Up @@ -165,12 +164,12 @@ class Glocation(private val web: WebView) : DefaultLifecycleObserver {

@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
private fun requestLocationUpdates() {
client.requestLocationUpdates(LocationRequest.Builder(Priority.PRIORITY_LOW_POWER, 4000).apply {
app.fusedLocation.requestLocationUpdates(LocationRequest.Builder(Priority.PRIORITY_LOW_POWER, 4000).apply {
// enableHighAccuracy - PRIORITY_HIGH_ACCURACY
// expirationTime = timeout
// maxWaitTime = maximumAge
setMinUpdateIntervalMillis(1000)
setMinUpdateDistanceMeters(5f)
setMinUpdateIntervalMillis(1000)
}.build(), callback, Looper.getMainLooper()).addOnCompleteListener { task ->
if (task.isSuccessful) {
Timber.d("Start watching location")
Expand All @@ -180,8 +179,8 @@ class Glocation(private val web: WebView) : DefaultLifecycleObserver {
)
}
}
private fun removeLocationUpdates() = client.removeLocationUpdates(callback).addOnCompleteListener { task ->
if (task.isSuccessful) Timber.d("Stop watching location")
else Timber.w("Stop watch failed: ${task.exception}")
private fun removeLocationUpdates() = app.fusedLocation.removeLocationUpdates(callback).addOnCompleteListener {
if (it.isSuccessful) Timber.d("Stop watching location")
else Timber.w("Stop watch failed: ${it.exception}")
}
}
Loading