diff --git a/android/tinySSB/app/build.gradle b/android/tinySSB/app/build.gradle
index 021680d..9b50b9a 100644
--- a/android/tinySSB/app/build.gradle
+++ b/android/tinySSB/app/build.gradle
@@ -86,6 +86,7 @@ dependencies {
// database
implementation "androidx.room:room-runtime:2.3.0"
+ implementation 'com.google.android.gms:play-services-location:18.0.0' // 21.3.0'
kapt "androidx.room:room-compiler:2.3.0"
// implementation "androidx.room:room-rxjava2:2.3.0"
// implementation "androidx.room:room-guava:2.3.0"
diff --git a/android/tinySSB/app/src/main/assets/web/prod/chat.js b/android/tinySSB/app/src/main/assets/web/prod/chat.js
index 58f0430..1c33053 100644
--- a/android/tinySSB/app/src/main/assets/web/prod/chat.js
+++ b/android/tinySSB/app/src/main/assets/web/prod/chat.js
@@ -67,7 +67,13 @@ function new_text_post(s) {
if (s.length == 0) {
return;
}
- var draft = unicodeStringToTypedArray(document.getElementById('draft').value); // escapeHTML(
+ var draft = ''
+ if (Android.isGeoLocationEnabled() == "true") {
+ var plusCode = Android.getCurrentLocationAsPlusCode();
+ if (plusCode != null && plusCode.length > 0) //check if we actually received a location
+ draft += "pfx:loc/plus," + plusCode + "|";
+ }
+ draft += unicodeStringToTypedArray(document.getElementById('draft').value); // escapeHTML(
var ch = tremola.chats[curr_chat]
if (!(ch.timeline instanceof Timeline)) {
ch.timeline = Timeline.fromJSON(ch.timeline)
@@ -92,7 +98,13 @@ function new_text_post(s) {
}
function new_voice_post(voice_b64) {
- var draft = unicodeStringToTypedArray(document.getElementById('draft').value); // escapeHTML(
+ var draft = ''
+ if (Android.isGeoLocationEnabled() == "true") {
+ var plusCode = Android.getCurrentLocationAsPlusCode();
+ if (plusCode != null && plusCode.length > 0) //check if we actually received a location
+ draft += "pfx:loc/plus," + plusCode + "|";
+ }
+ draft += unicodeStringToTypedArray(document.getElementById('draft').value); // escapeHTML(
if (draft.length == 0)
draft = "null"
else
@@ -144,16 +156,48 @@ function new_image_post() {
function load_post_item(p) { // { 'key', 'from', 'when', 'body', 'to' (if group or public)>
var pl = document.getElementById('lst:posts');
var is_other = p["from"] != myId;
+ /*
var box = "
\n");
+ // split each message, so that we may access the fields of the message
+ var fieldsOfBody = textOfBody.split(/(\|)/g);
+ var geoLocPlusCode = null;
+ // here we check all prefixes that might be in a message, it is currently not possible to send a sketch with any prefixes since this completely breaks the sketch because uses the character '|'
+ var i = 0;
+ var otherText = "";
+ while (i < fieldsOfBody.length){
+ var field = fieldsOfBody[i];
+ if (field.startsWith("pfx:loc/plus")){
+ var partsOfGeoLoc = field.split(',');
+ geoLocPlusCode = partsOfGeoLoc[1];
+ i++;
+ } else {
+ otherText += field;
+ }
+ i++;
+ }
+ if ((p.voice != null) && geoLocPlusCode == null)
+ box += " onclick='play_voice(\"" + curr_chat + "\", \"" + p.key + "\");'";
+ if ((geoLocPlusCode != null) && (p.voice == null))
+ box += " onclick='showGeoMenu(\"" + geoLocPlusCode + "\");'";
+ if ((geoLocPlusCode != null) && (p.voice != null))
+ box += " onclick='showGeoVoiceMenu(\"" + geoLocPlusCode + "\",\"" + curr_chat + "\", \"" + p.key + "\");'";
+
box += ">"
+
// console.log("box=", box);
if (is_other)
box += "" + fid2display(p["from"]) + " ";
+ // if (geoLocPlusCode != null)
+ // box += "" + "this message contains geolocation" + " ";
var txt = ""
if (p["body"] != null) {
- txt = escapeHTML(p["body"]).replace(/\n/g, " \n");
+ // txt = escapeHTML(p["body"]).replace(/\n/g, " \n");
+ txt = otherText;
// Sketch app
if (txt.startsWith("data:image/png;base64")) { // check if the string is a data url
let compressedBase64 = txt.split(',')[1];
@@ -231,6 +275,8 @@ function load_post_item(p) { // { 'key', 'from', 'when', 'body', 'to' (if group
box += txt
var d = new Date(p["when"]);
d = d.toDateString() + ' ' + d.toTimeString().substring(0, 5);
+ if (geoLocPlusCode != null)
+ d = '📌 ' + d
box += "
";
box += d + "
";
var row;
diff --git a/android/tinySSB/app/src/main/assets/web/prod/settings.js b/android/tinySSB/app/src/main/assets/web/prod/settings.js
index ace2332..be28354 100644
--- a/android/tinySSB/app/src/main/assets/web/prod/settings.js
+++ b/android/tinySSB/app/src/main/assets/web/prod/settings.js
@@ -14,14 +14,17 @@ const BrowserOnlySettings = {
'hide_forgotten_kanbans': true,
'udp_multicast_enabled': true,
'ble_enabled': true,
- 'websocket_url': "ws://meet.dmi.unibas.ch:8989"
+ 'websocket_url': "ws://meet.dmi.unibas.ch:8989",
+ 'geo_location': true
}
// button/toggle handler for boolean settings; settingID is determined by the id of the html object that emitted the event (e.id)
function toggle_changed(e) {
- // console.log("toggle ", e.id);
+ console.log("toggle:", e.id);
tremola.settings[e.id] = e.checked;
- backend("settings:set " + e.id + " " + e.checked)
+ var cmd = "settings:set " + e.id + " " + e.checked
+ backend(cmd)
+ console.log(`sent "${cmd}" to backend`)
persist()
applySetting(e.id, e.checked);
}
diff --git a/android/tinySSB/app/src/main/assets/web/prod/sketch.js b/android/tinySSB/app/src/main/assets/web/prod/sketch.js
index 488aef0..12e68fa 100644
--- a/android/tinySSB/app/src/main/assets/web/prod/sketch.js
+++ b/android/tinySSB/app/src/main/assets/web/prod/sketch.js
@@ -433,7 +433,7 @@ async function sketch_get_current_size() {
// return the current sketch as a base64 string (including the preceding data type descriptor)
async function sketch_getImage() {
- // console.log('getImage', JSON.stringify(sketch.svg));
+ console.log('getImage', JSON.stringify(sketch.svg));
const buf = new ArrayBuffer(bipf_encodingLength(sketch.svg))
const e = bipf_encode(sketch.svg, buf, 0)
// console.log(' bipf:', JSON.stringify(new Uint8Array(buf)))
@@ -474,6 +474,7 @@ async function sketch_getImage() {
for (var i = 0; i < len; i++) {
binary += String.fromCharCode( bytes[ i ] );
}
+
let shortenedDataURL = 'data:image/svg+bipf;base64,' + btoa(binary);
return shortenedDataURL
@@ -481,33 +482,42 @@ async function sketch_getImage() {
//function called by the drawing submit button
async function chat_sendDrawing() {
- let img = await sketch_getImage()
- if (img.length == 0) {
+ var img = await sketch_getImage()
+ if (img.length == 0)
return;
- }
- // send to backend
- var ch = tremola.chats[curr_chat]
- if (!(ch.timeline instanceof Timeline)) {
- ch.timeline = Timeline.fromJSON(ch.timeline)
- }
- let tips = JSON.stringify(ch.timeline.get_tips())
- if (curr_chat == "ALL") {
- var cmd = `publ:post ${tips} ` + btoa(img) + " null"; // + recps
- // console.log(cmd)
- backend(cmd);
- } else {
- var recps = tremola.chats[curr_chat].members.join(' ');
- var cmd = `priv:post ${tips} ` + btoa(img) + " null " + recps;
- backend(cmd);
- }
+ launch_snackbar("sending sketch ...")
+ setTimeout(function () { // delay sending (and getting location beforehand), allows snackbar to show
+
+ //add geolocation to message if enabled.
+ if (Android.isGeoLocationEnabled() == "true"){ //ony add if enabled
+ var plusCode = Android.getCurrentLocationAsPlusCode();
+ if (plusCode != null && plusCode.length > 0) //check if we actually received a location
+ img = "pfx:loc/plus," + plusCode + "|" + img;
+ }
+
+ // send to backend
+ var ch = tremola.chats[curr_chat]
+ if (!(ch.timeline instanceof Timeline))
+ ch.timeline = Timeline.fromJSON(ch.timeline)
+ let tips = JSON.stringify(ch.timeline.get_tips())
+ if (curr_chat == "ALL") {
+ var cmd = `publ:post ${tips} ` + btoa(img) + " null"; // + recps
+ // console.log(cmd)
+ backend(cmd);
+ } else {
+ var recps = tremola.chats[curr_chat].members.join(' ');
+ var cmd = `priv:post ${tips} ` + btoa(img) + " null " + recps;
+ backend(cmd);
+ }
- closeOverlay();
- // setTimeout(function () { // let image rendering (fetching size) take place before we scroll
- let c = document.getElementById('core');
- c.scrollTop = c.scrollHeight;
- // }, 100);
+ closeOverlay();
+ // setTimeout(function () { // let image rendering (fetching size) take place before we scroll
+ let c = document.getElementById('core');
+ c.scrollTop = c.scrollHeight;
+ // }, 100);
- // close sketch
- chat_closeSketch();
+ // close sketch
+ chat_closeSketch();
+ }, 100);
}
diff --git a/android/tinySSB/app/src/main/assets/web/prod/tools.js b/android/tinySSB/app/src/main/assets/web/prod/tools.js
index 8c91602..a381e13 100644
--- a/android/tinySSB/app/src/main/assets/web/prod/tools.js
+++ b/android/tinySSB/app/src/main/assets/web/prod/tools.js
@@ -5,11 +5,11 @@
function load_prod_list() {
document.getElementById("lst:prod").innerHTML = '';
load_prod_item('Kanban', 'img/kanban.svg', 'setScenario("kanban")',
- 'text text text h aghjwd gldfhjs hlgsf hgljksf hgls fdhglf sdhgl hfgskj hls dfhgjl shgjkls hgl sfdhgjk sdfjklg hljs hfgl dfjlsfs');
+ 'Collaboratively visualize your work items and give participants a view of progress and process, from start to finish. Author: Jannick Heisch');
load_prod_item('Event Scheduler (dpi24.14)', 'img/schedule.svg', 'setScenario("scheduling")',
- 'text text text h aghjwd gldfhjs hlgsf hgljksf hgls fdhglf sdhgl hfgskj hls dfhgjl shgjkls hgl sfdhgjk sdfjklg hljs hfgl dfjlsfs');
+ 'Collaboratively find suitable dates by collecting availability of participants. Authors: Sascha Schumacher and Jasra Mohamed Yoosuf');
load_prod_item('Kahoot Quiz (dpi24.15)', 'img/quiz.svg', 'xyz',
- 'text text text h aghjwd gldfhjs hlgsf hgljksf hgls fdhglf sdhgl hfgskj hls dfhgjl shgjkls hgl sfdhgjk sdfjklg hljs hfgl dfjlsfs');
+ 'Create and participate in quizzes - a fun way for users to test their knowledge and learn new information. Authors: Anoozh Akileswaran, Prabavan Balasubramaniam and Jakob Spiess');
load_prod_item('Lokens (coming soon)', 'img/hand_and_coins.svg', 'xyz',
'see Erick Lavoie: "GOC-Ledger: State-based Conflict-Free Replicated Ledger from Grow-Only Counters", https://arxiv.org/abs/2305.16976');
}
@@ -21,7 +21,7 @@ function load_prod_item(title, imageName, cb, descr) {
row = `";
item.innerHTML = row;
document.getElementById('lst:prod').appendChild(item);
}
diff --git a/android/tinySSB/app/src/main/assets/web/tremola.css b/android/tinySSB/app/src/main/assets/web/tremola.css
index 27a6d07..2c9e8c1 100644
--- a/android/tinySSB/app/src/main/assets/web/tremola.css
+++ b/android/tinySSB/app/src/main/assets/web/tremola.css
@@ -95,6 +95,22 @@ textarea {
box-shadow: 0 0 25px rgba(0,0,0,0.9);
}
+.geo-menu-overlay {
+ display: none;
+ position: absolute;
+ left: 50%;
+ top: 20%;
+ transform: translateX(-50%);
+ width: auto;
+ max-width: 90%;
+ min-width: 70%;
+ background: #fff;
+ padding: 0.5em;
+ z-index: 10002; /* high z-index */
+ border-radius: 5px;
+ box-shadow: 0 0 25px rgba(0,0,0,0.9);
+}
+
.qr-overlay {
display: none;
background: #fff;
@@ -516,4 +532,5 @@ input:checked + .slider:before {
filter: invert(62%) sepia(8%) saturate(116%) hue-rotate(145deg) brightness(89%) contrast(89%);
}
+
/* eof */
diff --git a/android/tinySSB/app/src/main/assets/web/tremola.html b/android/tinySSB/app/src/main/assets/web/tremola.html
index 81942b6..7360344 100644
--- a/android/tinySSB/app/src/main/assets/web/tremola.html
+++ b/android/tinySSB/app/src/main/assets/web/tremola.html
@@ -30,11 +30,14 @@
-
-
-
+
+
+
+
+
+
create sketch
@@ -64,7 +67,7 @@
☰
-
+
@@ -489,6 +492,11 @@
Game Invitation declined
Etienne Mettaz
Cedrik Schimschar
Christian Tschudin
+
+ Students of DPI.fs24:
+ Luca Gloor
+ Anna Pietzak
+ Pius Walser
Icons
@@ -566,12 +574,21 @@
Game Invitation declined
-
+
Configurations
+
+
Send current location with messages
+
+
+
+
+
Connect to Peers via BLE
+
URL:
@@ -669,7 +687,7 @@
Game Invitation declined
WIPE EVERYTHING IMMEDIATELY
-
+
diff --git a/android/tinySSB/app/src/main/assets/web/tremola_ui.js b/android/tinySSB/app/src/main/assets/web/tremola_ui.js
index 1a4f0f9..0c4492c 100644
--- a/android/tinySSB/app/src/main/assets/web/tremola_ui.js
+++ b/android/tinySSB/app/src/main/assets/web/tremola_ui.js
@@ -22,11 +22,11 @@ var scenarioDisplay = {
'chats': ['div:qr', 'core', 'lst:chats', 'div:footer', 'plus'],
'contacts': ['div:qr', 'core', 'lst:contacts', 'div:footer', 'plus'],
'posts': ['div:back', 'core', 'div:posts', 'div:textarea'],
- 'games': ['div:qr', 'core', 'lst:games', 'div:footer'],
+ 'games': ['div:back', 'core', 'lst:games', 'div:footer'],
'members': ['div:back', 'core', 'lst:members', 'div:confirm-members'],
- 'productivity': ['div:qr', 'core', 'lst:prod', 'div:footer'],
+ 'productivity': ['div:back', 'core', 'lst:prod', 'div:footer'],
'settings': ['div:back', 'div:settings', 'core'],
- 'kanban': ['div:qr', 'core', 'lst:kanban', 'div:footer', 'plus'], // KANBAN
+ 'kanban': ['div:back', 'core', 'lst:kanban', 'div:footer', 'plus'], // KANBAN
'board': ['div:back', 'core', 'div:board'], // KANBAN
'duels': ['div:back', 'core', 'lst:duels', 'plus'], // BATTLESHIP
'battleships': ['div:back', 'core', 'div:battleships'], // BATTLESHIP
@@ -310,6 +310,7 @@ function menu_settings() {
function closeOverlay() {
document.getElementById('menu').style.display = 'none';
+ document.getElementById('geo-menu').style.display = 'none';
document.getElementById('qr-overlay').style.display = 'none';
document.getElementById('preview-overlay').style.display = 'none';
document.getElementById('image-overlay').style.display = 'none';
@@ -654,4 +655,55 @@ function refresh_connection_progressbar(min_entries, old_min_entries, old_want_e
}
}
+function show_geo_location(locPlus) {
+// var win = window.open("https://maps.app.goo.gl/Z7WWLG8UTAnfyJpb7", '_blank');
+
+ var win = window.open("https://plus.codes/" + locPlus, '_blank');
+ win.focus();
+}
+
+function copyToClipboard(text) {
+ navigator.clipboard.writeText(text).then(() => {
+ console.log('Text copied to clipboard successfully.');
+ }).catch(err => {
+ console.error('Failed to copy text to clipboard:', err);
+ });
+}
+
+function showGeoMenu(plusCode) {
+ closeOverlay();
+ var latLongString = Android.getCoordinatesForPlusCode(plusCode);
+ var latLong = JSON.parse(latLongString);
+ var LatitudeLongitude = latLong.latitude + " " + latLong.longitude;
+ var m = '';
+ m += "" + plusCode + " ";
+ m += "Lat: " + latLong.latitude + " Long: " + latLong.longitude + " ";
+ m += "Show Location";
+ document.getElementById("geo-menu").innerHTML = m;
+ document.getElementById("geo-menu").style.display = 'initial';
+ document.getElementById("overlay-trans").style.display = 'initial';
+}
+
+function showGeoVoiceMenu(plusCode, chat, key) {
+ closeOverlay();
+ var latLongString = Android.getCoordinatesForPlusCode(plusCode);
+ var latLong = JSON.parse(latLongString);
+ var LatitudeLongitude = latLong.latitude + " " + latLong.longitude;
+ var m = '';
+ m += "" + plusCode + " ";
+ m += "Lat: " + latLong.latitude + " Long: " + latLong.longitude + " ";
+ m += "Show Location ";
+ m += "Play Voice Message";
+ document.getElementById("geo-menu").innerHTML = m;
+ document.getElementById("geo-menu").style.display = 'initial';
+ document.getElementById("overlay-trans").style.display = 'initial';
+}
+
// ---
\ No newline at end of file
diff --git a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/Settings.kt b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/Settings.kt
index e7d0cfc..a11907d 100644
--- a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/Settings.kt
+++ b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/Settings.kt
@@ -1,6 +1,7 @@
package nz.scuttlebutt.tremolavossbol
import android.content.Context
+import android.util.Log
import nz.scuttlebutt.tremolavossbol.utils.Constants.Companion.TINYSSB_SIMPLEPUB_URL
import org.json.JSONObject
@@ -20,7 +21,8 @@ class Settings(val context: MainActivity) {
"ble_enabled" to "true",
"udp_multicast_enabled" to "true",
"websocket_enabled" to "false",
- "websocket_url" to TINYSSB_SIMPLEPUB_URL
+ "websocket_url" to TINYSSB_SIMPLEPUB_URL,
+ "geo_location_enabled" to "true"
)
fun getSettings(): String {
val currentSettings = mutableMapOf()
@@ -40,6 +42,7 @@ class Settings(val context: MainActivity) {
// Sets a value for the provided settingID and executes necessary backend actions to align with the updated setting.
fun set(settingID: String, value: String): Boolean {
+ Log.d("set", "ID=${settingID} val=${value}")
if (!defaultSettings.keys.contains(settingID)) {
return false // default of setting id must be defined
}
@@ -52,6 +55,7 @@ class Settings(val context: MainActivity) {
"ble_enabled" -> handleBleEnabled(value.toBoolean())
"udp_multicast_enabled" -> handleUdpMulticastEnabled(value.toBoolean())
"websocket_url" -> handleWebsocketUrl(value)
+ "geo_location_enabled" -> handleGeoLocation(value.toBoolean())
}
return true
}
@@ -75,6 +79,10 @@ class Settings(val context: MainActivity) {
return sharedPreferences.getString("udp_multicast_enabled", defaultSettings["udp_multicast_enabled"]).toBoolean()
}
+ fun isGeoLocationEnabled(): Boolean {
+ return sharedPreferences.getString("geo_location_enabled", defaultSettings["geo_location_enabled"]).toBoolean()
+ }
+
fun getWebsocketUrl(): String {
return sharedPreferences.getString("websocket_url", defaultSettings["websocket_url"])!!
}
@@ -104,4 +112,13 @@ class Settings(val context: MainActivity) {
context.websocket?.updateUrl(value)
}
+ fun handleGeoLocation(value: Boolean) {
+ Log.d("set", "no side effects yet for setting geoLocation to ${value}")
+ if (value) {
+ //TODO Stop sending location
+ } else {
+ //TODO start sending location
+ }
+ }
+
}
\ No newline at end of file
diff --git a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/WebAppInterface.kt b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/WebAppInterface.kt
index 7cb34f9..f5cd41a 100644
--- a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/WebAppInterface.kt
+++ b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/WebAppInterface.kt
@@ -12,10 +12,16 @@ import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.widget.Toast
import androidx.core.content.ContextCompat.checkSelfPermission
+import androidx.annotation.RequiresPermission
import com.google.zxing.integration.android.IntentIntegrator
+import com.google.android.gms.location.LocationServices
+import com.google.android.gms.tasks.CancellationTokenSource
+import com.google.android.gms.tasks.Tasks
+
import org.json.JSONArray
import org.json.JSONObject
-
+import java.sql.Time
+import java.util.concurrent.TimeUnit
import nz.scuttlebutt.tremolavossbol.utils.Bipf
import nz.scuttlebutt.tremolavossbol.utils.Bipf.Companion.BIPF_BYTES
@@ -33,6 +39,7 @@ import nz.scuttlebutt.tremolavossbol.utils.Constants.Companion.TINYSSB_APP_SCHED
import nz.scuttlebutt.tremolavossbol.utils.HelperFunctions.Companion.deRef
import nz.scuttlebutt.tremolavossbol.utils.HelperFunctions.Companion.toBase64
import nz.scuttlebutt.tremolavossbol.utils.HelperFunctions.Companion.toHex
+import nz.scuttlebutt.tremolavossbol.utils.PlusCodesUtils
import nz.scuttlebutt.tremolavossbol.games.battleships.BattleshipGame
import nz.scuttlebutt.tremolavossbol.games.common.GamesHandler
import nz.scuttlebutt.tremolavossbol.games.battleships.GameStates
@@ -47,6 +54,51 @@ class WebAppInterface(val act: MainActivity, val webView: WebView, val gameHandl
val frontend_frontier = act.getSharedPreferences("frontend_frontier", Context.MODE_PRIVATE)
var gamesHandler = gameHandler
+ /**
+ * Retrieves the current geolocation of the Android device and returns it as a PlusCode.
+ */
+ @JavascriptInterface
+ @RequiresPermission(
+ anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION],
+ )
+ fun getCurrentLocationAsPlusCode(): String {
+ val locationClient = LocationServices.getFusedLocationProviderClient(act)
+
+ try {
+ val currentLocationTask = locationClient.getCurrentLocation(102, CancellationTokenSource().token)
+ val currentLocation = Tasks.await(currentLocationTask, 2, TimeUnit.SECONDS)
+ return PlusCodesUtils.encode(currentLocation.latitude, currentLocation.longitude)
+ } catch (e: Exception) {
+ val lastLocationTast = locationClient.lastLocation
+ try {
+ val lastLocation = Tasks.await(lastLocationTast, 2, TimeUnit.SECONDS)
+ return PlusCodesUtils.encode(lastLocation.latitude, lastLocation.longitude)
+ } catch (e: Exception) {
+ Toast.makeText(act, "Failed to get location. Location is not sent with this message.", Toast.LENGTH_LONG).show()
+ return ""
+ }
+ }
+
+ }
+
+ @JavascriptInterface
+ fun getCoordinatesForPlusCode(code: String): String {
+ val (latitude, longitude) = PlusCodesUtils.decode(code)
+ return JSONObject()
+ .put("latitude", latitude)
+ .put("longitude", longitude)
+ .toString()
+ }
+
+ @JavascriptInterface
+ fun isGeoLocationEnabled(): String {
+ return if (act.settings!!.isGeoLocationEnabled()) {
+ "true"
+ } else {
+ "false"
+ }
+ }
+
@JavascriptInterface
fun onFrontendRequest(s: String) {
//handle the data captured from webview}
diff --git a/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/utils/PlusCodesUtils.kt b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/utils/PlusCodesUtils.kt
new file mode 100644
index 0000000..a29ee1a
--- /dev/null
+++ b/android/tinySSB/app/src/main/java/nz/scuttlebutt/tremolavossbol/utils/PlusCodesUtils.kt
@@ -0,0 +1,91 @@
+package nz.scuttlebutt.tremolavossbol.utils
+
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.pow
+
+/**
+ * Utility class to encode and decode location coordinates to/from a Google Plus Code.
+ *
+ * Created by translating [this](https://github.com/google/open-location-code/blob/main/java/src/main/java/com/google/openlocationcode/OpenLocationCode.java)
+ * Java code (freely provided by Google) to Kotlin
+ */
+object PlusCodesUtils {
+ private const val CODE_ALPHABET: String = "23456789CFGHJMPQRVWX"
+ private const val SEPARATOR: Char = '+'
+ private const val MAX_DIGIT_COUNT: Int = 15
+ private const val PAIR_CODE_LENGTH: Int = 10
+ private const val GRID_CODE_LENGTH: Int = MAX_DIGIT_COUNT - PAIR_CODE_LENGTH
+ private const val ENCODING_BASE = CODE_ALPHABET.length
+ private const val LATITUDE_MAX: Int = 90
+ private const val LONGITUDE_MAX: Int = 180
+ private const val GRID_COLUMNS: Int = 4
+ private const val GRID_ROWS: Int = 5
+ private const val LAT_INTEGER_MULTIPLIER: Long = 8000 * 3125
+ private const val LNG_INTEGER_MULTIPLIER: Long = 8000 * 1024
+ private const val LAT_MSP_VALUE: Long = LAT_INTEGER_MULTIPLIER * ENCODING_BASE * ENCODING_BASE
+ private const val LNG_MSP_VALUE: Long = LNG_INTEGER_MULTIPLIER * ENCODING_BASE * ENCODING_BASE
+
+ fun encode(latitude: Double, longitude: Double): String {
+ var mutableLatitude = clipLatitude(latitude)
+
+ // Latitude 90 needs to be adjusted to be just less, so the returned code can also be decoded.
+ if (mutableLatitude == LATITUDE_MAX.toDouble()) {
+ mutableLatitude = 89.9998875
+ }
+
+ val revBuilder = StringBuilder()
+ var latVal = getLatVal(mutableLatitude)
+ var lngVal = getLngVal(normalizeLongitude(longitude))
+
+ for (i in 0..4) {
+ revBuilder.append(CODE_ALPHABET[(lngVal % ENCODING_BASE).toInt()])
+ revBuilder.append(CODE_ALPHABET[(latVal % ENCODING_BASE).toInt()])
+ latVal /= ENCODING_BASE
+ lngVal /= ENCODING_BASE
+
+ if (i == 0) {
+ revBuilder.append(SEPARATOR)
+ }
+ }
+ return revBuilder.reverse().toString()
+ }
+
+ private fun clipLatitude(latitude: Double): Double {
+ return min(max(latitude, -LATITUDE_MAX.toDouble()), LATITUDE_MAX.toDouble())
+ }
+
+ private fun normalizeLongitude(longitude: Double): Double {
+ if (longitude >= -LONGITUDE_MAX && longitude < LONGITUDE_MAX)
+ return longitude
+ val circleDeg = 2 * LONGITUDE_MAX
+ return (longitude % circleDeg + circleDeg + LONGITUDE_MAX) % circleDeg - LONGITUDE_MAX
+
+ }
+
+ private fun getLatVal(latitude: Double) =
+ (Math.round((latitude + LATITUDE_MAX) * LAT_INTEGER_MULTIPLIER * 1e6) / 1e6 /
+ GRID_ROWS.toDouble().pow(GRID_CODE_LENGTH)).toLong()
+
+ private fun getLngVal(longitude: Double) =
+ (Math.round((longitude + LONGITUDE_MAX) * LNG_INTEGER_MULTIPLIER * 1e6) / 1e6 /
+ GRID_COLUMNS.toDouble().pow(GRID_CODE_LENGTH)).toLong()
+
+ fun decode(code: String): Pair {
+ val clean = code.filterNot { it == SEPARATOR }
+
+ var latVal = -LATITUDE_MAX * LAT_INTEGER_MULTIPLIER
+ var lngVal = -LONGITUDE_MAX * LNG_INTEGER_MULTIPLIER
+
+ var latPlaceVal = LAT_MSP_VALUE
+ var lngPlaceVal = LNG_MSP_VALUE
+ for (i in clean.indices step 2) {
+ latPlaceVal /= ENCODING_BASE
+ lngPlaceVal /= ENCODING_BASE
+ latVal += CODE_ALPHABET.indexOf(clean[i]) * latPlaceVal
+ lngVal += CODE_ALPHABET.indexOf(clean[i + 1]) * lngPlaceVal
+ }
+ return Pair(latVal.toDouble() / LAT_INTEGER_MULTIPLIER,
+ lngVal.toDouble() / LNG_INTEGER_MULTIPLIER)
+ }
+}