Skip to content

Commit 8e3cd5e

Browse files
committed
Initial commit
0 parents  commit 8e3cd5e

23 files changed

+925
-0
lines changed

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
*.iml
2+
.gradle
3+
/local.properties
4+
/.idea
5+
.DS_Store
6+
/build
7+
/captures
8+
.externalNativeBuild
9+
.cxx

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# ReactMap for Android
2+
3+
Use ReactMap as an Android app with battery improvements, including:
4+
5+
* Use Google location services to follow location.
6+
* Reduce animations. (coming soon)

app/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

app/build.gradle.kts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
2+
plugins {
3+
alias(libs.plugins.androidApplication)
4+
alias(libs.plugins.kotlinAndroid)
5+
}
6+
7+
android {
8+
namespace = "be.mygod.reactmap"
9+
compileSdk = 34
10+
11+
defaultConfig {
12+
applicationId = "be.mygod.reactmap"
13+
minSdk = 24
14+
targetSdk = 34
15+
versionCode = 10
16+
versionName = "0.3.0"
17+
18+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
19+
}
20+
21+
buildTypes {
22+
debug {
23+
isPseudoLocalesEnabled = true
24+
}
25+
release {
26+
isMinifyEnabled = true
27+
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
28+
}
29+
}
30+
buildFeatures.buildConfig = true
31+
val javaVersion = JavaVersion.VERSION_11
32+
compileOptions {
33+
sourceCompatibility = javaVersion
34+
targetCompatibility = javaVersion
35+
}
36+
kotlinOptions.jvmTarget = javaVersion.toString()
37+
packaging.resources.excludes += "/META-INF/{AL2.0,LGPL2.1}"
38+
}
39+
40+
dependencies {
41+
implementation(libs.play.services.location)
42+
implementation(libs.lifecycle.common)
43+
testImplementation(libs.junit)
44+
androidTestImplementation(libs.androidx.test.ext.junit)
45+
androidTestImplementation(libs.espresso.core)
46+
}

app/proguard-rules.pro

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Add project specific ProGuard rules here.
2+
# You can control the set of applied configuration files using the
3+
# proguardFiles setting in build.gradle.
4+
#
5+
# For more details, see
6+
# http://developer.android.com/guide/developing/tools/proguard.html
7+
8+
# If your project uses WebView with JS, uncomment the following
9+
# and specify the fully qualified class name to the JavaScript interface
10+
# class:
11+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12+
# public *;
13+
#}
14+
15+
# Uncomment this to preserve the line number information for
16+
# debugging stack traces.
17+
#-keepattributes SourceFile,LineNumberTable
18+
19+
# If you keep the line number information, uncomment this to
20+
# hide the original source file name.
21+
#-renamesourcefileattribute SourceFile
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package be.mygod.reactmap
2+
3+
import androidx.test.platform.app.InstrumentationRegistry
4+
import androidx.test.ext.junit.runners.AndroidJUnit4
5+
6+
import org.junit.Test
7+
import org.junit.runner.RunWith
8+
9+
import org.junit.Assert.*
10+
11+
/**
12+
* Instrumented test, which will execute on an Android device.
13+
*
14+
* See [testing documentation](http://d.android.com/tools/testing).
15+
*/
16+
@RunWith(AndroidJUnit4::class)
17+
class ExampleInstrumentedTest {
18+
@Test
19+
fun useAppContext() {
20+
// Context of the app under test.
21+
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22+
assertEquals("be.mygod.reactmap", appContext.packageName)
23+
}
24+
}

