Compare commits

...

4 Commits

  1. 43
      app/build.gradle
  2. 14
      app/src/androidTest/java/co/zzyzx/sensorlogger/DatabaseTest.kt
  3. 9
      app/src/androidTest/java/co/zzyzx/sensorlogger/ExampleInstrumentedTest.kt
  4. 39
      app/src/main/AndroidManifest.xml
  5. 30
      app/src/main/java/co/zzyzx/sensorlogger/ActivityTransitionIntentService.kt
  6. 109
      app/src/main/java/co/zzyzx/sensorlogger/EndlessService.kt
  7. 21
      app/src/main/java/co/zzyzx/sensorlogger/MainActivity.kt
  8. 18
      app/src/main/java/co/zzyzx/sensorlogger/db/Database.kt
  9. 11
      app/src/main/java/co/zzyzx/sensorlogger/db/dao.kt
  10. 8
      app/src/main/java/co/zzyzx/sensorlogger/db/entity.kt

43
app/build.gradle

@ -12,7 +12,11 @@ android {
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// The following argument makes the Android Test Orchestrator run its
// "pm clear" command after each test invocation. This command ensures
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
}
buildTypes {
release {
@ -20,29 +24,44 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
// Gradle automatically adds 'android.test.runner' as a dependency.
// useLibrary 'android.test.runner'
// useLibrary 'android.test.base'
// useLibrary 'android.test.mock'
//
// testOptions {
// execution 'ANDROIDX_TEST_ORCHESTRATOR'
// }
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'androidx.appcompat:appcompat:1.1.0'
// implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'com.github.kittinunf.fuel:fuel:2.1.0'
implementation 'com.github.kittinunf.fuel:fuel-android:2.1.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0-M2'
// Room dependencies
// implementation 'androidx.room:room-runtime:2.0.0'
// kapt 'androidx.room:room-compiler:2.0.0'
implementation 'android.arch.persistence.room:runtime:1.1.1'
annotationProcessor 'android.arch.persistence.room:compiler:1.1.1'
kapt 'android.arch.persistence.room:compiler:1.1.1'
implementation 'androidx.room:room-runtime:2.1.0'
kapt 'androidx.room:room-compiler:2.1.0'
// activity recognition
implementation 'com.google.android.gms:play-services-location:17.0.0'
implementation 'com.opencsv:opencsv:4.0'
// implementation 'android.arch.lifecycle:excompile 'com.opencsv:opencsv:4.0'tensions:1.1.1'
// kapt 'android.arch.lifecycle:compiler:1.1.1'
// Required -- JUnit 4 framework
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
// Core library
androidTestImplementation 'androidx.test:core:1.2.0'
// AndroidJUnitRunner and JUnit Rules
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
// Assertions
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
}

14
app/src/androidTest/java/co/zzyzx/sensorlogger/DatabaseTest.kt

@ -1,9 +1,9 @@
package co.zzyzx.sensorlogger
import android.arch.persistence.room.Room
import android.support.test.InstrumentationRegistry
import android.support.test.runner.AndroidJUnit4
import androidx.room.Room
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import co.zzyzx.sensorlogger.db.AppDatabase
import co.zzyzx.sensorlogger.db.Record
import co.zzyzx.sensorlogger.db.RecordDao
@ -23,7 +23,7 @@ class DatabaseTest {
@Before
fun createDb() {
val appContext = InstrumentationRegistry.getTargetContext()
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
db = Room.inMemoryDatabaseBuilder(appContext, AppDatabase::class.java)
.allowMainThreadQueries()
.build()
@ -41,9 +41,7 @@ class DatabaseTest {
data = "123,23232"
)
)
for (rec in records) {
recordDao.insertAll(rec)
}
recordDao.insert(*records.toTypedArray())
}
@After
@ -61,7 +59,7 @@ class DatabaseTest {
sensor = "gps",
data = "333,32322"
)
recordDao.insertAll(rec)
recordDao.insert(rec)
val byName = recordDao.findBySensor("gps")
assertEquals(byName.data, rec.data)
assertEquals(byName.timestamp, rec.timestamp)

9
app/src/androidTest/java/co/zzyzx/sensorlogger/ExampleInstrumentedTest.kt

@ -1,13 +1,12 @@
package co.zzyzx.sensorlogger
import android.support.test.InstrumentationRegistry
import android.support.test.runner.AndroidJUnit4
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
@ -18,7 +17,7 @@ class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getTargetContext()
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("co.zzyzx.sensorlogger", appContext.packageName)
}
}

