mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Merge remote-tracking branch 'origin/master' into feature/mapbox-v10-migration
This commit is contained in:
commit
7fd3cbba69
16 changed files with 202 additions and 121 deletions
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
## Thank you for sending in a pull request, here's some tips to get started!
|
||||
|
||||
(Please delete all these tips and replace with your text)
|
||||
|
||||
- Mention "#(issue)" in the description, when applicable
|
||||
- Please do not check in files that don't have real changes
|
||||
- Please do not reformat lines that you didn't have to change the code on
|
||||
- If your other co-developers have comments on your PR please tweak as needed
|
||||
- Do not use any external image service, just paste or drag and drop the image here and it will be uploaded automatically
|
||||
7
.github/workflows/android.yml
vendored
7
.github/workflows/android.yml
vendored
|
|
@ -17,18 +17,17 @@ jobs:
|
|||
|
||||
- name: Load secrets
|
||||
run: |
|
||||
rm ./app/google-services.json
|
||||
cp ./app/google-services-example.json ./app/google-services.json
|
||||
rm ./app/src/main/res/values/mapbox-token.xml
|
||||
echo -e "<resources>\n <string name=\"mapbox_access_token\">$MAPBOXTOKEN</string>\n</resources>" > ./app/src/main/res/values/mapbox-token.xml
|
||||
mkdir -p ~/.gradle
|
||||
echo "MAPBOX_DOWNLOADS_TOKEN=$MAPBOXTOKEN" >>~/.gradle/gradle.properties
|
||||
env:
|
||||
GSERVICES: ${{ secrets.GSERVICES }}
|
||||
MAPBOXTOKEN: ${{ secrets.MAPBOXTOKEN }}
|
||||
|
||||
- name: Mock curfirmware version for CI
|
||||
- name: Mock files for CI
|
||||
run: |
|
||||
rm ./app/google-services.json
|
||||
cp ./app/google-services-example.json ./app/google-services.json
|
||||
rm ./app/src/main/res/values/curfirmwareversion.xml
|
||||
cp ./app/special/curfirmwareversion.xml ./app/src/main/res/values/
|
||||
|
||||
|
|
|
|||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -98,7 +98,7 @@ jobs:
|
|||
with:
|
||||
draft: true
|
||||
prerelease: true
|
||||
release_name: ${{ github.event.inputs.version}} alpha
|
||||
release_name: Meshtastic Android ${{ github.event.inputs.version}} alpha
|
||||
tag_name: ${{ github.event.inputs.version}}
|
||||
body: |
|
||||
Autogenerated by github action, developer should edit as required before publishing...
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ cp ./app/special/curfirmwareversion.xml ./app/src/main/res/values/
|
|||
cat ~/.gradle/gradle.properties
|
||||
MAPBOX_DOWNLOADS_TOKEN=sk.yourtokenherexxx
|
||||
```
|
||||
- (optional) to run CI tests on your fork: 1) allow GitHub Actions; 2) add your token at: Settings > Secrets > Actions > New repository secret: Name: MAPBOXTOKEN Value: sk.yourtokenherexxx
|
||||
|
||||
- Now you should be able to select "Run / Run" in the IDE and it will happily start running on your phone
|
||||
or the emulator. Note: The emulators don't support bluetooth, so some features can not be used in
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ android {
|
|||
applicationId "com.geeksville.mesh"
|
||||
minSdkVersion 21 // The oldest emulator image I have tried is 22 (though 21 probably works)
|
||||
targetSdkVersion 30 // 30 can't work until an explicit location permissions dialog is added
|
||||
versionCode 20255 // format is Mmmss (where M is 1+the numeric major number
|
||||
versionName "1.2.55"
|
||||
versionCode 20256 // format is Mmmss (where M is 1+the numeric major number
|
||||
versionName "1.2.56"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
// per https://developer.android.com/studio/write/vector-asset-studio
|
||||
|
|
@ -199,5 +199,7 @@ dependencies {
|
|||
// implementation "androidx.work:work-runtime:$work_version"
|
||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||
|
||||
implementation "androidx.core:core-splashscreen:1.0.0-beta01"
|
||||
|
||||
implementation project(':geeksville-androidlib')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@
|
|||
android:label="@string/app_name"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:theme="@style/AppTheme"
|
||||
android:theme="@style/Theme.App.Starting"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import androidx.appcompat.app.AppCompatDelegate
|
|||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
|
|
@ -43,7 +44,6 @@ import com.geeksville.android.Logging
|
|||
import com.geeksville.android.ServiceClient
|
||||
import com.geeksville.concurrent.handledLaunch
|
||||
import com.geeksville.mesh.android.*
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import com.geeksville.mesh.databinding.ActivityMainBinding
|
||||
import com.geeksville.mesh.model.ChannelSet
|
||||
import com.geeksville.mesh.model.DeviceVersion
|
||||
|
|
@ -60,16 +60,16 @@ import com.google.android.gms.tasks.Task
|
|||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import com.vorlonsoft.android.rate.AppRate
|
||||
import com.vorlonsoft.android.rate.StoreType
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.FileOutputStream
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import java.nio.charset.Charset
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
/*
|
||||
|
|
@ -388,12 +388,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
|
||||
return if (message != null) {
|
||||
errormsg("Denied permissions: $message")
|
||||
Snackbar.make(findViewById(android.R.id.content), message, Snackbar.LENGTH_INDEFINITE)
|
||||
.apply { view.findViewById<TextView>(R.id.snackbar_text).isSingleLine = false }
|
||||
.setAction(R.string.okay) {
|
||||
// dismiss
|
||||
}
|
||||
.show()
|
||||
showSnackbar(message)
|
||||
true
|
||||
} else
|
||||
false
|
||||
|
|
@ -483,6 +478,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
|
|
@ -661,20 +657,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
}
|
||||
CREATE_CSV_FILE -> {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
data?.data?.let { file_uri ->
|
||||
// model.allPackets is a result of a query, so we need to use observer for
|
||||
// the query to materialize
|
||||
model.allPackets.observe(this, { packets ->
|
||||
if (packets != null) {
|
||||
// no need for observer once got non-null list
|
||||
model.allPackets.removeObservers(this)
|
||||
// execute on the default thread pool to not block the main thread
|
||||
CoroutineScope(Dispatchers.Default + Job()).handledLaunch {
|
||||
saveMessagesCSV(file_uri, packets)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
data?.data?.let { file_uri -> model.saveMessagesCSV(file_uri) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -814,30 +797,35 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
}
|
||||
}
|
||||
|
||||
private fun showToast(msgId: Int) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
private fun showSnackbar(msgId: Int) {
|
||||
Snackbar.make(
|
||||
findViewById(android.R.id.content),
|
||||
msgId,
|
||||
Toast.LENGTH_LONG
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
||||
private fun showToast(msg: String) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
private fun showSnackbar(msg: String) {
|
||||
Snackbar.make(
|
||||
findViewById(android.R.id.content),
|
||||
msg,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
Snackbar.LENGTH_INDEFINITE
|
||||
)
|
||||
.apply { view.findViewById<TextView>(R.id.snackbar_text).isSingleLine = false }
|
||||
.setAction(R.string.okay) {
|
||||
// dismiss
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun perhapsChangeChannel() {
|
||||
fun perhapsChangeChannel(url: Uri? = requestedChannelUrl) {
|
||||
// If the is opening a channel URL, handle it now
|
||||
requestedChannelUrl?.let { url ->
|
||||
if (url != null) {
|
||||
try {
|
||||
val channels = ChannelSet(url)
|
||||
val primary = channels.primaryChannel
|
||||
if (primary == null)
|
||||
showToast(R.string.channel_invalid)
|
||||
showSnackbar(R.string.channel_invalid)
|
||||
else {
|
||||
requestedChannelUrl = null
|
||||
|
||||
|
|
@ -853,13 +841,14 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
model.setChannels(channels)
|
||||
} catch (ex: RemoteException) {
|
||||
errormsg("Couldn't change channel ${ex.message}")
|
||||
showToast(R.string.cant_change_no_radio)
|
||||
showSnackbar(R.string.cant_change_no_radio)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
} catch (ex: InvalidProtocolBufferException) {
|
||||
showToast(R.string.channel_invalid)
|
||||
} catch (ex: Throwable) {
|
||||
errormsg("Channel url error: ${ex.message}")
|
||||
showSnackbar("${getString(R.string.channel_invalid)}: ${ex.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1184,46 +1173,12 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
try {
|
||||
val packageInfo: PackageInfo = packageManager.getPackageInfo(packageName, 0)
|
||||
val versionName = packageInfo.versionName
|
||||
showToast(versionName)
|
||||
Toast.makeText(this, versionName, Toast.LENGTH_LONG).show()
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
errormsg("Can not find the version: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveMessagesCSV(file_uri: Uri, packets: List<Packet>) {
|
||||
// Extract distances to this device from position messages and put (node,SNR,distance) in
|
||||
// the file_uri
|
||||
val myNodeNum = model.myNodeInfo.value?.myNodeNum ?: return
|
||||
|
||||
applicationContext.contentResolver.openFileDescriptor(file_uri, "w")?.use {
|
||||
FileOutputStream(it.fileDescriptor).use { fs ->
|
||||
// Write header
|
||||
fs.write(("from,rssi,snr,time,dist\n").toByteArray())
|
||||
// Packets are ordered by time, we keep most recent position of
|
||||
// our device in my_position.
|
||||
var my_position: MeshProtos.Position? = null
|
||||
packets.forEach {
|
||||
it.proto?.let { packet_proto ->
|
||||
it.position?.let { position ->
|
||||
if (packet_proto.from == myNodeNum) {
|
||||
my_position = position
|
||||
} else if (my_position != null) {
|
||||
val dist = positionToMeter(my_position!!, position).roundToInt()
|
||||
fs.write(
|
||||
"%x,%d,%f,%d,%d\n".format(
|
||||
packet_proto.from, packet_proto.rxRssi,
|
||||
packet_proto.rxSnr, packet_proto.rxTime, dist
|
||||
).toByteArray()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Theme functions
|
||||
|
||||
private fun chooseThemeDialog() {
|
||||
|
|
|
|||
|
|
@ -71,6 +71,13 @@ data class Position(
|
|||
/// @return bearing to the other position in degrees
|
||||
fun bearing(o: Position) = bearing(latitude, longitude, o.latitude, o.longitude)
|
||||
|
||||
// If GPS gives a crap position don't crash our app
|
||||
fun isValid(): Boolean {
|
||||
return (latitude <= 90.0 && latitude >= -90) &&
|
||||
latitude != 0.0 &&
|
||||
longitude != 0.0
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Position(lat=${latitude.anonymize}, lon=${longitude.anonymize}, alt=${altitude.anonymize}, time=${time}, batteryPctLevel=${batteryPctLevel})"
|
||||
}
|
||||
|
|
@ -112,11 +119,7 @@ data class NodeInfo(
|
|||
/// return the position if it is valid, else null
|
||||
val validPosition: Position?
|
||||
get() {
|
||||
return position?.takeIf {
|
||||
(it.latitude <= 90.0 && it.latitude >= -90) && // If GPS gives a crap position don't crash our app
|
||||
it.latitude != 0.0 &&
|
||||
it.longitude != 0.0
|
||||
}
|
||||
return position?.takeIf { it.isValid() }
|
||||
}
|
||||
|
||||
/// @return distance in meters to some other node (or null if unknown)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ package com.geeksville.mesh.database
|
|||
import androidx.lifecycle.LiveData
|
||||
import com.geeksville.mesh.database.dao.PacketDao
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class PacketRepository(private val packetDao : PacketDao) {
|
||||
val allPackets : LiveData<List<Packet>> = packetDao.getAllPacket(500)
|
||||
val allPackets : LiveData<List<Packet>> = packetDao.getAllPacket(MAX_ITEMS)
|
||||
val allPacketsInReceiveOrder : Flow<List<Packet>> = packetDao.getAllPacketsInReceiveOrder(MAX_ITEMS)
|
||||
|
||||
suspend fun insert(packet: Packet) {
|
||||
packetDao.insert(packet)
|
||||
|
|
@ -14,4 +16,9 @@ class PacketRepository(private val packetDao : PacketDao) {
|
|||
suspend fun deleteAll() {
|
||||
packetDao.deleteAll()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_ITEMS = 500
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import androidx.room.Dao
|
|||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface PacketDao {
|
||||
|
|
@ -12,6 +13,9 @@ interface PacketDao {
|
|||
@Query("Select * from packet order by received_date desc limit 0,:maxItem")
|
||||
fun getAllPacket(maxItem: Int): LiveData<List<Packet>>
|
||||
|
||||
@Query("Select * from packet order by received_date asc limit 0,:maxItem")
|
||||
fun getAllPacketsInReceiveOrder(maxItem: Int): Flow<List<Packet>>
|
||||
|
||||
@Insert
|
||||
fun insert(packet: Packet)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,15 +12,21 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.mesh.IMeshService
|
||||
import com.geeksville.mesh.MyNodeInfo
|
||||
import com.geeksville.mesh.RadioConfigProtos
|
||||
import com.geeksville.mesh.*
|
||||
import com.geeksville.mesh.database.MeshtasticDatabase
|
||||
import com.geeksville.mesh.database.PacketRepository
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import com.geeksville.mesh.service.MeshService
|
||||
import com.geeksville.mesh.ui.positionToMeter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedWriter
|
||||
import java.io.FileWriter
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/// Given a human name, strip out the first letter of the first three words and return that as the initials for
|
||||
/// that user. If the original name is only one word, strip vowels from the original name and if the result is
|
||||
|
|
@ -257,5 +263,96 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging
|
|||
errormsg("Can't set username on device, is device offline? ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the persisted packet data out to a CSV file in the specified location.
|
||||
*/
|
||||
fun saveMessagesCSV(file_uri: Uri) {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
// Extract distances to this device from position messages and put (node,SNR,distance) in
|
||||
// the file_uri
|
||||
val myNodeNum = myNodeInfo.value?.myNodeNum ?: return@launch
|
||||
|
||||
// Capture the current node value while we're still on main thread
|
||||
val nodes = nodeDB.nodes.value ?: emptyMap()
|
||||
|
||||
writeToUri(file_uri) { writer ->
|
||||
// Create a map of nodes keyed by their ID
|
||||
val nodesById = nodes.values.associateBy { it.num }
|
||||
|
||||
writer.appendLine("date,time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx snr,distance,hop limit,payload")
|
||||
|
||||
// Packets are ordered by time, we keep most recent position of
|
||||
// our device in localNodePosition.
|
||||
var localNodePosition: MeshProtos.Position? = null
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd,HH:mm:ss", Locale.getDefault())
|
||||
repository.allPacketsInReceiveOrder.first().forEach { packet ->
|
||||
packet.proto?.let { proto ->
|
||||
packet.position?.let { position ->
|
||||
if (proto.from == myNodeNum) {
|
||||
localNodePosition = position
|
||||
} else {
|
||||
val rxDateTime = dateFormat.format(packet.received_date)
|
||||
val rxFrom = proto.from.toUInt()
|
||||
val senderName = nodesById[proto.from]?.user?.longName ?: ""
|
||||
|
||||
// sender lat & long
|
||||
val senderPos = packet.position
|
||||
?.let { p -> Position(p) }
|
||||
?.takeIf { p -> p.isValid() }
|
||||
val senderLat = senderPos?.latitude ?: ""
|
||||
val senderLong = senderPos?.longitude ?: ""
|
||||
|
||||
// rx lat, long, and elevation
|
||||
val rxPos = localNodePosition
|
||||
?.let { p -> Position(p) }
|
||||
?.takeIf { p -> p.isValid() }
|
||||
val rxLat = rxPos?.latitude ?: ""
|
||||
val rxLong = rxPos?.longitude ?: ""
|
||||
val rxAlt = rxPos?.altitude ?: ""
|
||||
val rxSnr = "%f".format(proto.rxSnr)
|
||||
|
||||
// Calculate the distance if both positions are valid
|
||||
val dist = if (senderPos == null || rxPos == null) {
|
||||
""
|
||||
} else {
|
||||
positionToMeter(
|
||||
localNodePosition!!,
|
||||
position
|
||||
).roundToInt().toString()
|
||||
}
|
||||
|
||||
val hopLimit = proto.hopLimit
|
||||
|
||||
val payload = when {
|
||||
proto.decoded.portnumValue != Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> "<${proto.decoded.portnum}>"
|
||||
proto.hasDecoded() -> "\"" + proto.decoded.payload.toStringUtf8()
|
||||
.replace("\"", "\\\"") + "\""
|
||||
proto.hasEncrypted() -> "${proto.encrypted.size()} encrypted bytes"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
// date,time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx snr,distance,hop limit,payload
|
||||
writer.appendLine("$rxDateTime,$rxFrom,$senderName,$senderLat,$senderLong,$rxLat,$rxLong,$rxAlt,$rxSnr,$dist,$hopLimit,$payload")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun writeToUri(uri: Uri, crossinline block: suspend (BufferedWriter) -> Unit) {
|
||||
withContext(Dispatchers.IO) {
|
||||
app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
|
||||
FileWriter(parcelFileDescriptor.fileDescriptor).use { fileWriter ->
|
||||
BufferedWriter(fileWriter).use { writer ->
|
||||
block.invoke(writer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.geeksville.mesh.service
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Service
|
||||
import android.companion.CompanionDeviceManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
|
|
@ -285,6 +286,22 @@ class RadioInterfaceService : Service(), Logging {
|
|||
|
||||
debug("Setting bonded device to ${address.anonymize}")
|
||||
|
||||
// We only keep an association to one device at a time... (move to BluetoothInterface?)
|
||||
if (BluetoothInterface.hasCompanionDeviceApi(this)) {
|
||||
if (address != null) {
|
||||
val deviceManager = getSystemService(CompanionDeviceManager::class.java)
|
||||
val c = address[0]
|
||||
val rest = address.substring(1)
|
||||
|
||||
deviceManager.associations.forEach { old ->
|
||||
if (rest != old) {
|
||||
debug("Forgetting old BLE association ${old.anonymize}")
|
||||
deviceManager.disassociate(old)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getPrefs(this).edit(commit = true) {
|
||||
if (address == null)
|
||||
this.remove(DEVADDR_KEY)
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.protobuf.ByteString
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import java.net.MalformedURLException
|
||||
import java.security.SecureRandom
|
||||
|
||||
|
||||
|
|
@ -320,18 +319,8 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
|||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
|
||||
if (result != null) {
|
||||
if (result.contents == null) {
|
||||
Snackbar.make(binding.scanButton, R.string.channel_invalid, Snackbar.LENGTH_LONG).show()
|
||||
} else {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = ChannelSet(Uri.parse(result.contents)).getChannelUrl(false)
|
||||
startActivity(intent)
|
||||
} catch (ex: ActivityNotFoundException) {
|
||||
Snackbar.make(binding.scanButton, R.string.channel_invalid, Snackbar.LENGTH_LONG).show()
|
||||
} catch (ex: MalformedURLException) {
|
||||
Snackbar.make(binding.scanButton, R.string.channel_invalid, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
if (result.contents != null) {
|
||||
((requireActivity() as MainActivity).perhapsChangeChannel(Uri.parse(result.contents)))
|
||||
}
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M14.99,4.59V5c0,1.1 -0.9,2 -2,2h-2v2c0,0.55 -0.45,1 -1,1h-2v2h6c0.55,0 1,0.45 1,1v3h1c0.89,0 1.64,0.59 1.9,1.4C19.19,15.98 20,14.08 20,12c0,-3.35 -2.08,-6.23 -5.01,-7.41zM8.99,16v-1l-4.78,-4.78C4.08,10.79 4,11.39 4,12c0,4.07 3.06,7.43 6.99,7.93V18c-1.1,0 -2,-0.9 -2,-2z"
|
||||
android:strokeAlpha="0.3"
|
||||
android:fillAlpha="0.3"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10.99,19.93C7.06,19.43 4,16.07 4,12c0,-0.61 0.08,-1.21 0.21,-1.78L8.99,15v1c0,1.1 0.9,2 2,2v1.93zM17.89,17.4c-0.26,-0.81 -1,-1.4 -1.9,-1.4h-1v-3c0,-0.55 -0.45,-1 -1,-1h-6v-2h2c0.55,0 1,-0.45 1,-1L10.99,7h2c1.1,0 2,-0.9 2,-2v-0.41C17.92,5.77 20,8.65 20,12c0,2.08 -0.81,3.98 -2.11,5.4z"/>
|
||||
</vector>
|
||||
|
|
@ -100,7 +100,6 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:text="@string/reset"
|
||||
app:icon="@drawable/ic_twotone_public_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/bottomButtonsGuideline"
|
||||
app:layout_constraintEnd_toStartOf="@id/editableCheckbox" />
|
||||
|
||||
|
|
|
|||
|
|
@ -73,4 +73,18 @@
|
|||
<item name="materialThemeOverlay">@style/MyThemeOverlay_Toolbar</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
|
||||
// Set the splash screen background, animated icon, and animation duration.
|
||||
<item name="windowSplashScreenBackground">@color/selectedColor</item>
|
||||
|
||||
// Use windowSplashScreenAnimatedIcon to add either a drawable or an
|
||||
// animated drawable. One of these is required.
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher2_foreground</item>
|
||||
<item name="windowSplashScreenAnimationDuration">1000</item> # Required for
|
||||
# animated icons
|
||||
|
||||
// Set the theme of the Activity that directly follows your splash screen.
|
||||
<item name="postSplashScreenTheme">@style/AppTheme</item> # Required.
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue