Commit e069ace8 authored by Adrian Cochrane's avatar Adrian Cochrane
Browse files

Publish basic code, now that all the functionality's there.

parent f99a31ca
/build
\ No newline at end of file
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 29
buildToolsVersion "29.0.3"
defaultConfig {
applicationId "com.macbros.myapplication"
minSdkVersion 16
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
splits { abi { enable false } }
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.gms:play-services-maps:17.0.0'
implementation 'com.google.android.gms:play-services-location:17.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
//h3 library
implementation "com.uber:h3:3.4.1"
implementation 'androidx.cardview:cardview:1.0.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
\ No newline at end of file
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
\ No newline at end of file
package com.macbros.myapplication
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.macbros.myapplication", appContext.packageName)
}
}
\ No newline at end of file
<resources>
<!--
TODO: Before you run your application, you need a Google Maps API key.
To get one, follow this link, follow the directions and press "Create" at the end:
https://console.developers.google.com/flows/enableapi?apiid=maps_android_backend&keyType=CLIENT_SIDE_ANDROID&r=03:A4:86:18:AE:4B:1D:E7:F6:FA:99:FC:36:0A:A5:3F:E5:32:B3:E9%3Bcom.macbros.myapplication
You can also add your credentials to an existing key, using these values:
Package name:
com.macbros.myapplication
SHA-1 certificate fingerprint:
03:A4:86:18:AE:4B:1D:E7:F6:FA:99:FC:36:0A:A5:3F:E5:32:B3:E9
Alternatively, follow the directions here:
https://developers.google.com/maps/documentation/android/start#get-key
Once you have your key (it starts with "AIza"), replace the "google_maps_key"
string in this file.
-->
<string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">AIzaSyAWcCoFaQokgeCB_4e1mO8J9PLluTFWSqM</string>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.macbros.myapplication">
<!--
The ACCESS_COARSE/FINE_LOCATION permissions are not required to use
Google Maps Android API v2, but you must specify either coarse or fine
location permissions for the "MyLocation" functionality.
-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<!--
The API key for Google Maps-based APIs is defined as a string resource.
(See the file "res/values/google_maps_api.xml").
Note that the API key is linked to the encryption key used to sign the APK.
You need a different API key for each encryption key, including the release key that is used to
sign the APK for publishing.
You can define the keys for the debug and release targets in src/debug/ and src/release/.
-->
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="@string/google_maps_key" />
<activity
android:name=".MapsActivity"
android:label="@string/title_activity_maps">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
\ No newline at end of file
package com.macbros.myapplication
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.IntentSender
import android.content.pm.PackageManager
import android.location.Location
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Xml
import androidx.core.app.ActivityCompat
import com.google.android.gms.common.api.ResolvableApiException
import com.google.android.gms.location.*
import com.uber.h3core.H3Core
import kotlinx.android.synthetic.main.activity_maps.*
import org.xmlpull.v1.XmlPullParser
import java.lang.IllegalStateException
import java.net.URL
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList
class MapsActivity : AppCompatActivity() {
private val logfile: String = "locations.tsv"
private lateinit var fusedLocationClient: FusedLocationProviderClient
private lateinit var lastLocation: Location
private lateinit var locationCallback: LocationCallback
private lateinit var locationRequest: LocationRequest
private var locationUpdateState = false
private lateinit var h3: H3Core
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_maps)
h3 = H3Core.newSystemInstance()
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
locationCallback = object : LocationCallback() {
override fun onLocationResult(p0: LocationResult) {
super.onLocationResult(p0)
lastLocation = p0.lastLocation
val latlongS= "Lat: " + lastLocation.latitude.toString() + " Long: " + lastLocation.longitude.toString()
val h3Address = h3.geoToH3Address(lastLocation.latitude, lastLocation.longitude, 14)
val hex = h3.stringToH3(h3Address)
val kRingsResult = h3.kRings(h3Address, 1)
latlong.text = latlongS
h3Addr.text = h3Address
stringToH3.text = hex.toString()
krings.text = kRingsResult
.toString()
val file = applicationContext.openFileOutput(logfile, Context.MODE_APPEND)
val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss z", Locale.US)
val line = h3Address + "\t" + formatter.format(Date(System.currentTimeMillis())) + "\n"
file.write(line.encodeToByteArray())
}
}
createLocationRequest()
shareBtn.setOnClickListener {
val log = applicationContext.openFileInput(logfile).bufferedReader().readText()
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, log)
putExtra(Intent.EXTRA_TITLE, logfile)
type = "text/tab-separated-values"
}
val shareIntent = Intent.createChooser(sendIntent, null)
startActivity(shareIntent)
}
recheckBtn.setOnClickListener {
val source = notificationSource.text.toString()
if (source.startsWith("http://") || source.startsWith("https://"))
hittestFeed(notificationSource.text.toString())
else latlong.text = hittest(source)
}
}
private fun hittest(text: String, message: String = "WARNING! You may have been in contact with COVID19!"): String {
val zone = h3.stringToH3(text)
var resolution = h3.h3GetResolution(zone)
val log = applicationContext.openFileInput(logfile).bufferedReader()
val zones = ArrayList<Long>()
for (line in log.readLines()) {
// FIXME consider time
zones.add(h3.stringToH3(line.split('\t')[0]))
}
for (other in zones) {
if (h3.h3ToParent(other, resolution) == zone) return message
}
while (resolution > 0) {
if (zones.contains(h3.h3ToParent(zone, resolution))) return message
resolution--
}
return ""
}
private fun requestXML(url: String): XmlPullParser {
val resp = URL(url).openConnection()
val parser = Xml.newPullParser()
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
parser.setInput(resp.getInputStream(), null)
parser.nextTag()
return parser
}
private fun hittestFeed(url: String) {
// TODO add parameter for zoneID, which resolution should that be at?
val xml = requestXML(url)
xml.require(XmlPullParser.START_TAG, null, "feed")
while (xml.next() != XmlPullParser.END_TAG) {
if (xml.eventType != XmlPullParser.START_TAG) continue
if (xml.name == "entry") {
val entry = readEntry(xml)
if (entry.content == null) continue
for (line in entry.content.split('\n')) {
var message = "WARNING! You may have been in contact with COVID19!"
if (entry.summary != null) message = entry.summary
else if (entry.title != null) message = entry.title
latlong.text = hittest(line.split('\t')[0], message)
}
} else {
skip(xml)
}
}
}
data class Entry(val title: String?, val summary: String?, val content: String?)
private fun readEntry(xml: XmlPullParser, type: String = "text/tab-separated-values"): Entry {
xml.require(XmlPullParser.START_TAG, null, "entry")
var title: String? = null
var summary: String? = null
var content: String? = null
while (xml.next() != XmlPullParser.END_TAG) {
if (xml.eventType != XmlPullParser.START_TAG) continue
when (xml.name) {
"title" -> title = readText(xml)
"summary" -> summary = readText(xml)
"content" -> if (xml.getAttributeValue(null, "type") == type)
content = readText(xml)
}
}
return Entry(title, summary, content)
}
private fun readText(xml: XmlPullParser): String {
var result = ""
if (xml.next() == XmlPullParser.TEXT) {
result = xml.text
xml.nextTag()
}
return result
}
private fun skip(xml: XmlPullParser) {
if (xml.eventType != XmlPullParser.START_TAG) throw IllegalStateException()
var depth = 1
while (depth != 0) {
when (xml.next()) {
XmlPullParser.END_TAG -> depth--
XmlPullParser.START_TAG -> depth++
}
}
}
private fun startLocationUpdates() {
//1
if (ActivityCompat.checkSelfPermission(this,
android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
arrayOf(android.Manifest.permission.ACCESS_FINE_LOCATION),
LOCATION_PERMISSION_REQUEST_CODE)
return
}
//2
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, null /* Looper */)
}
private fun createLocationRequest() {
// 1
locationRequest = LocationRequest()
// 2
locationRequest.interval = 10000
// 3
locationRequest.fastestInterval = 5000
locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
val builder = LocationSettingsRequest.Builder()
.addLocationRequest(locationRequest)
// 4
val client = LocationServices.getSettingsClient(this)
val task = client.checkLocationSettings(builder.build())
// 5
task.addOnSuccessListener {
locationUpdateState = true
startLocationUpdates()
}
task.addOnFailureListener { e ->
// 6
if (e is ResolvableApiException) {
// Location settings are not satisfied, but this can be fixed
// by showing the user a dialog.
try {
// Show the dialog by calling startResolutionForResult(),
// and check the result in onActivityResult().
e.startResolutionForResult(this@MapsActivity,
REQUEST_CHECK_SETTINGS)
} catch (sendEx: IntentSender.SendIntentException) {
// Ignore the error.
}
}
}
}
override fun onPause() {
super.onPause()
fusedLocationClient.removeLocationUpdates(locationCallback)
}
// 3
public override fun onResume() {
super.onResume()
if (!locationUpdateState) {
startLocationUpdates()
}
}
companion object {
private const val LOCATION_PERMISSION_REQUEST_CODE = 1
// 3
private const val REQUEST_CHECK_SETTINGS = 2
}
// 1
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CHECK_SETTINGS) {
if (resultCode == Activity.RESULT_OK) {
locationUpdateState = true
startLocationUpdates()
}
}
}
}
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path