39
app/src/main/AndroidManifest.xml

@ -1,12 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="co.zzyzx.sensorlogger">
package="co.zzyzx.sensorlogger">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="com.google.android.gms.permission.ACTIVITY_RECOGNITION" />
<application
@ -20,21 +23,37 @@
<service
android:name=".EndlessService"
android:enabled="true"
android:exported="false">
</service>
android:exported="false"></service>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver android:enabled="true" android:name=".StartReceiver">
<receiver
android:name=".StartReceiver"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<!-- You don't need to include android:required="false" if your app's
minSdkVersion is 28 or higher. -->
<uses-library
android:name="android.test.runner"
android:required="false" />
<!-- For both of these declarations, you don't need to include
android:required="false" if your app's minSdkVersion is 28
or higher. -->
<uses-library
android:name="android.test.base"
android:required="false" />
<uses-library
android:name="android.test.mock"
android:required="false" />
</application>

30
app/src/main/java/co/zzyzx/sensorlogger/ActivityTransitionIntentService.kt

@ -0,0 +1,30 @@
package co.zzyzx.sensorlogger
import android.app.IntentService
import android.content.Intent
import android.preference.PreferenceManager
import android.widget.Toast
import com.google.android.gms.location.ActivityTransitionResult
private const val TAG = "ActivityTransitionIS"
class ActivityTransitionIntentService : IntentService(TAG) {
override fun onHandleIntent(intent: Intent) {
val result = ActivityTransitionResult.extractResult(intent) as ActivityTransitionResult
val events = result.transitionEvents
var txt = ArrayList<String>(0)
for (evt in events) {
txt.add("${evt.activityType} - ${evt.transitionType}")
}
log("ACT_TRANSITION: ${events.joinToString(",")}")
Toast.makeText(applicationContext, events.joinToString(","), Toast.LENGTH_SHORT)
PreferenceManager.getDefaultSharedPreferences(this).edit()
.putString("ACTIVITY", txt.joinToString("|"))
.apply()
}
}

109
app/src/main/java/co/zzyzx/sensorlogger/EndlessService.kt

