feat: store and export measurements locally
This commit is contained in:
@@ -58,6 +58,8 @@ dependencies {
|
|||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.2.0'
|
implementation 'androidx.recyclerview:recyclerview:1.2.0'
|
||||||
|
|
||||||
|
implementation 'com.google.code.gson:gson:2.8.8'
|
||||||
|
|
||||||
implementation group: 'commons-codec', name: 'commons-codec', version: '1.16.0'
|
implementation group: 'commons-codec', name: 'commons-codec', version: '1.16.0'
|
||||||
|
|
||||||
implementation "com.android.volley:volley:1.2.1"
|
implementation "com.android.volley:volley:1.2.1"
|
||||||
|
|||||||
@@ -17,6 +17,17 @@
|
|||||||
android:name="android.permission.ACCESS_FINE_LOCATION"
|
android:name="android.permission.ACCESS_FINE_LOCATION"
|
||||||
android:maxSdkVersion="30" />
|
android:maxSdkVersion="30" />
|
||||||
|
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||||
|
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.FOREGROUND_SERVICE" />
|
android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
|
||||||
@@ -50,6 +61,17 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<service android:name=".BLEService" android:foregroundServiceType="connectedDevice" />
|
<service android:name=".BLEService" android:foregroundServiceType="connectedDevice" />
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.provider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/provider_paths" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
161
app/src/main/java/com/example/sensortestingapp/Database.kt
Normal file
161
app/src/main/java/com/example/sensortestingapp/Database.kt
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package com.example.sensortestingapp
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.sqlite.SQLiteDatabase
|
||||||
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.BaseColumns
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonSyntaxException
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
|
||||||
|
object LoggerContract {
|
||||||
|
// Table contents are grouped together in an anonymous object.
|
||||||
|
object LogEntry : BaseColumns {
|
||||||
|
const val TABLE_NAME = "measurements"
|
||||||
|
const val COLUMN_NAME_TS = "ts"
|
||||||
|
const val COLUMN_NAME_PAYLOAD = "payload"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val SQL_CREATE_ENTRIES =
|
||||||
|
"CREATE TABLE ${LogEntry.TABLE_NAME} (" +
|
||||||
|
"${BaseColumns._ID} INTEGER PRIMARY KEY," +
|
||||||
|
"${LogEntry.COLUMN_NAME_TS} TEXT," +
|
||||||
|
"${LogEntry.COLUMN_NAME_PAYLOAD} TEXT)"
|
||||||
|
|
||||||
|
private const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS ${LogEntry.TABLE_NAME}"
|
||||||
|
|
||||||
|
class LoggerDbHelper(context: Context) :
|
||||||
|
SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
|
||||||
|
override fun onCreate(db: SQLiteDatabase) {
|
||||||
|
db.execSQL(SQL_CREATE_ENTRIES)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
|
// This database is only a cache for online data, so its upgrade policy is
|
||||||
|
// to simply to discard the data and start over
|
||||||
|
db.execSQL(SQL_DELETE_ENTRIES)
|
||||||
|
onCreate(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
|
onUpgrade(db, oldVersion, newVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// If you change the database schema, you must increment the database version.
|
||||||
|
const val DATABASE_VERSION = 2
|
||||||
|
const val DATABASE_NAME = "Logger.db"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoggerDb(context: Context) {
|
||||||
|
|
||||||
|
private val dbHelper = LoggerDbHelper(context)
|
||||||
|
private val dbWrite = dbHelper.writableDatabase
|
||||||
|
private val dbRead = dbHelper.writableDatabase
|
||||||
|
val context: Context = context
|
||||||
|
|
||||||
|
fun writeLog(payload: Any): Long? {
|
||||||
|
val gson = Gson()
|
||||||
|
val jsonString = gson.toJson(payload)
|
||||||
|
|
||||||
|
val date = LocalDateTime.now()
|
||||||
|
val ts = date.format(DateTimeFormatter.ISO_DATE_TIME)
|
||||||
|
|
||||||
|
Log.i("Database", jsonString)
|
||||||
|
|
||||||
|
val values = ContentValues().apply {
|
||||||
|
put(LogEntry.COLUMN_NAME_TS, ts)
|
||||||
|
put(LogEntry.COLUMN_NAME_PAYLOAD, jsonString)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbWrite?.insert(LogEntry.TABLE_NAME, null, values)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun exportToUri(uri: Uri) {
|
||||||
|
val projection =
|
||||||
|
arrayOf(BaseColumns._ID, LogEntry.COLUMN_NAME_PAYLOAD, LogEntry.COLUMN_NAME_TS)
|
||||||
|
|
||||||
|
val sortOrder = "${BaseColumns._ID} ASC"
|
||||||
|
|
||||||
|
val cursor = dbRead.query(
|
||||||
|
LogEntry.TABLE_NAME, // The table to query
|
||||||
|
projection, // The array of columns to return (pass null to get all)
|
||||||
|
null, // The columns for the WHERE clause
|
||||||
|
null, // The values for the WHERE clause
|
||||||
|
null, // don't group the rows
|
||||||
|
null, // don't filter by row groups
|
||||||
|
sortOrder // The sort order
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
val gson = Gson()
|
||||||
|
val type = object : TypeToken<HashMap<String, Any>>() {}.type
|
||||||
|
var headerWritten = false
|
||||||
|
val sep = ","
|
||||||
|
|
||||||
|
context.contentResolver.openOutputStream(uri)?.use { writer ->
|
||||||
|
val newLine = '\n'
|
||||||
|
|
||||||
|
with(cursor) {
|
||||||
|
while (moveToNext()) {
|
||||||
|
val ts = getString(getColumnIndexOrThrow(LogEntry.COLUMN_NAME_TS))
|
||||||
|
val storedField =
|
||||||
|
getString(getColumnIndexOrThrow(LogEntry.COLUMN_NAME_PAYLOAD))
|
||||||
|
|
||||||
|
try {
|
||||||
|
val payload: HashMap<String, Any> = gson.fromJson(storedField, type)
|
||||||
|
|
||||||
|
if (!headerWritten) {
|
||||||
|
val headerRow =
|
||||||
|
"timestamp" + sep + payload.keys.joinToString(sep) + newLine
|
||||||
|
writer.write(headerRow.toByteArray())
|
||||||
|
|
||||||
|
headerWritten = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val row = ts + sep + payload.values.joinToString(sep) + newLine
|
||||||
|
|
||||||
|
writer.write(row.toByteArray())
|
||||||
|
} catch (exception: JsonSyntaxException) {
|
||||||
|
Log.e("db", exception.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
truncate()
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun truncate() {
|
||||||
|
dbWrite.execSQL("DELETE FROM ${LogEntry.TABLE_NAME}");
|
||||||
|
dbWrite.execSQL("VACUUM");
|
||||||
|
}
|
||||||
|
|
||||||
|
fun close() {
|
||||||
|
dbHelper.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -109,5 +109,9 @@ object DecoderIaq {
|
|||||||
", occ=" + occupancy +
|
", occ=" + occupancy +
|
||||||
'}'
|
'}'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toCsv() : String {
|
||||||
|
return "${msgType},${co2},${voc},${humidity},${temperature},${pressure}${pm25},${pm10},${occupancy}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,8 +37,10 @@ class KirbyDevice(
|
|||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val connectionManager: ConnectionManager,
|
private val connectionManager: ConnectionManager,
|
||||||
private val bleDevice: BluetoothDevice,
|
private val bleDevice: BluetoothDevice,
|
||||||
|
private val loggerDb: LoggerContract.LoggerDb,
|
||||||
private val onStateChange: (device: KirbyDevice) -> Unit,
|
private val onStateChange: (device: KirbyDevice) -> Unit,
|
||||||
|
|
||||||
|
|
||||||
) : BleListener(bleDevice.address), DeviceListEntry {
|
) : BleListener(bleDevice.address), DeviceListEntry {
|
||||||
private val queue: RequestQueue = Volley.newRequestQueue(context)
|
private val queue: RequestQueue = Volley.newRequestQueue(context)
|
||||||
|
|
||||||
@@ -138,6 +140,8 @@ class KirbyDevice(
|
|||||||
Log.i("BleListener", "Char received: $payload")
|
Log.i("BleListener", "Char received: $payload")
|
||||||
val base64Payload = Base64.getEncoder().encodeToString(characteristic.value)
|
val base64Payload = Base64.getEncoder().encodeToString(characteristic.value)
|
||||||
publishMeasurement(base64Payload)
|
publishMeasurement(base64Payload)
|
||||||
|
|
||||||
|
loggerDb.writeLog( measurement)
|
||||||
}
|
}
|
||||||
|
|
||||||
measurements.add(payload)
|
measurements.add(payload)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import android.content.Context
|
|||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -19,16 +20,23 @@ import android.view.Menu
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View.INVISIBLE
|
import android.view.View.INVISIBLE
|
||||||
import android.view.View.VISIBLE
|
import android.view.View.VISIBLE
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import com.example.sensortestingapp.databinding.ActivityMainBinding
|
import com.example.sensortestingapp.databinding.ActivityMainBinding
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
|
||||||
private const val ENABLE_BLUETOOTH_REQUEST_CODE = 1
|
private const val ENABLE_BLUETOOTH_REQUEST_CODE = 1
|
||||||
private const val RUNTIME_PERMISSION_REQUEST_CODE = 2
|
private const val RUNTIME_PERMISSION_REQUEST_CODE = 2
|
||||||
|
|
||||||
|
|
||||||
fun Context.hasPermission(permissionType: String): Boolean {
|
fun Context.hasPermission(permissionType: String): Boolean {
|
||||||
return ContextCompat.checkSelfPermission(this, permissionType) ==
|
return ContextCompat.checkSelfPermission(this, permissionType) ==
|
||||||
PackageManager.PERMISSION_GRANTED
|
PackageManager.PERMISSION_GRANTED
|
||||||
@@ -47,6 +55,9 @@ fun Context.hasRequiredRuntimePermissions(): Boolean {
|
|||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
|
private lateinit var loggerDb: LoggerContract.LoggerDb
|
||||||
|
private lateinit var createFileLauncher: ActivityResultLauncher<String>
|
||||||
|
|
||||||
|
|
||||||
private val bluetoothAdapter: BluetoothAdapter by lazy {
|
private val bluetoothAdapter: BluetoothAdapter by lazy {
|
||||||
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||||
@@ -66,10 +77,12 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private var onPermissionsGrantedCallback: Runnable? = null
|
private var onPermissionsGrantedCallback: Runnable? = null
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
@@ -90,8 +103,19 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loggerDb = LoggerContract.LoggerDb(this.applicationContext)
|
||||||
|
|
||||||
setupDevicesList()
|
setupDevicesList()
|
||||||
|
|
||||||
|
checkStoragePermission()
|
||||||
|
|
||||||
|
createFileLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.CreateDocument()) { uri: Uri? ->
|
||||||
|
uri?.let {
|
||||||
|
// Use the URI to write your CSV content
|
||||||
|
loggerDb.exportToUri(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleScanning(): Unit {
|
private fun toggleScanning(): Unit {
|
||||||
@@ -107,6 +131,21 @@ class MainActivity : AppCompatActivity() {
|
|||||||
return deviceName.contains("kirby") || deviceName.contains("krby")
|
return deviceName.contains("kirby") || deviceName.contains("krby")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun checkStoragePermission() {
|
||||||
|
if (ContextCompat.checkSelfPermission(
|
||||||
|
this,
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
// Permission is not granted, request it
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
this,
|
||||||
|
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
|
||||||
|
4242
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun setupDevicesList() {
|
private fun setupDevicesList() {
|
||||||
binding.devicesList.apply {
|
binding.devicesList.apply {
|
||||||
@@ -181,7 +220,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun newKirbyDevice(bleDevice: BluetoothDevice): KirbyDevice {
|
private fun newKirbyDevice(bleDevice: BluetoothDevice): KirbyDevice {
|
||||||
|
|
||||||
val device = KirbyDevice(this.applicationContext, connectionManager, bleDevice) {
|
val device = KirbyDevice(this.applicationContext, connectionManager, bleDevice, loggerDb) {
|
||||||
val i = kirbyDevices.indexOfFirst { d -> d === it }
|
val i = kirbyDevices.indexOfFirst { d -> d === it }
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
deviceListAdapter.notifyItemChanged(i)
|
deviceListAdapter.notifyItemChanged(i)
|
||||||
@@ -201,12 +240,20 @@ class MainActivity : AppCompatActivity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
// Handle action bar item clicks here. The action bar will
|
// Handle action bar item clicks here. The action bar will
|
||||||
// automatically handle clicks on the Home/Up button, so long
|
// automatically handle clicks on the Home/Up button, so long
|
||||||
// as you specify a parent activity in AndroidManifest.xml.
|
// as you specify a parent activity in AndroidManifest.xml.
|
||||||
return when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.action_settings -> true
|
R.id.action_export -> {
|
||||||
|
|
||||||
|
val date = LocalDateTime.now()
|
||||||
|
val fileName = "kirby_export_${date.format(DateTimeFormatter.ISO_DATE_TIME)}.csv"
|
||||||
|
createFileLauncher.launch(fileName)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,6 +265,11 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
loggerDb.close()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
when (requestCode) {
|
when (requestCode) {
|
||||||
@@ -275,10 +327,14 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun Activity.requestRelevantRuntimePermissions() {
|
private fun Activity.requestRelevantRuntimePermissions() {
|
||||||
|
|
||||||
if (hasRequiredRuntimePermissions()) {
|
if (hasRequiredRuntimePermissions()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
when {
|
when {
|
||||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> {
|
Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> {
|
||||||
requestLocationPermission()
|
requestLocationPermission()
|
||||||
@@ -290,6 +346,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun requestLocationPermission() {
|
private fun requestLocationPermission() {
|
||||||
val onClick = { dialog: DialogInterface, which: Int ->
|
val onClick = { dialog: DialogInterface, which: Int ->
|
||||||
ActivityCompat.requestPermissions(
|
ActivityCompat.requestPermissions(
|
||||||
@@ -318,6 +375,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun requestBluetoothPermissions() {
|
private fun requestBluetoothPermissions() {
|
||||||
val onClick = { dialog: DialogInterface, which: Int ->
|
val onClick = { dialog: DialogInterface, which: Int ->
|
||||||
ActivityCompat.requestPermissions(
|
ActivityCompat.requestPermissions(
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
tools:context="com.example.sensortestingapp.MainActivity">
|
tools:context="com.example.sensortestingapp.MainActivity">
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_settings"
|
android:id="@+id/action_export"
|
||||||
android:orderInCategory="100"
|
android:orderInCategory="100"
|
||||||
android:title="@string/action_settings"
|
android:title="@string/action_export"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
</menu>
|
</menu>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Sensor Testing App</string>
|
<string name="app_name">Sensor Testing App</string>
|
||||||
<string name="action_settings">Settings</string>
|
<string name="action_export">Export to csv</string>
|
||||||
<!-- Strings used for fragments for navigation -->
|
<!-- Strings used for fragments for navigation -->
|
||||||
<string name="first_fragment_label">First Fragment</string>
|
<string name="first_fragment_label">First Fragment</string>
|
||||||
<string name="second_fragment_label">Second Fragment</string>
|
<string name="second_fragment_label">Second Fragment</string>
|
||||||
|
|||||||
10
app/src/main/res/xml/provider_paths.xml
Normal file
10
app/src/main/res/xml/provider_paths.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<files-path
|
||||||
|
name="files"
|
||||||
|
path="."/>
|
||||||
|
|
||||||
|
<external-path
|
||||||
|
name="external_files"
|
||||||
|
path="."/>
|
||||||
|
</paths>
|
||||||
Reference in New Issue
Block a user