app/src/main/AndroidManifest.xml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:tools="http://schemas.android.com/tools">
4+
5+
<application
6+
android:allowBackup="true"
7+
android:dataExtractionRules="@xml/data_extraction_rules"
8+
android:fullBackupContent="@xml/backup_rules"
9+
android:label="@string/app_name"
10+
android:supportsRtl="true"
11+
android:theme="@style/Theme.ReactMap"
12+
tools:targetApi="31">
13+
<activity
14+
android:name=".MainActivity"
15+
android:exported="true"
16+
android:label="@string/app_name"
17+
android:theme="@style/Theme.ReactMap">
18+
<intent-filter>
19+
<action android:name="android.intent.action.MAIN" />
20+
21+
<category android:name="android.intent.category.LAUNCHER" />
22+
</intent-filter>
23+
</activity>
24+
</application>
25+
26+
</manifest>
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package be.mygod.reactmap
2+
3+
import android.Manifest
4+
import android.annotation.SuppressLint
5+
import android.content.pm.PackageManager
6+
import android.location.Location
7+
import android.os.Build
8+
import android.os.Looper
9+
import android.util.Log
10+
import android.webkit.JavascriptInterface
11+
import android.webkit.WebView
12+
import androidx.annotation.RequiresPermission
13+
import androidx.core.app.ComponentActivity
14+
import androidx.lifecycle.DefaultLifecycleObserver
15+
import androidx.lifecycle.Lifecycle
16+
import androidx.lifecycle.LifecycleOwner
17+
import com.google.android.gms.location.FusedLocationProviderClient
18+
import com.google.android.gms.location.LocationCallback
19+
import com.google.android.gms.location.LocationRequest
20+
import com.google.android.gms.location.LocationResult
21+
22+
class Glocation(private val web: WebView, private val permissionRequestCode: Int) : DefaultLifecycleObserver {
23+
companion object {
24+
private const val TAG = "Glocation"
25+
const val PERMISSION_DENIED = 1
26+
const val POSITION_UNAVAILABLE = 2
27+
const val TIMEOUT = 3
28+
29+
fun Location.toGeolocationPosition() = """{
30+
coords: {
31+
latitude: $latitude,
32+
longitude: $longitude,
33+
altitude: ${if (hasAltitude()) altitude else null},
34+
accuracy: $accuracy,
35+
altitudeAccuracy: ${
36+
if (Build.VERSION.SDK_INT >= 26 && hasVerticalAccuracy()) verticalAccuracyMeters else null
37+
},
38+
heading: ${if (hasBearing()) bearing else null},
39+
speed: ${if (hasSpeed()) speed else null},
40+
},
41+
timestamp: ${time.toULong()},
42+
}"""
43+
44+
fun Exception?.toGeolocationPositionError() = """{
45+
code: ${if (this is SecurityException) PERMISSION_DENIED else POSITION_UNAVAILABLE},
46+
message: ${this?.message?.let { "'$it'" }},
47+
}"""
48+
}
49+
50+
private val activity = web.let {
51+
it.addJavascriptInterface(this, "_glocation")
52+
it.context as ComponentActivity
53+
}.also { it.lifecycle.addObserver(this) }
54+
private val jsSetup = activity.resources.openRawResource(R.raw.setup).bufferedReader().readText()
55+
private val client = FusedLocationProviderClient(activity)
56+
private val pendingRequests = mutableSetOf<Long>()
57+
private var pendingWatch = false
58+
private val activeListeners = mutableSetOf<Long>()
59+
private val callback = object : LocationCallback() {
60+
override fun onLocationResult(result: LocationResult) {
61+
val location = result.lastLocation ?: return
62+
val ids = activeListeners.joinToString()
63+
Log.d(TAG, "onLocationResult ${location.time}")
64+
web.evaluateJavascript(
65+
"navigator.geolocation._watchPositionSuccess([$ids], ${location.toGeolocationPosition()})", null)
66+
}
67+
}
68+
private var requestingLocationUpdates = false
69+
70+
fun clear() {
71+
pendingRequests.clear()
72+
activeListeners.clear()
73+
requestingLocationUpdates = false
74+
removeLocationUpdates()
75+
}
76+
77+
fun setupGeolocation() = web.evaluateJavascript(jsSetup, null)
78+
79+
private fun checkPermissions() = when {
80+
activity.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) ==
81+
PackageManager.PERMISSION_GRANTED -> true
82+
else -> {
83+
activity.requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), permissionRequestCode)
84+
null
85+
}
86+
}
87+
88+
fun onRequestPermissionsResult(granted: Boolean) {
89+
if (pendingRequests.isNotEmpty()) {
90+
getCurrentPosition(granted, pendingRequests.joinToString())
91+
pendingRequests.clear()
92+
}
93+
if (pendingWatch) {
94+
watchPosition(granted)
95+
pendingWatch = false
96+
}
97+
}
98+
99+
@JavascriptInterface
100+
fun getCurrentPosition(i: Long) {
101+
Log.d(TAG, "getCurrentPosition($i)")
102+
when (val granted = checkPermissions()) {
103+
null -> pendingRequests.add(i)
104+
else -> getCurrentPosition(granted, i.toString())
105+
}
106+
}
107+
108+
private fun getCurrentPosition(granted: Boolean, ids: String) {
109+
@SuppressLint("MissingPermission")
110+
if (granted) client.lastLocation.addOnCompleteListener { task ->
111+
val location = task.result
112+
web.evaluateJavascript(if (location == null) {
113+
"navigator.geolocation._getCurrentPositionError([$ids], ${task.exception.toGeolocationPositionError()})"
114+
} else "navigator.geolocation._getCurrentPositionSuccess([$ids], ${location.toGeolocationPosition()})",
115+
null)
116+
} else web.evaluateJavascript(
117+
"navigator.geolocation._getCurrentPositionError([$ids], { code: $PERMISSION_DENIED })", null)
118+
}
119+
120+
@JavascriptInterface
121+
fun watchPosition(i: Long) {
122+
Log.d(TAG, "watchPosition($i)")
123+
if (!activeListeners.add(i) || requestingLocationUpdates || pendingWatch) return
124+
when (val granted = checkPermissions()) {
125+
null -> pendingWatch = true
126+
else -> watchPosition(granted)
127+
}
128+
}
129+
130+
private fun watchPosition(granted: Boolean) {
131+
if (granted) @SuppressLint("MissingPermission") {
132+
requestingLocationUpdates = true
133+
if (activity.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) requestLocationUpdates()
134+
} else web.evaluateJavascript("navigator.geolocation._watchPositionError([${activeListeners.joinToString()}]," +
135+
" { code: $PERMISSION_DENIED })", null)
136+
}
137+
138+
@JavascriptInterface
139+
fun clearWatch(i: Long) {
140+
Log.d(TAG, "clearWatch($i)")
141+
if (!activeListeners.remove(i) || activeListeners.isNotEmpty() || !requestingLocationUpdates) return
142+
requestingLocationUpdates = false
143+
removeLocationUpdates()
144+
}
145+
146+
override fun onStart(owner: LifecycleOwner) {
147+
@SuppressLint("MissingPermission")
148+
if (requestingLocationUpdates) requestLocationUpdates()
149+
}
150+
override fun onStop(owner: LifecycleOwner) {
151+
if (requestingLocationUpdates) removeLocationUpdates()
152+
}
153+
154+
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
155+
private fun requestLocationUpdates() = client.requestLocationUpdates(LocationRequest.create().apply {
156+
priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY // enableHighAccuracy - PRIORITY_HIGH_ACCURACY
157+
// expirationTime = timeout
158+
// maxWaitTime = maximumAge
159+
fastestInterval = 1000
160+
interval = 4000
161+
smallestDisplacement = 5f
162+
}, callback, Looper.getMainLooper()).addOnCompleteListener { task ->
163+
if (task.isSuccessful) {
164+
Log.d(TAG, "Start watching location")
165+
} else web.evaluateJavascript("navigator.geolocation._watchPositionError([${activeListeners.joinToString()}]," +
166+
" ${task.exception.toGeolocationPositionError()})", null)
167+
}
168+
private fun removeLocationUpdates() = client.removeLocationUpdates(callback).addOnCompleteListener { task ->
169+
if (task.isSuccessful) Log.d(TAG, "Stop watching location")
170+
else Log.w(TAG, "Stop watch failed: ${task.exception}")
171+
}
172+
}

0 commit comments

Comments
 (0)