@ -4,7 +4,6 @@ import android.app.*
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
@ -18,14 +17,14 @@ import android.os.IBinder
import android.os.PowerManager
import android.provider.Settings
import android.widget.Toast
import co.zzyzx.sensorlogger.db.Record
import co.zzyzx.sensorlogger.db.RecordRepository
import com.github.kittinunf.fuel.Fuel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import com.google.android.gms.location.*
import java.text.SimpleDateFormat
import java.time.Instant
import java.util.*
import kotlin.collections.ArrayList
fun Double.format(digits: Int) = java.lang.String.format("%.${digits}f", this)
@ -40,12 +39,24 @@ class EndlessService : Service(), SensorEventListener, LocationListener {
private val notificationChannelId = "ENDLESS SERVICE CHANNEL"
private val notificationId = 1011
private lateinit var mActivityRecognitionClient: ActivityRecognitionClient
private lateinit var mSensorManager: SensorManager
private lateinit var mAccelerometer: Sensor
private lateinit var mGyroscope: Sensor
private var mLocationManager: LocationManager? = null // need to do this because of permission
private lateinit var recRepo: RecordRepository
private var dataTemp = ArrayList<Record>(0)
fun addNewRecord(sensor: String, text: String) {
if (dataTemp.size < 2000) {
dataTemp.add(Record(Instant.now().toEpochMilli(), sensor, text))
} else {
val toSave = dataTemp.clone() as List<Record>
dataTemp.clear()
recRepo.addBulkRecord(toSave)
}
}
override fun onBind(intent: Intent): IBinder? {
log("Some component want to bind with the service")
@ -68,10 +79,8 @@ class EndlessService : Service(), SensorEventListener, LocationListener {
"with a null intent. It has been probably restarted by the system."
)
}
mSensorManager.registerListener(this, mAccelerometer, SENSOR_DELAY)
mSensorManager.registerListener(this, mGyroscope, SENSOR_DELAY)
// by returning this we make sure the service is restarted if the system kills the service
return START_STICKY
}
@ -90,6 +99,7 @@ class EndlessService : Service(), SensorEventListener, LocationListener {
mSensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
mGyroscope = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
setupActivityRecognitionUpdates()
if (checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
mLocationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
@ -109,7 +119,66 @@ class EndlessService : Service(), SensorEventListener, LocationListener {
}
}
private fun getActivityTransitionPendingIntent(): PendingIntent {
val intent = Intent(this, ActivityTransitionIntentService::class.java)
return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
private fun setupActivityRecognitionUpdates() {
// ActivityRecognition
val transitions = mutableListOf<ActivityTransition>()
transitions +=
ActivityTransition.Builder()
.setActivityType(DetectedActivity.IN_VEHICLE)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
.build()
/*transitions +=
ActivityTransition.Builder()
.setActivityType(DetectedActivity.IN_VEHICLE)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_EXIT)
.build()*/
transitions +=
ActivityTransition.Builder()
.setActivityType(DetectedActivity.ON_BICYCLE)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
.build()
/*transitions +=
ActivityTransition.Builder()
.setActivityType(DetectedActivity.ON_BICYCLE)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_EXIT)
.build()*/
transitions +=
ActivityTransition.Builder()
.setActivityType(DetectedActivity.WALKING)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
.build()
/*transitions +=
ActivityTransition.Builder()
.setActivityType(DetectedActivity.WALKING)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_EXIT)
.build()*/
transitions +=
ActivityTransition.Builder()
.setActivityType(DetectedActivity.STILL)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
.build()
val request = ActivityTransitionRequest(transitions)
val pendingIntent = getActivityTransitionPendingIntent()
mActivityRecognitionClient = ActivityRecognition.getClient(applicationContext)
val task =
mActivityRecognitionClient.requestActivityTransitionUpdates(request, pendingIntent)
task.addOnSuccessListener { log("ACT_TRANSITION: success listener") }
task.addOnFailureListener { exception ->
log("ACT_TRANSITION: failed $exception")
}
}
override fun onDestroy() {
if (dataTemp.size > 0) {
recRepo.addBulkRecord(dataTemp)
}
super.onDestroy()
mSensorManager.unregisterListener(this)
mLocationManager?.removeUpdates(this)
@ -133,7 +202,7 @@ class EndlessService : Service(), SensorEventListener, LocationListener {
}
// we're starting a loop in a coroutine
GlobalScope.launch(Dispatchers.IO) {
/* GlobalScope.launch(Dispatchers.IO) {
while (isServiceStarted) {
launch(Dispatchers.IO) {
pingFakeServer()
@ -141,7 +210,7 @@ class EndlessService : Service(), SensorEventListener, LocationListener {
delay(1 * 180 * 1000)
}
log("End of the loop for the service")
}
} */
}
private fun stopService() {
@ -176,9 +245,9 @@ class EndlessService : Service(), SensorEventListener, LocationListener {
result.accuracy,
result.provider,
result.isFromMockProvider
).joinToString(",")
).joinToString(",")
recRepo.addNewRecord("location", txt)
addNewRecord("location", txt)
val notification = createNotification(notiText)
nm.notify(notificationId, notification)
}
@ -219,7 +288,7 @@ class EndlessService : Service(), SensorEventListener, LocationListener {
accelLin[1].format(3),
accelLin[2].format(3)
).joinToString(",")
recRepo.addNewRecord("accelerometer", txt)
addNewRecord("accelerometer", txt)
}
Sensor.TYPE_GYROSCOPE -> {
val txt = arrayOf(
@ -227,7 +296,8 @@ class EndlessService : Service(), SensorEventListener, LocationListener {
evt.values[1].format(3),
evt.values[2].format(3)
).joinToString(",")
recRepo.addNewRecord("gyroscope", txt)
addNewRecord("gyroscope", txt)
}
}
}
@ -256,7 +326,8 @@ class EndlessService : Service(), SensorEventListener, LocationListener {
val (bytes, error) = result
if (bytes != null) {
log("[response bytes] ${String(bytes)}")
val notification = createNotification("[req] ${gmtTime} - ${String(bytes)}")
val notification =
createNotification("[req] ${gmtTime} - ${String(bytes)}")
nm.notify(notificationId, notification)
} else {
log("[response error] ${error?.message}")
@ -276,13 +347,13 @@ class EndlessService : Service(), SensorEventListener, LocationListener {
val channel = NotificationChannel(
notificationChannelId,
"SL notifications channel",
NotificationManager.IMPORTANCE_DEFAULT
NotificationManager.IMPORTANCE_LOW
).let {
it.description = "SL Service channel"
it.enableLights(true)
it.lightColor = Color.RED
it.enableVibration(true)
it.vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
it.enableLights(false)
//it.lightColor = Color.RED
it.enableVibration(false)
//it.vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
it
}
nm.createNotificationChannel(channel)

21
app/src/main/java/co/zzyzx/sensorlogger/MainActivity.kt

@ -4,17 +4,21 @@ import android.Manifest
import android.app.ActivityManager
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.location.LocationManager
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.preference.Preference
import android.preference.PreferenceManager
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import co.zzyzx.sensorlogger.db.RecordRepository
import com.opencsv.CSVWriter
import kotlinx.android.synthetic.main.activity_main.*
@ -23,13 +27,15 @@ import kotlinx.coroutines.launch
import java.io.File
import java.io.FileWriter
import java.time.Instant
import java.util.prefs.PreferenceChangeListener
const val PERMISSION_LOCATION = 0
const val WRITE_EXTERNAL_STORAGE = 1
class MainActivity : AppCompatActivity(),
ActivityCompat.OnRequestPermissionsResultCallback {
ActivityCompat.OnRequestPermissionsResultCallback,
SharedPreferences.OnSharedPreferenceChangeListener {
private lateinit var recRepo: RecordRepository
private var handler = Handler()
@ -333,4 +339,11 @@ class MainActivity : AppCompatActivity(),
}
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
if (key == "ACTIVITY") {
status_textview.text = PreferenceManager.getDefaultSharedPreferences(this).getString(key, "- Nah -")
}
}
}

18
app/src/main/java/co/zzyzx/sensorlogger/db/Database.kt

@ -1,16 +1,15 @@
package co.zzyzx.sensorlogger.db
import android.arch.persistence.room.Database
import android.arch.persistence.room.Room
import android.arch.persistence.room.RoomDatabase
import android.content.Context
import co.zzyzx.sensorlogger.log
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.time.Instant
@Database(entities = arrayOf(Record::class), version = 1)
@Database(entities = [Record::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun recordDao(): RecordDao
}
@ -31,8 +30,13 @@ class RecordRepository {
fun addNewRecord(sensor: String, data: String) {
val now = Instant.now().toEpochMilli()
GlobalScope.launch {
log("add [${now}] ${sensor} - ${data}")
dao.insertAll(Record(timestamp = now, sensor = sensor, data = data))
dao.insert(Record(timestamp = now, sensor = sensor, data = data))
}
}
fun addBulkRecord(recs: List<Record>) {
GlobalScope.launch {
dao.insert(*recs.toTypedArray())
}
}

11
app/src/main/java/co/zzyzx/sensorlogger/db/dao.kt

@ -1,7 +1,8 @@
package co.zzyzx.sensorlogger.db
import android.arch.lifecycle.LiveData
import android.arch.persistence.room.*
import androidx.lifecycle.LiveData
import androidx.room.*
@Dao
interface RecordDao {
@ -12,18 +13,16 @@ interface RecordDao {
fun getAll(): List<Record>
@Query("SELECT * FROM record WHERE sensor LIKE :sensor ORDER BY timestamp DESC LIMIT 1")
fun getLatestLiveData(sensor : String): LiveData<List<Record>>
fun getLatestLiveData(sensor: String): LiveData<List<Record>>
@Query("SELECT * FROM record WHERE sensor LIKE :name ORDER BY timestamp DESC LIMIT 1")
fun findBySensor(name: String): Record
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(vararg records: Record)
@Insert
fun insert(vararg records: Record)
@Delete
fun delete(record: Record)

8
app/src/main/java/co/zzyzx/sensorlogger/db/entity.kt

@ -1,9 +1,9 @@
package co.zzyzx.sensorlogger.db
import android.arch.persistence.room.ColumnInfo
import android.arch.persistence.room.Entity
import android.arch.persistence.room.Index
import android.arch.persistence.room.PrimaryKey
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(indices = [(Index(value = ["timestamp", "sensor"], unique = true))])

Loading…
Cancel
Save