Livebeat is now able to send, store and show beats
This commit is contained in:
@@ -35,13 +35,16 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
implementation 'androidx.core:core-ktx:1.2.0'
|
implementation 'androidx.core:core-ktx:1.3.2'
|
||||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||||
implementation 'com.google.android.material:material:1.1.0'
|
implementation 'com.google.android.material:material:1.1.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.2.2'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.2.2'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.2.2'
|
implementation 'androidx.navigation:navigation-ui-ktx:2.2.2'
|
||||||
|
implementation 'com.squareup.moshi:moshi:1.11.0'
|
||||||
|
implementation 'com.squareup.moshi:moshi-kotlin:1.11.0'
|
||||||
implementation 'com.rabbitmq:amqp-client:5.9.0'
|
implementation 'com.rabbitmq:amqp-client:5.9.0'
|
||||||
|
implementation "com.squareup.okhttp3:okhttp:4.9.0"
|
||||||
testImplementation 'junit:junit:4.+'
|
testImplementation 'junit:junit:4.+'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="de.nicolasklier.livebeat">
|
package="de.nicolasklier.livebeat">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"></uses-permission>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.sensor.gyroscope"
|
||||||
|
android:required="true" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -11,6 +18,7 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
android:theme="@style/Theme.Livebeat">
|
android:theme="@style/Theme.Livebeat">
|
||||||
<service android:name=".TrackerService" />
|
<service android:name=".TrackerService" />
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
package de.nicolasklier.livebeat
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Button
|
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple [Fragment] subclass as the default destination in the navigation.
|
|
||||||
*/
|
|
||||||
class FirstFragment : Fragment() {
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
// Inflate the layout for this fragment
|
|
||||||
return inflater.inflate(R.layout.fragment_first, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
view.findViewById<Button>(R.id.button_first).setOnClickListener {
|
|
||||||
findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +1,205 @@
|
|||||||
package de.nicolasklier.livebeat
|
package de.nicolasklier.livebeat
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
|
@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic val API_URL = "http://192.168.178.26:8040"
|
||||||
|
var TOKEN = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private var broadcastReceiver: BroadcastReceiver? = null
|
||||||
|
|
||||||
|
@SuppressLint("HardwareIds")
|
||||||
|
fun checkIfPhoneIsRegistered() {
|
||||||
|
if (TOKEN == "") return;
|
||||||
|
Thread(Runnable {
|
||||||
|
val androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
|
||||||
|
val client = OkHttpClient()
|
||||||
|
val req = Request.Builder()
|
||||||
|
.url("$API_URL/phone/$androidId")
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
val response = client.newCall(req).execute()
|
||||||
|
|
||||||
|
if (response.code != 200) {
|
||||||
|
Snackbar.make(findViewById<FloatingActionButton>(R.id.fab), "Device isn't registered yet.", Snackbar.LENGTH_SHORT)
|
||||||
|
.setBackgroundTint(Color.YELLOW)
|
||||||
|
.setTextColor(Color.BLACK)
|
||||||
|
.show()
|
||||||
|
|
||||||
|
// Register device
|
||||||
|
val phone = Phone(
|
||||||
|
androidId,
|
||||||
|
Build.MODEL,
|
||||||
|
Build.PRODUCT,
|
||||||
|
Build.VERSION.RELEASE,
|
||||||
|
System.getProperty("os.arch")
|
||||||
|
)
|
||||||
|
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
|
||||||
|
val phoneToJson = moshi.adapter(Phone::class.java)
|
||||||
|
val json = phoneToJson.toJson(phone)
|
||||||
|
|
||||||
|
val createPhone = Request.Builder()
|
||||||
|
.url("$API_URL/phone")
|
||||||
|
.post(
|
||||||
|
(json).toRequestBody()
|
||||||
|
)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("token", TOKEN)
|
||||||
|
.build()
|
||||||
|
client.newCall(createPhone).execute()
|
||||||
|
}
|
||||||
|
}).start()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
checkPerms()
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
setSupportActionBar(findViewById(R.id.toolbar))
|
setSupportActionBar(findViewById(R.id.toolbar))
|
||||||
startService(Intent(this, TrackerService::class.java))
|
startService(Intent(this, TrackerService::class.java))
|
||||||
|
|
||||||
|
// Check authorization
|
||||||
|
val backendChecks = Thread(Runnable {
|
||||||
|
val username = findViewById<TextView>(R.id.username).text
|
||||||
|
val password = findViewById<TextView>(R.id.password).text
|
||||||
|
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
|
||||||
|
val jsonToLogin = moshi.adapter(Login::class.java)
|
||||||
|
|
||||||
|
val client = OkHttpClient()
|
||||||
|
val req = Request.Builder()
|
||||||
|
.url("$API_URL/user/login")
|
||||||
|
.post(
|
||||||
|
("{ \"username\": \"" + username + "\"," +
|
||||||
|
"\"password\": \"" + password + "\" }").toRequestBody()
|
||||||
|
)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.build()
|
||||||
|
val loginResponse = client.newCall(req).execute()
|
||||||
|
val responseBody = loginResponse.body!!.string()
|
||||||
|
|
||||||
|
if (loginResponse.code == 200) {
|
||||||
|
TOKEN = jsonToLogin.fromJson(responseBody)!!.token
|
||||||
|
|
||||||
|
// Since we are in another thread, we have to get back to the UI thread.
|
||||||
|
this.runOnUiThread(Runnable {
|
||||||
|
findViewById<TextView>(R.id.httpStatus).setTextColor(Color.GREEN)
|
||||||
|
findViewById<TextView>(R.id.httpStatus).text = "CONNECTED"
|
||||||
|
})
|
||||||
|
|
||||||
|
Snackbar.make(findViewById<FloatingActionButton>(R.id.fab), "Login succeeded", Snackbar.LENGTH_SHORT)
|
||||||
|
.setBackgroundTint(Color.GREEN)
|
||||||
|
.setActionTextColor(Color.WHITE)
|
||||||
|
.show()
|
||||||
|
|
||||||
|
checkIfPhoneIsRegistered()
|
||||||
|
} else {
|
||||||
|
// Since we are in another thread, we have to get back to the UI thread.
|
||||||
|
this.runOnUiThread(Runnable {
|
||||||
|
findViewById<TextView>(R.id.httpStatus).setTextColor(Color.RED)
|
||||||
|
findViewById<TextView>(R.id.httpStatus).text = "LOGIN FAILED"
|
||||||
|
})
|
||||||
|
Snackbar.make(findViewById<FloatingActionButton>(R.id.fab), "Login failed", Snackbar.LENGTH_LONG)
|
||||||
|
.setBackgroundTint(Color.RED)
|
||||||
|
.setActionTextColor(Color.WHITE)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
backendChecks.start()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Here we receive updates from our tracker service.
|
||||||
|
*/
|
||||||
|
this.broadcastReceiver = object : BroadcastReceiver() {
|
||||||
|
@SuppressLint("CutPasteId")
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val statusRabbit = intent.getBooleanExtra("statusRabbit", false)
|
||||||
|
val statusHttp = intent.getIntExtra("statusHttp", 404)
|
||||||
|
|
||||||
|
if (statusRabbit) {
|
||||||
|
findViewById<TextView>(R.id.rabbitStatus).text = "connected"
|
||||||
|
findViewById<TextView>(R.id.rabbitStatus).setTextColor(Color.GREEN)
|
||||||
|
} else {
|
||||||
|
findViewById<TextView>(R.id.rabbitStatus).text = "disconnected"
|
||||||
|
findViewById<TextView>(R.id.rabbitStatus).setTextColor(Color.RED)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusHttp == 200) {
|
||||||
|
findViewById<TextView>(R.id.httpStatus).text = "ONLINE (no login)"
|
||||||
|
findViewById<TextView>(R.id.httpStatus).setTextColor(Color.CYAN)
|
||||||
|
findViewById<Button>(R.id.signin).isEnabled = false
|
||||||
|
findViewById<Button>(R.id.signin).text = "Signed in"
|
||||||
|
} else {
|
||||||
|
findViewById<TextView>(R.id.httpStatus).text = "OFFLINE"
|
||||||
|
findViewById<TextView>(R.id.httpStatus).setTextColor(Color.RED)
|
||||||
|
findViewById<Button>(R.id.signin).isEnabled = true
|
||||||
|
findViewById<Button>(R.id.signin).text = "Sign in"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerReceiver(broadcastReceiver, IntentFilter("de.nicolasklier.livebeat"))
|
||||||
|
|
||||||
findViewById<FloatingActionButton>(R.id.fab).setOnClickListener { view ->
|
findViewById<FloatingActionButton>(R.id.fab).setOnClickListener { view ->
|
||||||
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
|
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
|
||||||
.setAction("Action", null).show()
|
.setAction("Action", null).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun checkPerms() {
|
||||||
|
if (ActivityCompat.checkSelfPermission(
|
||||||
|
this,
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
|
) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
|
||||||
|
this,
|
||||||
|
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||||
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
|
||||||
|
if (broadcastReceiver != null) {
|
||||||
|
unregisterReceiver(broadcastReceiver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
// Inflate the menu; this adds items to the action bar if it is present.
|
// Inflate the menu; this adds items to the action bar if it is present.
|
||||||
menuInflater.inflate(R.menu.menu_main, menu)
|
menuInflater.inflate(R.menu.menu_main, menu)
|
||||||
@@ -37,4 +215,28 @@ class MainActivity : AppCompatActivity() {
|
|||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(
|
||||||
|
requestCode: Int,
|
||||||
|
permissions: Array<out String>,
|
||||||
|
grantResults: IntArray
|
||||||
|
) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
if (requestCode == 1) {
|
||||||
|
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
Snackbar.make(findViewById<FloatingActionButton>(R.id.fab), "Location access is allowed", Snackbar.LENGTH_SHORT)
|
||||||
|
.setBackgroundTint(Color.GREEN)
|
||||||
|
.setAction("Action", null).show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Snackbar.make(findViewById<FloatingActionButton>(R.id.fab), "Location access is required.", Snackbar.LENGTH_INDEFINITE)
|
||||||
|
.setBackgroundTint(Color.RED)
|
||||||
|
.setActionTextColor(Color.WHITE)
|
||||||
|
.setAction("Grant", View.OnClickListener {
|
||||||
|
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 1)
|
||||||
|
}).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
26
android/app/src/main/java/de/nicolasklier/livebeat/Models.kt
Normal file
26
android/app/src/main/java/de/nicolasklier/livebeat/Models.kt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package de.nicolasklier.livebeat
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import com.squareup.moshi.ToJson
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents one singe beat that will be send to the database.
|
||||||
|
*/
|
||||||
|
class Beat(
|
||||||
|
final val token: String, // Token of this phone
|
||||||
|
final val gpsLocation: Array<Double>,
|
||||||
|
final val battery: Int?,
|
||||||
|
final val timestamp: Long
|
||||||
|
) {}
|
||||||
|
|
||||||
|
class Phone(
|
||||||
|
final val androidId: String,
|
||||||
|
final val modelName: String,
|
||||||
|
final val displayName: String,
|
||||||
|
final val operatingSystem: String,
|
||||||
|
final val architecture: String
|
||||||
|
) {}
|
||||||
|
|
||||||
|
class Login(
|
||||||
|
final val token: String
|
||||||
|
) {}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package de.nicolasklier.livebeat
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Button
|
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple [Fragment] subclass as the second destination in the navigation.
|
|
||||||
*/
|
|
||||||
class SecondFragment : Fragment() {
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
// Inflate the layout for this fragment
|
|
||||||
return inflater.inflate(R.layout.fragment_second, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
view.findViewById<Button>(R.id.button_second).setOnClickListener {
|
|
||||||
findNavController().navigate(R.id.action_SecondFragment_to_FirstFragment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
package de.nicolasklier.livebeat;
|
|
||||||
|
|
||||||
import android.app.Notification;
|
|
||||||
import android.app.NotificationChannel;
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentFilter;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.app.NotificationCompat;
|
|
||||||
|
|
||||||
import com.rabbitmq.client.Channel;
|
|
||||||
import com.rabbitmq.client.Connection;
|
|
||||||
import com.rabbitmq.client.ConnectionFactory;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
|
|
||||||
public class TrackerService extends Service {
|
|
||||||
|
|
||||||
private static final int NOTIF_ID = 1;
|
|
||||||
private static final String NOTIF_CHANNEL_ID = "LiveTracker";
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(Intent intent) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
||||||
final Connection[] conn = new Connection[1];
|
|
||||||
|
|
||||||
// This thread only connects to RabbitMQ
|
|
||||||
Thread thread = new Thread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
ConnectionFactory factory = new ConnectionFactory();
|
|
||||||
factory.setUsername("lineage");
|
|
||||||
factory.setPassword("ZSo$X97GQ547JXL7nGq");
|
|
||||||
factory.setVirtualHost("/");
|
|
||||||
factory.setHost("192.168.178.26");
|
|
||||||
factory.setPort(5672);
|
|
||||||
|
|
||||||
try {
|
|
||||||
conn[0] = factory.newConnection();
|
|
||||||
Channel channel = conn[0].createChannel();
|
|
||||||
channel.queueDeclare("Tracker", false, false, false, null);
|
|
||||||
channel.basicPublish("", "Tracker", null, "Test message!!!!!!!!!!!!!!!!!".getBytes());
|
|
||||||
Log.i("RabbitMQ", "run: Published test message");
|
|
||||||
} catch (IOException | TimeoutException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
thread.start();
|
|
||||||
startForeground();
|
|
||||||
|
|
||||||
return super.onStartCommand(intent, flags, startId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startForeground() {
|
|
||||||
Intent noticicationIntent = new Intent(this, MainActivity.class);
|
|
||||||
|
|
||||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, noticicationIntent, 0);
|
|
||||||
|
|
||||||
NotificationChannel chan = new NotificationChannel(
|
|
||||||
NOTIF_CHANNEL_ID,
|
|
||||||
"Tracker",
|
|
||||||
NotificationManager.IMPORTANCE_LOW);
|
|
||||||
chan.setLightColor(Color.BLACK);
|
|
||||||
chan.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
|
|
||||||
|
|
||||||
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
|
||||||
assert manager != null;
|
|
||||||
manager.createNotificationChannel(chan);
|
|
||||||
|
|
||||||
startForeground(NOTIF_ID, new NotificationCompat.Builder(this, NOTIF_CHANNEL_ID)
|
|
||||||
.setOngoing(true)
|
|
||||||
.setContentTitle("Livebeat")
|
|
||||||
.setContentText("Tracker is running")
|
|
||||||
.setContentIntent(pendingIntent)
|
|
||||||
.setCategory(Notification.CATEGORY_SERVICE)
|
|
||||||
.setPriority(NotificationManager.IMPORTANCE_LOW)
|
|
||||||
.setChannelId(NOTIF_CHANNEL_ID)
|
|
||||||
.setColor(Color.BLACK)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package de.nicolasklier.livebeat
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.*
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.location.LocationManager
|
||||||
|
import android.os.BatteryManager
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import com.rabbitmq.client.Channel
|
||||||
|
import com.rabbitmq.client.Connection
|
||||||
|
import com.rabbitmq.client.ConnectionFactory
|
||||||
|
import com.squareup.moshi.JsonAdapter
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.TimeoutException
|
||||||
|
|
||||||
|
class TrackerService : Service() {
|
||||||
|
|
||||||
|
val conn = arrayOfNulls<Connection>(1)
|
||||||
|
val channel = arrayOfNulls<Channel>(1)
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun connectWithBroker() {
|
||||||
|
// This thread only connects to RabbitMQ
|
||||||
|
val connectionThread = Thread(Runnable {
|
||||||
|
val client = OkHttpClient()
|
||||||
|
val req = Request.Builder()
|
||||||
|
.url(MainActivity.API_URL)
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val factory = ConnectionFactory()
|
||||||
|
factory.username = "lineage"
|
||||||
|
factory.password = "ZSo\$X97GQ547JXL7nGq"
|
||||||
|
factory.virtualHost = "/"
|
||||||
|
factory.host = "nk-home.ddns.net"
|
||||||
|
factory.port = 5672
|
||||||
|
factory.isAutomaticRecoveryEnabled = true
|
||||||
|
try {
|
||||||
|
conn[0] = factory.newConnection()
|
||||||
|
channel[0] = conn[0]?.createChannel()
|
||||||
|
|
||||||
|
val intent = Intent("de.nicolasklier.livebeat")
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.putBoolean("statusRabbit", true)
|
||||||
|
bundle.putInt("statusHttp", client.newCall(req).execute().code)
|
||||||
|
intent.putExtras(bundle)
|
||||||
|
this.sendBroadcast(intent)
|
||||||
|
|
||||||
|
channel[0]?.queueDeclare("tracker", true, false, false, null)
|
||||||
|
//channel[0]?.basicPublish("", "Tracker", null, "Test message".toByteArray())
|
||||||
|
Log.i("RabbitMQ", "run: Published test message")
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
} catch (e: TimeoutException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
connectionThread.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("HardwareIds")
|
||||||
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||||
|
val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||||
|
|
||||||
|
// Android id as unique identifier
|
||||||
|
val androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
|
||||||
|
|
||||||
|
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
locationManager.requestLocationUpdates("gps", 5000, 0f
|
||||||
|
) { location ->
|
||||||
|
Log.i("Location", "Location is: " + location.latitude + " | " + location.longitude)
|
||||||
|
|
||||||
|
/* Get battery */
|
||||||
|
val batteryStatus: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { ifilter ->
|
||||||
|
this@TrackerService.registerReceiver(null, ifilter)
|
||||||
|
}
|
||||||
|
val batteryPct: Float? = batteryStatus?.let { intent ->
|
||||||
|
val level: Int = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
|
||||||
|
val scale: Int = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
|
||||||
|
level * 100 / scale.toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
val beat = Beat(androidId, arrayOf(location.latitude, location.longitude, location.accuracy.toDouble(), location.speed.toDouble()), batteryPct?.toInt(), location.time)
|
||||||
|
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
|
||||||
|
val jsonAdapter: JsonAdapter<Beat> = moshi.adapter(Beat::class.java)
|
||||||
|
|
||||||
|
Thread(Runnable {
|
||||||
|
channel[0]?.basicPublish("", "tracker", null, jsonAdapter.toJson(beat).toByteArray())
|
||||||
|
}).start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectWithBroker()
|
||||||
|
startForeground()
|
||||||
|
return super.onStartCommand(intent, flags, startId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startForeground() {
|
||||||
|
val noticicationIntent = Intent(this, MainActivity::class.java)
|
||||||
|
val pendingIntent = PendingIntent.getActivity(this, 0, noticicationIntent, 0)
|
||||||
|
val chan = NotificationChannel(
|
||||||
|
NOTIF_CHANNEL_ID,
|
||||||
|
"Tracker",
|
||||||
|
NotificationManager.IMPORTANCE_LOW)
|
||||||
|
chan.lightColor = Color.BLACK
|
||||||
|
chan.lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||||
|
val manager = (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
|
||||||
|
manager.createNotificationChannel(chan)
|
||||||
|
startForeground(NOTIF_ID, NotificationCompat.Builder(this, NOTIF_CHANNEL_ID)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setContentTitle("Livebeat")
|
||||||
|
.setContentText("Tracker is running")
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setCategory(Notification.CATEGORY_SERVICE)
|
||||||
|
.setPriority(NotificationManager.IMPORTANCE_LOW)
|
||||||
|
.setChannelId(NOTIF_CHANNEL_ID)
|
||||||
|
.setColorized(true)
|
||||||
|
.setColor(Color.BLACK)
|
||||||
|
.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val NOTIF_ID = 1
|
||||||
|
private const val NOTIF_CHANNEL_ID = "LiveTracker"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
android/app/src/main/res/drawable/round_clear_white_18dp.png
Normal file
BIN
android/app/src/main/res/drawable/round_clear_white_18dp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 221 B |
@@ -15,12 +15,127 @@
|
|||||||
android:id="@+id/toolbar"
|
android:id="@+id/toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?attr/actionBarSize"
|
android:layout_height="?attr/actionBarSize"
|
||||||
android:background="?attr/colorPrimary"
|
android:background="@color/black"
|
||||||
app:popupTheme="@style/Theme.Livebeat.PopupOverlay" />
|
app:popupTheme="@style/Theme.Livebeat.PopupOverlay" />
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
<include layout="@layout/content_main" />
|
<LinearLayout
|
||||||
|
android:id="@+id/linearLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/black2"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="50dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="30dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/httpText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center|center_horizontal|center_vertical"
|
||||||
|
android:text="Backend"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textColor="@color/white" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/httpStatus"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="20dp"
|
||||||
|
android:layout_weight="0"
|
||||||
|
android:fontFamily="sans-serif-black"
|
||||||
|
android:text="disconnected"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:textAllCaps="true"
|
||||||
|
android:textColor="@color/orange"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/rabbitText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal|center_vertical"
|
||||||
|
android:layout_weight="0"
|
||||||
|
android:text="RabbitMQ"
|
||||||
|
android:textColor="@color/white" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/rabbitStatus"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="20dp"
|
||||||
|
android:layout_weight="0"
|
||||||
|
android:fontFamily="sans-serif-black"
|
||||||
|
android:text="disconnected"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textAllCaps="true"
|
||||||
|
android:textColor="@color/orange"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="120dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginLeft="80dp"
|
||||||
|
android:layout_marginRight="80dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/username"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:backgroundTint="@color/white"
|
||||||
|
android:ems="10"
|
||||||
|
android:inputType="textPersonName"
|
||||||
|
android:text="admin"
|
||||||
|
android:textColor="@color/white" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/password"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:backgroundTint="@color/white"
|
||||||
|
android:ems="10"
|
||||||
|
android:hint="Secret password"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:text="$1KDaNCDlyXAOg"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textColorHint="@color/white" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/signin"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="Sign in" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
android:id="@+id/fab"
|
android:id="@+id/fab"
|
||||||
@@ -28,6 +143,8 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="bottom|end"
|
android:layout_gravity="bottom|end"
|
||||||
android:layout_margin="@dimen/fab_margin"
|
android:layout_margin="@dimen/fab_margin"
|
||||||
app:srcCompat="@android:drawable/ic_dialog_email" />
|
app:backgroundTint="@color/orange"
|
||||||
|
app:rippleColor="#FFFFFF"
|
||||||
|
app:srcCompat="@drawable/round_clear_white_18dp" />
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
|
||||||
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/nav_host_fragment"
|
|
||||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
app:defaultNavHost="true"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:navGraph="@navigation/nav_graph" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".FirstFragment">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/textview_first"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/hello_first_fragment"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/button_first"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/button_first"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/next"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/textview_first" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".SecondFragment">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/textview_second"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/button_second"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/button_second"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/previous"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/textview_second" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/nav_graph"
|
|
||||||
app:startDestination="@id/FirstFragment">
|
|
||||||
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/FirstFragment"
|
|
||||||
android:name="de.nicolasklier.livebeat.FirstFragment"
|
|
||||||
android:label="@string/first_fragment_label"
|
|
||||||
tools:layout="@layout/fragment_first">
|
|
||||||
|
|
||||||
<action
|
|
||||||
android:id="@+id/action_FirstFragment_to_SecondFragment"
|
|
||||||
app:destination="@id/SecondFragment" />
|
|
||||||
</fragment>
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/SecondFragment"
|
|
||||||
android:name="de.nicolasklier.livebeat.SecondFragment"
|
|
||||||
android:label="@string/second_fragment_label"
|
|
||||||
tools:layout="@layout/fragment_second">
|
|
||||||
|
|
||||||
<action
|
|
||||||
android:id="@+id/action_SecondFragment_to_FirstFragment"
|
|
||||||
app:destination="@id/FirstFragment" />
|
|
||||||
</fragment>
|
|
||||||
</navigation>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
<!-- Base application theme. -->
|
|
||||||
<style name="Theme.Livebeat" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
|
||||||
<!-- Primary brand color. -->
|
|
||||||
<item name="colorPrimary">@color/purple_200</item>
|
|
||||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
|
||||||
<item name="colorOnPrimary">@color/black</item>
|
|
||||||
<!-- Secondary brand color. -->
|
|
||||||
<item name="colorSecondary">@color/teal_200</item>
|
|
||||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
|
||||||
<item name="colorOnSecondary">@color/black</item>
|
|
||||||
<!-- Status bar color. -->
|
|
||||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
|
||||||
<!-- Customize your theme here. -->
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="purple_200">#FFBB86FC</color>
|
<color name="black">#0D0D0D</color>
|
||||||
<color name="purple_500">#FF6200EE</color>
|
<color name="black2">#1C1C1C</color>
|
||||||
<color name="purple_700">#FF3700B3</color>
|
<color name="green">#B4D9C4</color>
|
||||||
<color name="teal_200">#FF03DAC5</color>
|
<color name="orange">#F24B0F</color>
|
||||||
<color name="teal_700">#FF018786</color>
|
|
||||||
<color name="black">#FF000000</color>
|
|
||||||
<color name="white">#FFFFFFFF</color>
|
<color name="white">#FFFFFFFF</color>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="Theme.Livebeat" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
<style name="Theme.Livebeat" parent="Theme.AppCompat.Light">
|
||||||
<!-- Primary brand color. -->
|
<!-- Primary brand color. -->
|
||||||
<item name="colorPrimary">@color/purple_500</item>
|
<item name="colorPrimary">@color/black</item>
|
||||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
<item name="colorPrimaryVariant">@color/black2</item>
|
||||||
<item name="colorOnPrimary">@color/white</item>
|
<item name="colorOnPrimary">@color/white</item>
|
||||||
<!-- Secondary brand color. -->
|
<!-- Secondary brand color. -->
|
||||||
<item name="colorSecondary">@color/teal_200</item>
|
<item name="colorSecondary">@color/orange</item>
|
||||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
<item name="colorSecondaryVariant">@color/orange</item>
|
||||||
<item name="colorOnSecondary">@color/black</item>
|
<item name="colorOnSecondary">@color/white</item>
|
||||||
<!-- Status bar color. -->
|
<!-- Status bar color. -->
|
||||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
<item name="android:statusBarColor">@color/black2</item>
|
||||||
<!-- Customize your theme here. -->
|
<!-- Customize your theme here. -->
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -20,6 +20,5 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Theme.Livebeat.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
|
<style name="Theme.Livebeat.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
|
||||||
|
|
||||||
<style name="Theme.Livebeat.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
|
<style name="Theme.Livebeat.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
|
||||||
</resources>
|
</resources>
|
||||||
@@ -4,6 +4,7 @@ import { config as dconfig } from 'dotenv';
|
|||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
import * as figlet from 'figlet';
|
import * as figlet from 'figlet';
|
||||||
import * as mongoose from 'mongoose';
|
import * as mongoose from 'mongoose';
|
||||||
|
import * as cors from 'cors';
|
||||||
import { exit } from 'process';
|
import { exit } from 'process';
|
||||||
import * as winston from 'winston';
|
import * as winston from 'winston';
|
||||||
import { RabbitMQ } from './lib/rabbit';
|
import { RabbitMQ } from './lib/rabbit';
|
||||||
@@ -13,6 +14,8 @@ import { DeleteUser, GetUser, LoginUser, MW_User, PatchUser } from './endpoints/
|
|||||||
import { hashPassword, randomPepper, randomString } from './lib/crypto';
|
import { hashPassword, randomPepper, randomString } from './lib/crypto';
|
||||||
import { UserType } from './models/user/user.interface';
|
import { UserType } from './models/user/user.interface';
|
||||||
import { User } from './models/user/user.model';
|
import { User } from './models/user/user.model';
|
||||||
|
import { GetPhone, PostPhone } from './endpoints/phone';
|
||||||
|
import { GetBeat } from './endpoints/beat';
|
||||||
|
|
||||||
// Load .env
|
// Load .env
|
||||||
dconfig({ debug: true, encoding: 'UTF-8' });
|
dconfig({ debug: true, encoding: 'UTF-8' });
|
||||||
@@ -127,15 +130,29 @@ async function run() {
|
|||||||
*/
|
*/
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
app.use(cors());
|
||||||
app.use(bodyParser.json({ limit: '5kb' }));
|
app.use(bodyParser.json({ limit: '5kb' }));
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
res.on('finish', () => {
|
||||||
|
const done = Date.now();
|
||||||
|
logger.debug(`${req.method} - ${req.url} ${JSON.stringify(req.body)} -> ${res.statusCode}`);
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/', (req, res) => res.status(200).send('OK'));
|
||||||
app.get('/user/', MW_User, (req, res) => GetUser(req, res));
|
app.get('/user/', MW_User, (req, res) => GetUser(req, res));
|
||||||
app.get('/user/:id', MW_User, (req, res) => GetUser(req, res));
|
app.get('/user/:id', MW_User, (req, res) => GetUser(req, res));
|
||||||
app.patch('/user/:id', MW_User, (req, res) => PatchUser(req, res));
|
app.patch('/user/:id', MW_User, (req, res) => PatchUser(req, res));
|
||||||
app.delete('/user/:id', MW_User, (req, res) => DeleteUser(req, res));
|
app.delete('/user/:id', MW_User, (req, res) => DeleteUser(req, res));
|
||||||
|
|
||||||
app.post('/user/login', (req, res) => LoginUser(req, res));
|
app.post('/user/login', (req, res) => LoginUser(req, res));
|
||||||
|
|
||||||
|
app.get('/phone/:id', (req, res) => GetPhone(req, res));
|
||||||
|
app.post('/phone', MW_User, (req, res) => PostPhone(req, res));
|
||||||
|
|
||||||
|
app.get('/beat/', MW_User, (req, res) => GetBeat(req, res));
|
||||||
|
|
||||||
app.listen(config.http.port, config.http.host, () => {
|
app.listen(config.http.port, config.http.host, () => {
|
||||||
logger.info(`HTTP server is running at ${config.http.host}:${config.http.port}`);
|
logger.info(`HTTP server is running at ${config.http.host}:${config.http.port}`);
|
||||||
});
|
});
|
||||||
|
|||||||
30
backend/endpoints/beat.ts
Normal file
30
backend/endpoints/beat.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Response } from "express";
|
||||||
|
import { logger } from "../app";
|
||||||
|
import { LivebeatRequest } from "../lib/request";
|
||||||
|
import { Beat } from "../models/beat/beat.model.";
|
||||||
|
import { Phone } from "../models/phone/phone.model";
|
||||||
|
|
||||||
|
export interface IFilter {
|
||||||
|
phone: string,
|
||||||
|
time: {
|
||||||
|
from: number,
|
||||||
|
to: number
|
||||||
|
},
|
||||||
|
max: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GetBeat(req: LivebeatRequest, res: Response) {
|
||||||
|
const filter: IFilter = req.body.filter as IFilter;
|
||||||
|
|
||||||
|
// If no filters are specified, we return the last 500 points. We take the first phone as default.
|
||||||
|
if (filter === undefined) {
|
||||||
|
const phone = await Phone.findOne({ user: req.user?._id });
|
||||||
|
logger.debug(`No filters were provided! Take ${phone?.displayName} as default.`);
|
||||||
|
|
||||||
|
if (phone !== undefined && phone !== null) {
|
||||||
|
logger.debug("Query for latest beats ...");
|
||||||
|
const beats = await Beat.find({ phone: phone._id }).limit(800).sort({ _id: -1 });
|
||||||
|
res.status(200).send(beats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
backend/endpoints/phone.ts
Normal file
63
backend/endpoints/phone.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { Response } from "express";
|
||||||
|
import { logger } from "../app";
|
||||||
|
import { LivebeatRequest } from "../lib/request";
|
||||||
|
import { Phone } from "../models/phone/phone.model";
|
||||||
|
|
||||||
|
export async function GetPhone(req: LivebeatRequest, res: Response) {
|
||||||
|
const phoneId: String = req.params['id'];
|
||||||
|
|
||||||
|
if (phoneId === undefined) {
|
||||||
|
res.status(400).send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check database for phone
|
||||||
|
const phone = await Phone.findOne({ androidId: phoneId, user: req.user?._id });
|
||||||
|
if (phone === undefined) {
|
||||||
|
res.status(404).send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send(phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PostPhone(req: LivebeatRequest, res: Response) {
|
||||||
|
const androidId: String = req.body.androidId;
|
||||||
|
const modelName: String = req.body.modelName;
|
||||||
|
const displayName: String = req.body.displayName;
|
||||||
|
const operatingSystem: String = req.body.operatingSystem;
|
||||||
|
const architecture: String = req.body.architecture;
|
||||||
|
|
||||||
|
if (androidId === undefined ||
|
||||||
|
modelName === undefined ||
|
||||||
|
displayName === undefined ||
|
||||||
|
operatingSystem === undefined ||
|
||||||
|
architecture === undefined) {
|
||||||
|
logger.debug("Request to /phone failed because of missing parameters.");
|
||||||
|
res.status(400).send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if phone already exists
|
||||||
|
const phone = await Phone.findOne({ androidId, user: req.user?._id });
|
||||||
|
|
||||||
|
if (phone !== null) {
|
||||||
|
logger.debug("Request to /phone failed because phone already exists.");
|
||||||
|
res.status(409).send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create phone
|
||||||
|
await Phone.create({
|
||||||
|
androidId,
|
||||||
|
displayName,
|
||||||
|
modelName,
|
||||||
|
operatingSystem,
|
||||||
|
architecture,
|
||||||
|
user: req.user?._id
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`New device (${displayName}) registered for ${req.user?.name}.`)
|
||||||
|
|
||||||
|
res.status(200).send();
|
||||||
|
}
|
||||||
@@ -3,7 +3,8 @@ import { verifyPassword } from "../lib/crypto";
|
|||||||
import { User } from "../models/user/user.model";
|
import { User } from "../models/user/user.model";
|
||||||
import { sign, decode, verify } from 'jsonwebtoken';
|
import { sign, decode, verify } from 'jsonwebtoken';
|
||||||
import { JWT_SECRET, logger } from "../app";
|
import { JWT_SECRET, logger } from "../app";
|
||||||
import { IUser } from "../models/user/user.interface";
|
import { LivebeatRequest } from '../lib/request';
|
||||||
|
import { SchemaTypes } from "mongoose";
|
||||||
|
|
||||||
export async function GetUser(req: Request, res: Response) {
|
export async function GetUser(req: Request, res: Response) {
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ export async function LoginUser(req: Request, res: Response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We're good. Create JWT token.
|
// We're good. Create JWT token.
|
||||||
const token = sign({ user: user._id }, JWT_SECRET!, { notBefore: Date.now(), expiresIn: '30d' });
|
const token = sign({ user: user._id }, JWT_SECRET, { expiresIn: '30d' });
|
||||||
|
|
||||||
logger.info(`User ${user.name} logged in.`)
|
logger.info(`User ${user.name} logged in.`)
|
||||||
res.status(200).send({ token });
|
res.status(200).send({ token });
|
||||||
@@ -58,29 +59,34 @@ export async function LoginUser(req: Request, res: Response) {
|
|||||||
* This middleware validates any tokens that are required to access most of the endpoints.
|
* This middleware validates any tokens that are required to access most of the endpoints.
|
||||||
* Note: This validation doesn't contain any permission checking.
|
* Note: This validation doesn't contain any permission checking.
|
||||||
*/
|
*/
|
||||||
export async function MW_User(req: Request, res: Response, next: () => void) {
|
export async function MW_User(req: LivebeatRequest, res: Response, next: () => void) {
|
||||||
if (req.headers.token === undefined) {
|
if (req.headers.token === undefined) {
|
||||||
res.status(401).send();
|
res.status(401).send({ message: "Token not specified" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const token = req.headers.token.toString();
|
const token = req.headers.token.toString();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Verify token
|
// Verify token
|
||||||
if(await verify(token, JWT_SECRET!, { algorithms: ['HS256'] })) {
|
if(await verify(token, JWT_SECRET, { algorithms: ['HS256'] })) {
|
||||||
// Token is valid, now look if user is in db (in case he got deleted)
|
// Token is valid, now look if user is in db (in case he got deleted)
|
||||||
const id: number = Number(decode(token, { json: true })!.id);
|
const id = decode(token, { json: true })!.user;
|
||||||
const db = await User.findOne({ where: { id } });
|
const db = await User.findById(id);
|
||||||
if (db !== undefined) {
|
|
||||||
|
if (db !== undefined && db !== null) {
|
||||||
|
req.user = db
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
res.status(401).send();
|
res.status(401).send({ message: "Token is not valid" });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res.status(401).send();
|
res.status(401).send({ message: "Token is not valid" });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err) res.status(401).send();
|
if (err) {
|
||||||
|
res.status(500).send({ message: "We failed validating your token for some reason." });
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,14 @@
|
|||||||
import * as amqp from 'amqplib';
|
import * as amqp from 'amqplib';
|
||||||
import { logger, RABBITMQ_URI } from '../app';
|
import { logger, RABBITMQ_URI } from '../app';
|
||||||
|
import { Beat } from '../models/beat/beat.model.';
|
||||||
|
import { Phone } from '../models/phone/phone.model';
|
||||||
|
|
||||||
|
interface IBeat {
|
||||||
|
token: string,
|
||||||
|
gpsLocation: Array<number>,
|
||||||
|
battery: number,
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
export class RabbitMQ {
|
export class RabbitMQ {
|
||||||
connection: amqp.Connection | null = null;
|
connection: amqp.Connection | null = null;
|
||||||
@@ -9,12 +18,32 @@ export class RabbitMQ {
|
|||||||
this.connection = await amqp.connect(RABBITMQ_URI);
|
this.connection = await amqp.connect(RABBITMQ_URI);
|
||||||
this.channel = await this.connection.createChannel();
|
this.channel = await this.connection.createChannel();
|
||||||
|
|
||||||
this.channel.consume('Tracker', (msg) => {
|
this.channel.consume('tracker', async (income) => {
|
||||||
logger.debug("Received from broker: " + msg?.content.toString());
|
if (income === undefined || income === null) return;
|
||||||
}, { noAck: false });
|
|
||||||
|
const msg: IBeat = JSON.parse(income.content.toString()) as IBeat
|
||||||
|
|
||||||
|
// Get phone
|
||||||
|
const phone = await Phone.findOne({ androidId: msg.token });
|
||||||
|
if (phone == undefined) {
|
||||||
|
logger.info(`Received beat from unknown device with id ${msg.token}`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async publish(queueName = 'Tracker', data: any) {
|
logger.info(`New beat from ${phone.displayName} with ${msg.gpsLocation[2]} accuracy and ${msg.battery}% battery`)
|
||||||
|
|
||||||
|
Beat.create({
|
||||||
|
phone: phone._id,
|
||||||
|
coordinate: [msg.gpsLocation[0], msg.gpsLocation[1]],
|
||||||
|
accuracy: msg.gpsLocation[2],
|
||||||
|
speed: msg.gpsLocation[3],
|
||||||
|
battery: msg.battery,
|
||||||
|
createdAt: msg.timestamp
|
||||||
|
});
|
||||||
|
}, { noAck: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async publish(queueName = 'tracker', data: any) {
|
||||||
if (this.connection == undefined) await this.init()
|
if (this.connection == undefined) await this.init()
|
||||||
this.channel?.sendToQueue(queueName, Buffer.from(data));
|
this.channel?.sendToQueue(queueName, Buffer.from(data));
|
||||||
}
|
}
|
||||||
|
|||||||
6
backend/lib/request.ts
Normal file
6
backend/lib/request.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { Request } from "express";
|
||||||
|
import { IUser } from "../models/user/user.interface";
|
||||||
|
|
||||||
|
export interface LivebeatRequest extends Request {
|
||||||
|
user?: IUser
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Document } from 'mongoose';
|
import { Document } from 'mongoose';
|
||||||
import { IPhone } from '../phone/phone.interface';
|
import { IPhone } from '../phone/phone.interface';
|
||||||
|
|
||||||
export interface ITrack extends Document {
|
export interface IBeat extends Document {
|
||||||
coordinate?: number[],
|
coordinate?: number[],
|
||||||
velocity?: number,
|
accuracy: number,
|
||||||
|
speed: number,
|
||||||
battery?: number,
|
battery?: number,
|
||||||
magneticField?: number,
|
|
||||||
phone: IPhone,
|
phone: IPhone,
|
||||||
createdAt?: Date
|
createdAt?: Date
|
||||||
}
|
}
|
||||||
6
backend/models/beat/beat.model..ts
Normal file
6
backend/models/beat/beat.model..ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { Model, model } from 'mongoose';
|
||||||
|
import { IBeat } from './beat.interface';
|
||||||
|
import { schemaBeat } from './beat.schema';
|
||||||
|
|
||||||
|
const modelBeat: Model<IBeat> = model<IBeat>('Beat', schemaBeat, 'Beat');
|
||||||
|
export { modelBeat as Beat };
|
||||||
16
backend/models/beat/beat.schema.ts
Normal file
16
backend/models/beat/beat.schema.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Schema, SchemaTypes } from 'mongoose';
|
||||||
|
import { Phone } from '../phone/phone.model';
|
||||||
|
|
||||||
|
const schemaBeat = new Schema({
|
||||||
|
coordinate: { type: [Number], required: false },
|
||||||
|
accuracy: { type: Number, required: false },
|
||||||
|
speed: { type: Number, required: false },
|
||||||
|
battery: { type: Number, required: false },
|
||||||
|
phone: { type: SchemaTypes.ObjectId, required: true, default: 'user' }
|
||||||
|
}, {
|
||||||
|
timestamps: {
|
||||||
|
createdAt: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export { schemaBeat };
|
||||||
@@ -2,6 +2,7 @@ import { Document } from 'mongoose';
|
|||||||
import { IUser } from '../user/user.interface';
|
import { IUser } from '../user/user.interface';
|
||||||
|
|
||||||
export interface IPhone extends Document {
|
export interface IPhone extends Document {
|
||||||
|
androidId: String,
|
||||||
displayName: String,
|
displayName: String,
|
||||||
modelName: String,
|
modelName: String,
|
||||||
operatingSystem: String,
|
operatingSystem: String,
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Schema } from 'mongoose';
|
import { Mongoose, Schema, SchemaType, SchemaTypes } from 'mongoose';
|
||||||
import { User } from '../user/user.model';
|
import { User } from '../user/user.model';
|
||||||
|
|
||||||
const schemaPhone = new Schema({
|
const schemaPhone = new Schema({
|
||||||
|
androidId: { type: String, required: true },
|
||||||
displayName: { type: String, required: true },
|
displayName: { type: String, required: true },
|
||||||
modelName: { type: String, required: false },
|
modelName: { type: String, required: false },
|
||||||
operatingSystem: { type: String, required: false },
|
operatingSystem: { type: String, required: false },
|
||||||
architecture: { type: String, required: false },
|
architecture: { type: String, required: false },
|
||||||
user: { type: User, required: true }
|
user: { type: SchemaTypes.ObjectId, required: true }
|
||||||
}, {
|
}, {
|
||||||
timestamps: {
|
timestamps: {
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { Model, model } from 'mongoose';
|
|
||||||
import { ITrack } from './track.interface';
|
|
||||||
import { schemaTrack } from './track.schema';
|
|
||||||
|
|
||||||
const modelTrack: Model<ITrack> = model<ITrack>('Track', schemaTrack, 'Track');
|
|
||||||
export { modelTrack as Phone };
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { Schema } from 'mongoose';
|
|
||||||
import { Phone } from '../phone/phone.model';
|
|
||||||
|
|
||||||
const schemaTrack = new Schema({
|
|
||||||
coordinate: { type: [Number], required: false },
|
|
||||||
velocity: { type: Number, required: false },
|
|
||||||
battery: { type: Number, required: false },
|
|
||||||
magneticField: { type: Number, required: false },
|
|
||||||
phone: { type: Phone, required: true, default: 'user' }
|
|
||||||
}, {
|
|
||||||
timestamps: {
|
|
||||||
createdAt: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export { schemaTrack };
|
|
||||||
18
backend/package-lock.json
generated
18
backend/package-lock.json
generated
@@ -96,6 +96,15 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/cors": {
|
||||||
|
"version": "2.8.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.8.tgz",
|
||||||
|
"integrity": "sha512-fO3gf3DxU2Trcbr75O7obVndW/X5k8rJNZkLXlQWStTHhP71PkRqjwPIEI0yMnJdg9R9OasjU+Bsr+Hr1xy/0w==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/express": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/dotenv": {
|
"@types/dotenv": {
|
||||||
"version": "8.2.0",
|
"version": "8.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-8.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-8.2.0.tgz",
|
||||||
@@ -868,6 +877,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||||
},
|
},
|
||||||
|
"cors": {
|
||||||
|
"version": "2.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||||
|
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
||||||
|
"requires": {
|
||||||
|
"object-assign": "^4",
|
||||||
|
"vary": "^1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"crypto-random-string": {
|
"crypto-random-string": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"argon2": "^0.27.0",
|
"argon2": "^0.27.0",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"figlet": "^1.5.0",
|
"figlet": "^1.5.0",
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
"concurrently": "^5.3.0",
|
"concurrently": "^5.3.0",
|
||||||
"nodemon": "^2.0.5",
|
"nodemon": "^2.0.5",
|
||||||
"@types/jsonwebtoken": "8.5.0",
|
"@types/jsonwebtoken": "8.5.0",
|
||||||
"@types/amqplib": "0.5.14"
|
"@types/amqplib": "0.5.14",
|
||||||
|
"@types/cors": "2.8.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,9 @@
|
|||||||
"src/assets"
|
"src/assets"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.scss"
|
"src/styles.scss",
|
||||||
|
"./node_modules/mapbox-gl/dist/mapbox-gl.css",
|
||||||
|
"./node_modules/@mapbox/mapbox-gl-geocoder/lib/mapbox-gl-geocoder.css"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": []
|
||||||
},
|
},
|
||||||
|
|||||||
991
frontend/package-lock.json
generated
991
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,11 @@
|
|||||||
"@nebular/auth": "^6.2.1",
|
"@nebular/auth": "^6.2.1",
|
||||||
"@nebular/eva-icons": "^6.2.1",
|
"@nebular/eva-icons": "^6.2.1",
|
||||||
"@nebular/theme": "^6.2.1",
|
"@nebular/theme": "^6.2.1",
|
||||||
|
"@types/mapbox-gl": "^1.12.5",
|
||||||
"eva-icons": "^1.1.3",
|
"eva-icons": "^1.1.3",
|
||||||
|
"geojson": "^0.5.0",
|
||||||
|
"mapbox-gl": "^1.12.0",
|
||||||
|
"ngx-mapbox-gl": "^4.8.1",
|
||||||
"rxjs": "~6.6.0",
|
"rxjs": "~6.6.0",
|
||||||
"tslib": "^2.0.0",
|
"tslib": "^2.0.0",
|
||||||
"zone.js": "~0.10.2"
|
"zone.js": "~0.10.2"
|
||||||
|
|||||||
16
frontend/src/app/api.service.spec.ts
Normal file
16
frontend/src/app/api.service.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { APIService } from './api.service';
|
||||||
|
|
||||||
|
describe('APIService', () => {
|
||||||
|
let service: APIService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(APIService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
62
frontend/src/app/api.service.ts
Normal file
62
frontend/src/app/api.service.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
export interface ILogin {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBeat {
|
||||||
|
coordinate?: number[];
|
||||||
|
accuracy: number;
|
||||||
|
speed: number;
|
||||||
|
battery?: number;
|
||||||
|
phone: any;
|
||||||
|
createdAt?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class APIService {
|
||||||
|
|
||||||
|
private token: string;
|
||||||
|
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
API_ENDPOINT = 'http://192.168.178.26:8040'
|
||||||
|
|
||||||
|
constructor(private httpClient: HttpClient) { }
|
||||||
|
|
||||||
|
async login(username: string, password: string): Promise<ILogin> {
|
||||||
|
return new Promise<ILogin>((resolve, reject) => {
|
||||||
|
console.log('POST');
|
||||||
|
|
||||||
|
this.httpClient.post(this.API_ENDPOINT + '/user/login', { username, password }, { responseType: 'json' })
|
||||||
|
.subscribe(token => {
|
||||||
|
console.log(token);
|
||||||
|
|
||||||
|
this.token = (token as ILogin).token;
|
||||||
|
this.username = username;
|
||||||
|
resolve(token as ILogin);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBeats(): Promise<IBeat[]> {
|
||||||
|
return new Promise<IBeat[]>((resolve, reject) => {
|
||||||
|
if (this.token === undefined) { reject([]); }
|
||||||
|
|
||||||
|
const headers = new HttpHeaders({ token: this.token });
|
||||||
|
|
||||||
|
this.httpClient.get(this.API_ENDPOINT + '/beat', { responseType: 'json', headers })
|
||||||
|
.subscribe(beats => {
|
||||||
|
console.log(beats);
|
||||||
|
resolve(beats as IBeat[]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSession(): boolean {
|
||||||
|
return this.token !== undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,22 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { Routes, RouterModule } from '@angular/router';
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
import { NbAuthComponent, NbLoginComponent } from '@nebular/auth';
|
import { NbAuthComponent, NbLoginComponent } from '@nebular/auth';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||||
|
import { LoginComponent } from './login/login.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: NbAuthComponent,
|
component: AppComponent
|
||||||
children: [
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: 'login',
|
||||||
component: NbLoginComponent
|
component: LoginComponent
|
||||||
}
|
},
|
||||||
]
|
{
|
||||||
|
path: 'dashboard',
|
||||||
|
component: DashboardComponent
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
<nb-auth-block>
|
<div id="header">
|
||||||
<nb-login></nb-login>
|
<p>Header</p>
|
||||||
</nb-auth-block>
|
<div class="left">
|
||||||
|
<span class="ident" *ngIf="this.api.hasSession()">Logged in as {{this.api.username}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
|
||||||
|
<!-- Display start page -->
|
||||||
|
<div id="startpage" *ngIf="false">
|
||||||
|
<h1>Livebeat</h1>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#header {
|
||||||
|
width: 100vw;
|
||||||
|
height: fit-content;
|
||||||
|
padding-top: 0.4rem;
|
||||||
|
padding-bottom: 0.4rem;
|
||||||
|
background-color: #1d1d1dd9;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
box-shadow: 10px 10px 50px 0px rgba(0,0,0,0.85);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { APIService } from './api.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@@ -7,4 +9,7 @@ import { Component } from '@angular/core';
|
|||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
title = 'Livebeat';
|
title = 'Livebeat';
|
||||||
|
|
||||||
|
constructor(public api: APIService, private router: Router) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,26 @@ import { NbCardModule, NbLayoutModule, NbSidebarModule, NbThemeModule } from '@n
|
|||||||
|
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
|
import { LoginComponent } from './login/login.component';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||||
|
import { NgxMapboxGLModule } from 'ngx-mapbox-gl';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent
|
AppComponent,
|
||||||
|
LoginComponent,
|
||||||
|
DashboardComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
|
FormsModule,
|
||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
|
NgxMapboxGLModule.withConfig({
|
||||||
|
accessToken: 'pk.eyJ1IjoibW9uZGVpMSIsImEiOiJja2dsY2ZtaG0xZ2o5MnR0ZWs0Mm82OTBpIn0.NzDWN3P6jJLmci_v3MM1tA'
|
||||||
|
}),
|
||||||
NbThemeModule.forRoot({ name: 'dark' }),
|
NbThemeModule.forRoot({ name: 'dark' }),
|
||||||
NbLayoutModule,
|
NbLayoutModule,
|
||||||
NbEvaIconsModule,
|
NbEvaIconsModule,
|
||||||
|
|||||||
13
frontend/src/app/dashboard/dashboard.component.html
Normal file
13
frontend/src/app/dashboard/dashboard.component.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<mgl-map [style]="'mapbox://styles/mapbox/outdoors-v11'">
|
||||||
|
<mgl-geojson-source id="locHistory" [data]="data"></mgl-geojson-source>
|
||||||
|
<mgl-layer
|
||||||
|
id="locHisotryLines"
|
||||||
|
type="line"
|
||||||
|
source="locHistory"
|
||||||
|
[paint]="{
|
||||||
|
'line-color': '#ff0000',
|
||||||
|
'line-width': 4
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
</mgl-layer>
|
||||||
|
</mgl-map>
|
||||||
8
frontend/src/app/dashboard/dashboard.component.scss
Normal file
8
frontend/src/app/dashboard/dashboard.component.scss
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
mgl-map {
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
25
frontend/src/app/dashboard/dashboard.component.spec.ts
Normal file
25
frontend/src/app/dashboard/dashboard.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DashboardComponent } from './dashboard.component';
|
||||||
|
|
||||||
|
describe('DashboardComponent', () => {
|
||||||
|
let component: DashboardComponent;
|
||||||
|
let fixture: ComponentFixture<DashboardComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ DashboardComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DashboardComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
32
frontend/src/app/dashboard/dashboard.component.ts
Normal file
32
frontend/src/app/dashboard/dashboard.component.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { AfterViewInit, Component, OnInit } from '@angular/core';
|
||||||
|
import { Map } from 'mapbox-gl';
|
||||||
|
import { APIService } from '../api.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-dashboard',
|
||||||
|
templateUrl: './dashboard.component.html',
|
||||||
|
styleUrls: ['./dashboard.component.scss']
|
||||||
|
})
|
||||||
|
export class DashboardComponent implements AfterViewInit {
|
||||||
|
map: Map;
|
||||||
|
|
||||||
|
data: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
|
||||||
|
type: 'FeatureCollection', features: [
|
||||||
|
{
|
||||||
|
type: 'Feature',
|
||||||
|
properties: null,
|
||||||
|
geometry: { type: 'LineString', coordinates: [] }
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(private api: APIService) { }
|
||||||
|
|
||||||
|
async ngAfterViewInit(): Promise<void> {
|
||||||
|
const beats = await this.api.getBeats();
|
||||||
|
beats.forEach((beat) => {
|
||||||
|
this.data.features[0].geometry.coordinates.push([beat.coordinate[1], beat.coordinate[0]]);
|
||||||
|
});
|
||||||
|
console.log("Now:", this.data.features);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
16
frontend/src/app/login/login.component.html
Normal file
16
frontend/src/app/login/login.component.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<div id="login">
|
||||||
|
<h2>Login</h2>
|
||||||
|
<p>Please authenticate yourself to proceed.</p>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<div id="username">
|
||||||
|
<label>Username</label><br>
|
||||||
|
<input type="text" name="username" placeholder="Username" [(ngModel)]="username">
|
||||||
|
</div>
|
||||||
|
<div id="password">
|
||||||
|
<label>Password</label><br>
|
||||||
|
<input type="password" name="password" placeholder="Password" [(ngModel)]="password">
|
||||||
|
</div>
|
||||||
|
<button (click)="perfomLogin()">Login</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
13
frontend/src/app/login/login.component.scss
Normal file
13
frontend/src/app/login/login.component.scss
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#login {
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: block;
|
||||||
|
height: fit-content;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 5rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: #1d1d1d;
|
||||||
|
box-shadow: 20px 20px 60px #191919,
|
||||||
|
-20px -20px 60px #212121;
|
||||||
|
|
||||||
|
}
|
||||||
25
frontend/src/app/login/login.component.spec.ts
Normal file
25
frontend/src/app/login/login.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LoginComponent } from './login.component';
|
||||||
|
|
||||||
|
describe('LoginComponent', () => {
|
||||||
|
let component: LoginComponent;
|
||||||
|
let fixture: ComponentFixture<LoginComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ LoginComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(LoginComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
31
frontend/src/app/login/login.component.ts
Normal file
31
frontend/src/app/login/login.component.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { APIService } from '../api.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-login',
|
||||||
|
templateUrl: './login.component.html',
|
||||||
|
styleUrls: ['./login.component.scss']
|
||||||
|
})
|
||||||
|
export class LoginComponent implements OnInit {
|
||||||
|
|
||||||
|
username = '';
|
||||||
|
password = '';
|
||||||
|
|
||||||
|
constructor(private api: APIService, private router: Router) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
async perfomLogin(): Promise<any> {
|
||||||
|
console.log('Clicked!');
|
||||||
|
|
||||||
|
if ((await this.api.login(this.username, this.password)).token !== undefined) {
|
||||||
|
console.log('Login was successful!');
|
||||||
|
this.router.navigate(['dashboard']);
|
||||||
|
} else {
|
||||||
|
console.log('Login was not successful!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -61,3 +61,4 @@ import 'zone.js/dist/zone'; // Included with Angular CLI.
|
|||||||
/***************************************************************************************************
|
/***************************************************************************************************
|
||||||
* APPLICATION IMPORTS
|
* APPLICATION IMPORTS
|
||||||
*/
|
*/
|
||||||
|
(window as any).global = window;
|
||||||
|
|||||||
@@ -2,9 +2,18 @@
|
|||||||
|
|
||||||
@import '~@nebular/auth/styles/globals';
|
@import '~@nebular/auth/styles/globals';
|
||||||
@import '~@nebular/theme/styles/globals';
|
@import '~@nebular/theme/styles/globals';
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;700;900&display=swap');
|
||||||
|
|
||||||
@include nb-install() {
|
@include nb-install() {
|
||||||
@include nb-theme-global();
|
|
||||||
@include nb-auth-global();
|
@include nb-auth-global();
|
||||||
|
@include nb-theme-global();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #1d1d1d;
|
||||||
|
color: #fff;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
/* You can add global styles to this file, and also import other style files */
|
/* You can add global styles to this file, and also import other style files */
|
||||||
|
|||||||
Reference in New Issue
Block a user