mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Compare commits
42 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8004e11dfe | ||
|
|
0c9dc72c4c | ||
|
|
8a214669ba | ||
|
|
113a4ffcdc | ||
|
|
cfdeb974b1 | ||
|
|
501b05ca59 | ||
|
|
60f7f98748 | ||
|
|
5f131da50d | ||
|
|
f20368833e | ||
|
|
51535f4e4f | ||
|
|
d5642d0a03 | ||
|
|
967f777374 | ||
|
|
104b044572 | ||
|
|
265c6a7bf7 | ||
|
|
b84dd3577f | ||
|
|
9682c41f2b | ||
|
|
ce90db8258 | ||
|
|
9506cfb9c4 | ||
|
|
708c502eb9 | ||
|
|
d4d39b282a | ||
|
|
b873f2928d | ||
|
|
de2b87557b | ||
|
|
8b13961b83 | ||
|
|
276f6dc758 | ||
|
|
496cd230dd | ||
|
|
22c370d231 | ||
|
|
1f3d817e3b | ||
|
|
bf8f4f1660 | ||
|
|
6258780106 | ||
|
|
67794f0433 | ||
|
|
4092fc5c7f | ||
|
|
91b2767634 | ||
|
|
52f7a862b3 | ||
|
|
ede48be4f3 | ||
|
|
598ec54cf3 | ||
|
|
49188adc36 | ||
|
|
c0fe9213f1 | ||
|
|
1294eee8e3 | ||
|
|
5659725b96 | ||
|
|
6ad2b1814f | ||
|
|
01f8154189 | ||
|
|
7395cc5583 |
60 changed files with 1587 additions and 733 deletions
7
.github/workflows/android.yml
vendored
7
.github/workflows/android.yml
vendored
|
|
@ -19,11 +19,12 @@ jobs:
|
||||||
- name: Load secrets
|
- name: Load secrets
|
||||||
run: |
|
run: |
|
||||||
rm ./app/src/main/res/values/mapbox-token.xml
|
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
|
echo -e "<resources>\n <string name=\"mapbox_access_token\">$MAPBOX_ACCESS_TOKEN</string>\n</resources>" > ./app/src/main/res/values/mapbox-token.xml
|
||||||
mkdir -p ~/.gradle
|
mkdir -p ~/.gradle
|
||||||
echo "MAPBOX_DOWNLOADS_TOKEN=$MAPBOXTOKEN" >>~/.gradle/gradle.properties
|
echo "MAPBOX_DOWNLOADS_TOKEN=$MAPBOX_DOWNLOADS_TOKEN" >>~/.gradle/gradle.properties
|
||||||
env:
|
env:
|
||||||
MAPBOXTOKEN: ${{ secrets.MAPBOXTOKEN }}
|
MAPBOX_ACCESS_TOKEN: ${{ secrets.MAPBOX_ACCESS_TOKEN }}
|
||||||
|
MAPBOX_DOWNLOADS_TOKEN: ${{ secrets.MAPBOX_DOWNLOADS_TOKEN }}
|
||||||
|
|
||||||
- name: Mock files for CI
|
- name: Mock files for CI
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
|
|
@ -22,14 +22,15 @@ jobs:
|
||||||
rm ./app/google-services.json
|
rm ./app/google-services.json
|
||||||
echo $GSERVICES > ./app/google-services.json
|
echo $GSERVICES > ./app/google-services.json
|
||||||
rm ./app/src/main/res/values/mapbox-token.xml
|
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
|
echo -e "<resources>\n <string name=\"mapbox_access_token\">$MAPBOX_ACCESS_TOKEN</string>\n</resources>" > ./app/src/main/res/values/mapbox-token.xml
|
||||||
mkdir -p ~/.gradle
|
mkdir -p ~/.gradle
|
||||||
echo "MAPBOX_DOWNLOADS_TOKEN=$MAPBOXTOKEN" >> ~/.gradle/gradle.properties
|
echo "MAPBOX_DOWNLOADS_TOKEN=$MAPBOX_DOWNLOADS_TOKEN" >> ~/.gradle/gradle.properties
|
||||||
echo $KEYSTORE | base64 -di > ./app/$KEYSTORE_FILENAME
|
echo $KEYSTORE | base64 -di > ./app/$KEYSTORE_FILENAME
|
||||||
echo "$KEYSTORE_PROPERTIES" > ./keystore.properties
|
echo "$KEYSTORE_PROPERTIES" > ./keystore.properties
|
||||||
env:
|
env:
|
||||||
GSERVICES: ${{ secrets.GSERVICES }}
|
GSERVICES: ${{ secrets.GSERVICES }}
|
||||||
MAPBOXTOKEN: ${{ secrets.MAPBOXTOKEN }}
|
MAPBOX_ACCESS_TOKEN: ${{ secrets.MAPBOX_ACCESS_TOKEN }}
|
||||||
|
MAPBOX_DOWNLOADS_TOKEN: ${{ secrets.MAPBOX_DOWNLOADS_TOKEN }}
|
||||||
KEYSTORE: ${{ secrets.KEYSTORE }}
|
KEYSTORE: ${{ secrets.KEYSTORE }}
|
||||||
KEYSTORE_FILENAME: ${{ secrets.KEYSTORE_FILENAME }}
|
KEYSTORE_FILENAME: ${{ secrets.KEYSTORE_FILENAME }}
|
||||||
KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }}
|
KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }}
|
||||||
|
|
@ -47,6 +48,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
repository: meshtastic/Meshtastic-device
|
repository: meshtastic/Meshtastic-device
|
||||||
releases-only: true
|
releases-only: true
|
||||||
|
prefix: 'v1.2.'
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Create version strings
|
- name: Create version strings
|
||||||
|
|
@ -72,7 +74,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
rm -rf ./app/src/main/assets/firmware
|
rm -rf ./app/src/main/assets/firmware
|
||||||
mkdir -p ./app/src/main/assets/firmware
|
mkdir -p ./app/src/main/assets/firmware
|
||||||
unzip -qq ./firmware.zip 'spiffs-*.bin' 'firmware-heltec*.bin' 'firmware-tbeam*.bin' 'firmware-tlora*.bin' -d ./app/src/main/assets/firmware
|
unzip -qq ./firmware.zip 'spiffs-*.bin' 'firmware-heltec*.bin' 'firmware-tbeam*.bin' 'firmware-tlora*.bin' 'firmware-nano*.bin' -d ./app/src/main/assets/firmware
|
||||||
rm ./firmware.zip
|
rm ./firmware.zip
|
||||||
|
|
||||||
- name: Validate Gradle wrapper
|
- name: Validate Gradle wrapper
|
||||||
|
|
|
||||||
1
.gitmodules
vendored
1
.gitmodules
vendored
|
|
@ -1,6 +1,7 @@
|
||||||
[submodule "app/src/main/proto"]
|
[submodule "app/src/main/proto"]
|
||||||
path = app/src/main/proto
|
path = app/src/main/proto
|
||||||
url = https://github.com/meshtastic/Meshtastic-protobufs.git
|
url = https://github.com/meshtastic/Meshtastic-protobufs.git
|
||||||
|
branch = 1.2-legacy
|
||||||
[submodule "geeksville-androidlib"]
|
[submodule "geeksville-androidlib"]
|
||||||
path = geeksville-androidlib
|
path = geeksville-androidlib
|
||||||
url = https://github.com/meshtastic/geeksville-androidlib.git
|
url = https://github.com/meshtastic/geeksville-androidlib.git
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,8 @@ android {
|
||||||
applicationId "com.geeksville.mesh"
|
applicationId "com.geeksville.mesh"
|
||||||
minSdkVersion 21 // The oldest emulator image I have tried is 22 (though 21 probably works)
|
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
|
targetSdkVersion 30 // 30 can't work until an explicit location permissions dialog is added
|
||||||
versionCode 20258 // format is Mmmss (where M is 1+the numeric major number
|
versionCode 20266 // format is Mmmss (where M is 1+the numeric major number
|
||||||
versionName "1.2.58"
|
versionName "1.2.66"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
// per https://developer.android.com/studio/write/vector-asset-studio
|
// per https://developer.android.com/studio/write/vector-asset-studio
|
||||||
|
|
@ -122,7 +122,7 @@ protobuf {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
def room_version = '2.4.1'
|
def room_version = '2.4.2'
|
||||||
|
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||||
|
|
@ -134,7 +134,7 @@ dependencies {
|
||||||
implementation 'com.google.android.material:material:1.5.0'
|
implementation 'com.google.android.material:material:1.5.0'
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.0.0'
|
implementation 'androidx.viewpager2:viewpager2:1.0.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0'
|
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
|
||||||
implementation "androidx.room:room-runtime:$room_version"
|
implementation "androidx.room:room-runtime:$room_version"
|
||||||
implementation "com.google.dagger:hilt-android:$hilt_version"
|
implementation "com.google.dagger:hilt-android:$hilt_version"
|
||||||
kapt "androidx.room:room-compiler:$room_version"
|
kapt "androidx.room:room-compiler:$room_version"
|
||||||
|
|
@ -175,9 +175,10 @@ dependencies {
|
||||||
|
|
||||||
// location services
|
// location services
|
||||||
implementation 'com.google.android.gms:play-services-location:19.0.1'
|
implementation 'com.google.android.gms:play-services-location:19.0.1'
|
||||||
|
|
||||||
// For Google Sign-In (owner name accesss)
|
// For Google Sign-In (owner name accesss)
|
||||||
implementation 'com.google.android.gms:play-services-auth:20.1.0'
|
implementation 'com.google.android.gms:play-services-auth:20.1.0'
|
||||||
|
// ML Kit barcode scanning
|
||||||
|
implementation 'com.google.android.gms:play-services-code-scanner:16.0.0-beta1'
|
||||||
|
|
||||||
// Add the Firebase SDK for Crashlytics.
|
// Add the Firebase SDK for Crashlytics.
|
||||||
implementation 'com.google.firebase:firebase-crashlytics:18.2.6'
|
implementation 'com.google.firebase:firebase-crashlytics:18.2.6'
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,9 @@
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="firebase_analytics_collection_enabled"
|
android:name="firebase_analytics_collection_enabled"
|
||||||
android:value="false" />
|
android:value="false" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.mlkit.vision.DEPENDENCIES"
|
||||||
|
android:value="barcode_ui"/>
|
||||||
|
|
||||||
<!-- we need bind job service for oreo -->
|
<!-- we need bind job service for oreo -->
|
||||||
<service
|
<service
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,7 @@ interface IMeshService {
|
||||||
*/
|
*/
|
||||||
void send(inout DataPacket packet);
|
void send(inout DataPacket packet);
|
||||||
|
|
||||||
|
void deleteMessage(int packetId);
|
||||||
void delete(int position);
|
|
||||||
|
|
||||||
void deleteAllMessages();
|
void deleteAllMessages();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ package com.geeksville.mesh
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
|
@ -15,4 +18,14 @@ object ApplicationModule {
|
||||||
fun provideSharedPreferences(application: Application): SharedPreferences {
|
fun provideSharedPreferences(application: Application): SharedPreferences {
|
||||||
return application.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE)
|
return application.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideProcessLifecycleOwner(): LifecycleOwner {
|
||||||
|
return ProcessLifecycleOwner.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideProcessLifecycle(processLifecycleOwner: LifecycleOwner): Lifecycle {
|
||||||
|
return processLifecycleOwner.lifecycle
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.geeksville.mesh
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around `Dispatchers` to allow for easier testing when using dispatchers
|
||||||
|
* in injected classes.
|
||||||
|
*/
|
||||||
|
class CoroutineDispatchers @Inject constructor() {
|
||||||
|
val main = Dispatchers.Main
|
||||||
|
val mainImmediate = Dispatchers.Main.immediate
|
||||||
|
val default = Dispatchers.Default
|
||||||
|
val io = Dispatchers.IO
|
||||||
|
}
|
||||||
|
|
@ -29,7 +29,9 @@ data class DataPacket(
|
||||||
var time: Long = System.currentTimeMillis(), // msecs since 1970
|
var time: Long = System.currentTimeMillis(), // msecs since 1970
|
||||||
var id: Int = 0, // 0 means unassigned
|
var id: Int = 0, // 0 means unassigned
|
||||||
var status: MessageStatus? = MessageStatus.UNKNOWN,
|
var status: MessageStatus? = MessageStatus.UNKNOWN,
|
||||||
var hopLimit: Int = 0
|
var hopLimit: Int = 0,
|
||||||
|
var channel: Int = 0, // channel index
|
||||||
|
var delayed: Int = 0 // S&F MeshProtos.MeshPacket.Delayed.(...)_VALUE
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -64,6 +66,8 @@ data class DataPacket(
|
||||||
parcel.readLong(),
|
parcel.readLong(),
|
||||||
parcel.readInt(),
|
parcel.readInt(),
|
||||||
parcel.readParcelable(MessageStatus::class.java.classLoader),
|
parcel.readParcelable(MessageStatus::class.java.classLoader),
|
||||||
|
parcel.readInt(),
|
||||||
|
parcel.readInt(),
|
||||||
parcel.readInt()
|
parcel.readInt()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -75,6 +79,7 @@ data class DataPacket(
|
||||||
|
|
||||||
if (from != other.from) return false
|
if (from != other.from) return false
|
||||||
if (to != other.to) return false
|
if (to != other.to) return false
|
||||||
|
if (channel != other.channel) return false
|
||||||
if (time != other.time) return false
|
if (time != other.time) return false
|
||||||
if (id != other.id) return false
|
if (id != other.id) return false
|
||||||
if (dataType != other.dataType) return false
|
if (dataType != other.dataType) return false
|
||||||
|
|
@ -94,6 +99,8 @@ data class DataPacket(
|
||||||
result = 31 * result + bytes!!.contentHashCode()
|
result = 31 * result + bytes!!.contentHashCode()
|
||||||
result = 31 * result + status.hashCode()
|
result = 31 * result + status.hashCode()
|
||||||
result = 31 * result + hopLimit
|
result = 31 * result + hopLimit
|
||||||
|
result = 31 * result + channel
|
||||||
|
result = 31 * result + delayed
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,6 +113,8 @@ data class DataPacket(
|
||||||
parcel.writeInt(id)
|
parcel.writeInt(id)
|
||||||
parcel.writeParcelable(status, flags)
|
parcel.writeParcelable(status, flags)
|
||||||
parcel.writeInt(hopLimit)
|
parcel.writeInt(hopLimit)
|
||||||
|
parcel.writeInt(channel)
|
||||||
|
parcel.writeInt(delayed)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
override fun describeContents(): Int {
|
||||||
|
|
@ -122,6 +131,8 @@ data class DataPacket(
|
||||||
id = parcel.readInt()
|
id = parcel.readInt()
|
||||||
status = parcel.readParcelable(MessageStatus::class.java.classLoader)
|
status = parcel.readParcelable(MessageStatus::class.java.classLoader)
|
||||||
hopLimit = parcel.readInt()
|
hopLimit = parcel.readInt()
|
||||||
|
channel = parcel.readInt()
|
||||||
|
delayed = parcel.readInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object CREATOR : Parcelable.Creator<DataPacket> {
|
companion object CREATOR : Parcelable.Creator<DataPacket> {
|
||||||
|
|
@ -145,7 +156,7 @@ data class DataPacket(
|
||||||
override fun newArray(size: Int): Array<DataPacket?> {
|
override fun newArray(size: Int): Array<DataPacket?> {
|
||||||
return arrayOfNulls(size)
|
return arrayOfNulls(size)
|
||||||
}
|
}
|
||||||
val utf8 = Charset.forName("UTF-8")
|
val utf8: Charset = Charset.forName("UTF-8")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.bluetooth.BluetoothManager
|
|
||||||
import android.content.*
|
import android.content.*
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
|
@ -14,6 +13,7 @@ import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
|
|
@ -40,6 +40,7 @@ import com.geeksville.android.ServiceClient
|
||||||
import com.geeksville.concurrent.handledLaunch
|
import com.geeksville.concurrent.handledLaunch
|
||||||
import com.geeksville.mesh.android.*
|
import com.geeksville.mesh.android.*
|
||||||
import com.geeksville.mesh.databinding.ActivityMainBinding
|
import com.geeksville.mesh.databinding.ActivityMainBinding
|
||||||
|
import com.geeksville.mesh.model.BluetoothViewModel
|
||||||
import com.geeksville.mesh.model.ChannelSet
|
import com.geeksville.mesh.model.ChannelSet
|
||||||
import com.geeksville.mesh.model.DeviceVersion
|
import com.geeksville.mesh.model.DeviceVersion
|
||||||
import com.geeksville.mesh.model.UIViewModel
|
import com.geeksville.mesh.model.UIViewModel
|
||||||
|
|
@ -125,7 +126,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||||
const val REQUEST_ENABLE_BT = 10
|
const val REQUEST_ENABLE_BT = 10
|
||||||
const val DID_REQUEST_PERM = 11
|
const val DID_REQUEST_PERM = 11
|
||||||
const val RC_SIGN_IN = 12 // google signin completed
|
const val RC_SIGN_IN = 12 // google signin completed
|
||||||
const val SELECT_DEVICE_REQUEST_CODE = 13
|
// const val SELECT_DEVICE_REQUEST_CODE = 13
|
||||||
const val CREATE_CSV_FILE = 14
|
const val CREATE_CSV_FILE = 14
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,11 +135,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||||
// Used to schedule a coroutine in the GUI thread
|
// Used to schedule a coroutine in the GUI thread
|
||||||
private val mainScope = CoroutineScope(Dispatchers.Main + Job())
|
private val mainScope = CoroutineScope(Dispatchers.Main + Job())
|
||||||
|
|
||||||
private val bluetoothAdapter: BluetoothAdapter? by lazy(LazyThreadSafetyMode.NONE) {
|
private val bluetoothViewModel: BluetoothViewModel by viewModels()
|
||||||
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
|
||||||
bluetoothManager.adapter
|
|
||||||
}
|
|
||||||
|
|
||||||
val model: UIViewModel by viewModels()
|
val model: UIViewModel by viewModels()
|
||||||
|
|
||||||
data class TabInfo(val text: String, val icon: Int, val content: Fragment)
|
data class TabInfo(val text: String, val icon: Int, val content: Fragment)
|
||||||
|
|
@ -148,7 +145,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||||
TabInfo(
|
TabInfo(
|
||||||
"Messages",
|
"Messages",
|
||||||
R.drawable.ic_twotone_message_24,
|
R.drawable.ic_twotone_message_24,
|
||||||
MessagesFragment()
|
ContactsFragment()
|
||||||
),
|
),
|
||||||
TabInfo(
|
TabInfo(
|
||||||
"Users",
|
"Users",
|
||||||
|
|
@ -187,28 +184,6 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val btStateReceiver = BluetoothStateReceiver {
|
|
||||||
updateBluetoothEnabled()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Don't tell our app we have bluetooth until we have bluetooth _and_ location access
|
|
||||||
*/
|
|
||||||
private fun updateBluetoothEnabled() {
|
|
||||||
var enabled = false // assume failure
|
|
||||||
|
|
||||||
if (hasConnectPermission()) {
|
|
||||||
/// ask the adapter if we have access
|
|
||||||
bluetoothAdapter?.apply {
|
|
||||||
enabled = isEnabled
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
errormsg("Still missing needed bluetooth permissions")
|
|
||||||
|
|
||||||
debug("Detected our bluetooth access=$enabled")
|
|
||||||
model.bluetoothEnabled.value = enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get the minimum permissions our app needs to run correctly
|
/** Get the minimum permissions our app needs to run correctly
|
||||||
*/
|
*/
|
||||||
private fun getMinimumPermissions(): List<String> {
|
private fun getMinimumPermissions(): List<String> {
|
||||||
|
|
@ -381,7 +356,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBluetoothEnabled()
|
bluetoothViewModel.permissionsUpdated()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -445,12 +420,6 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||||
/// Set theme
|
/// Set theme
|
||||||
setUITheme(prefs)
|
setUITheme(prefs)
|
||||||
|
|
||||||
/// Set initial bluetooth state
|
|
||||||
updateBluetoothEnabled()
|
|
||||||
|
|
||||||
/// We now want to be informed of bluetooth state
|
|
||||||
registerReceiver(btStateReceiver, btStateReceiver.intentFilter)
|
|
||||||
|
|
||||||
/* not yet working
|
/* not yet working
|
||||||
// Configure sign-in to request the user's ID, email address, and basic
|
// Configure sign-in to request the user's ID, email address, and basic
|
||||||
// profile. ID and basic profile are included in DEFAULT_SIGN_IN.
|
// profile. ID and basic profile are included in DEFAULT_SIGN_IN.
|
||||||
|
|
@ -545,7 +514,6 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||||
requestedChannelUrl = appLinkData
|
requestedChannelUrl = appLinkData
|
||||||
|
|
||||||
// if the device is connected already, process it now
|
// if the device is connected already, process it now
|
||||||
if (model.isConnected.value == MeshService.ConnectionState.CONNECTED)
|
|
||||||
perhapsChangeChannel()
|
perhapsChangeChannel()
|
||||||
|
|
||||||
// We now wait for the device to connect, once connected, we ask the user if they want to switch to the new channel
|
// We now wait for the device to connect, once connected, we ask the user if they want to switch to the new channel
|
||||||
|
|
@ -569,7 +537,6 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
unregisterReceiver(btStateReceiver)
|
|
||||||
unregisterMeshReceiver()
|
unregisterMeshReceiver()
|
||||||
mainScope.cancel("Activity going away")
|
mainScope.cancel("Activity going away")
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
@ -765,16 +732,16 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun perhapsChangeChannel(url: Uri? = requestedChannelUrl) {
|
private fun perhapsChangeChannel(url: Uri? = requestedChannelUrl) {
|
||||||
// If the is opening a channel URL, handle it now
|
// if the device is connected already, process it now
|
||||||
if (url != null) {
|
if (url != null && model.isConnected.value == MeshService.ConnectionState.CONNECTED) {
|
||||||
|
requestedChannelUrl = null
|
||||||
try {
|
try {
|
||||||
val channels = ChannelSet(url)
|
val channels = ChannelSet(url)
|
||||||
val primary = channels.primaryChannel
|
val primary = channels.primaryChannel
|
||||||
if (primary == null)
|
if (primary == null)
|
||||||
showSnackbar(R.string.channel_invalid)
|
showSnackbar(R.string.channel_invalid)
|
||||||
else {
|
else {
|
||||||
requestedChannelUrl = null
|
|
||||||
|
|
||||||
MaterialAlertDialogBuilder(this)
|
MaterialAlertDialogBuilder(this)
|
||||||
.setTitle(R.string.new_channel_rcvd)
|
.setTitle(R.string.new_channel_rcvd)
|
||||||
|
|
@ -1003,18 +970,27 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
|
|
||||||
|
bluetoothViewModel.enabled.observe(this) { enabled ->
|
||||||
|
if (!enabled) {
|
||||||
// Ask to start bluetooth if no USB devices are visible
|
// Ask to start bluetooth if no USB devices are visible
|
||||||
val hasUSB = SerialInterface.findDrivers(this).isNotEmpty()
|
val hasUSB = SerialInterface.findDrivers(this).isNotEmpty()
|
||||||
if (!isInTestLab && !hasUSB) {
|
if (!isInTestLab && !hasUSB) {
|
||||||
if (hasConnectPermission()) {
|
if (hasConnectPermission()) {
|
||||||
bluetoothAdapter?.let {
|
|
||||||
if (!it.isEnabled) {
|
|
||||||
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
|
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
|
||||||
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
|
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
|
||||||
}
|
|
||||||
}
|
|
||||||
} else requestPermission()
|
} else requestPermission()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call perhapsChangeChannel() whenever [changeChannelUrl] updates with a non-null value
|
||||||
|
model.requestChannelUrl.observe(this) { url ->
|
||||||
|
url?.let {
|
||||||
|
requestedChannelUrl = url
|
||||||
|
model.clearRequestChannelUrl()
|
||||||
|
perhapsChangeChannel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
bindMeshService()
|
bindMeshService()
|
||||||
|
|
@ -1043,7 +1019,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||||
}
|
}
|
||||||
|
|
||||||
val handler: Handler by lazy {
|
val handler: Handler by lazy {
|
||||||
Handler(mainLooper)
|
Handler(Looper.getMainLooper())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,43 @@
|
||||||
package com.geeksville.mesh.android
|
package com.geeksville.mesh.android
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.bluetooth.BluetoothManager
|
import android.bluetooth.BluetoothManager
|
||||||
|
import android.companion.CompanionDeviceManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.hardware.usb.UsbManager
|
import android.hardware.usb.UsbManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.geeksville.mesh.service.BluetoothInterface
|
import com.geeksville.android.GeeksvilleApplication
|
||||||
|
import com.geeksville.mesh.MainActivity
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return null on platforms without a BlueTooth driver (i.e. the emulator)
|
* @return null on platforms without a BlueTooth driver (i.e. the emulator)
|
||||||
*/
|
*/
|
||||||
val Context.bluetoothManager: BluetoothManager? get() = getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager?
|
val Context.bluetoothManager: BluetoothManager? get() = getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager?
|
||||||
|
|
||||||
|
val Context.deviceManager: CompanionDeviceManager?
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
get() {
|
||||||
|
val activity: MainActivity? = GeeksvilleApplication.currentActivity as MainActivity?
|
||||||
|
return if (hasCompanionDeviceApi()) activity?.getSystemService(Context.COMPANION_DEVICE_SERVICE) as? CompanionDeviceManager?
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
val Context.usbManager: UsbManager get() = requireNotNull(getSystemService(Context.USB_SERVICE) as? UsbManager?) { "USB_SERVICE is not available"}
|
val Context.usbManager: UsbManager get() = requireNotNull(getSystemService(Context.USB_SERVICE) as? UsbManager?) { "USB_SERVICE is not available"}
|
||||||
|
|
||||||
val Context.notificationManager: NotificationManager get() = requireNotNull(getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager?)
|
val Context.notificationManager: NotificationManager get() = requireNotNull(getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager?)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if CompanionDeviceManager API is present
|
||||||
|
*/
|
||||||
|
fun Context.hasCompanionDeviceApi(): Boolean =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||||
|
packageManager.hasSystemFeature(PackageManager.FEATURE_COMPANION_DEVICE_SETUP)
|
||||||
|
else false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* return a list of the permissions we don't have
|
* return a list of the permissions we don't have
|
||||||
*/
|
*/
|
||||||
|
|
@ -62,7 +81,7 @@ fun Context.getScanPermissions(): List<String> {
|
||||||
perms.add(Manifest.permission.BLUETOOTH_ADMIN)
|
perms.add(Manifest.permission.BLUETOOTH_ADMIN)
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
if (!BluetoothInterface.hasCompanionDeviceApi(this)) {
|
if (!hasCompanionDeviceApi()) {
|
||||||
perms.add(Manifest.permission.ACCESS_FINE_LOCATION)
|
perms.add(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
perms.add(Manifest.permission.BLUETOOTH_ADMIN)
|
perms.add(Manifest.permission.BLUETOOTH_ADMIN)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Laz
|
||||||
packetDao.getAllPacket(MAX_ITEMS)
|
packetDao.getAllPacket(MAX_ITEMS)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getAllPacketsInReceiveOrder(): Flow<List<Packet>> = withContext(Dispatchers.IO) {
|
suspend fun getAllPacketsInReceiveOrder(maxItems: Int = MAX_ITEMS): Flow<List<Packet>> = withContext(Dispatchers.IO) {
|
||||||
packetDao.getAllPacketsInReceiveOrder(MAX_ITEMS)
|
packetDao.getAllPacketsInReceiveOrder(maxItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun insert(packet: Packet) = withContext(Dispatchers.IO) {
|
suspend fun insert(packet: Packet) = withContext(Dispatchers.IO) {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ data class Packet(@PrimaryKey val uuid: String,
|
||||||
@ColumnInfo(name = "message") val raw_message: String
|
@ColumnInfo(name = "message") val raw_message: String
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val proto: MeshProtos.MeshPacket?
|
val meshPacket: MeshProtos.MeshPacket?
|
||||||
get() {
|
get() {
|
||||||
if (message_type == "packet") {
|
if (message_type == "packet") {
|
||||||
val builder = MeshProtos.MeshPacket.newBuilder()
|
val builder = MeshProtos.MeshPacket.newBuilder()
|
||||||
|
|
@ -28,13 +28,27 @@ data class Packet(@PrimaryKey val uuid: String,
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val nodeInfo: MeshProtos.NodeInfo?
|
||||||
|
get() {
|
||||||
|
if (message_type == "NodeInfo") {
|
||||||
|
val builder = MeshProtos.NodeInfo.newBuilder()
|
||||||
|
try {
|
||||||
|
TextFormat.getParser().merge(raw_message, builder)
|
||||||
|
return builder.build()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
val position: MeshProtos.Position?
|
val position: MeshProtos.Position?
|
||||||
get() {
|
get() {
|
||||||
return proto?.run {
|
return meshPacket?.run {
|
||||||
if (hasDecoded() && decoded.portnumValue == Portnums.PortNum.POSITION_APP_VALUE) {
|
if (hasDecoded() && decoded.portnumValue == Portnums.PortNum.POSITION_APP_VALUE) {
|
||||||
return MeshProtos.Position.parseFrom(decoded.payload)
|
return MeshProtos.Position.parseFrom(decoded.payload)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
} ?: nodeInfo?.position
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.geeksville.mesh.model
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.asLiveData
|
||||||
|
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thin view model which adapts the view layer to the `BluetoothRepository`.
|
||||||
|
*/
|
||||||
|
@HiltViewModel
|
||||||
|
class BluetoothViewModel @Inject constructor(
|
||||||
|
private val bluetoothRepository: BluetoothRepository,
|
||||||
|
) : ViewModel() {
|
||||||
|
/**
|
||||||
|
* Called when permissions have been updated. This causes an explicit refresh of the
|
||||||
|
* bluetooth state.
|
||||||
|
*/
|
||||||
|
fun permissionsUpdated() = bluetoothRepository.refreshState()
|
||||||
|
|
||||||
|
val enabled = bluetoothRepository.state.map { it.enabled }.asLiveData()
|
||||||
|
}
|
||||||
|
|
@ -30,10 +30,35 @@ class MessagesState(private val ui: UIViewModel) : Logging {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var contactsList = emptyMap<String?, DataPacket>().toMutableMap()
|
||||||
|
val contacts = object : MutableLiveData<MutableMap<String?, DataPacket>>() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emptyDataPacket(to: String? = DataPacket.ID_BROADCAST): DataPacket {
|
||||||
|
return DataPacket(to, null, 1, DataPacket.ID_LOCAL, 0L)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map each contactId to last DataPacket message sent or received
|
||||||
|
// Broadcast: it.to == DataPacket.ID_BROADCAST; Direct Messages: it.to != DataPacket.ID_BROADCAST
|
||||||
|
private fun buildContacts() {
|
||||||
|
contactsList = messagesList.associateBy {
|
||||||
|
if (it.from == DataPacket.ID_LOCAL || it.to == DataPacket.ID_BROADCAST)
|
||||||
|
it.to else it.from
|
||||||
|
}.toMutableMap()
|
||||||
|
|
||||||
|
val all = DataPacket.ID_BROADCAST // always show contacts, even when empty
|
||||||
|
if (contactsList[all] == null)
|
||||||
|
contactsList[all] = emptyDataPacket()
|
||||||
|
|
||||||
|
contacts.value = contactsList
|
||||||
|
}
|
||||||
|
|
||||||
fun setMessages(m: List<DataPacket>) {
|
fun setMessages(m: List<DataPacket>) {
|
||||||
messagesList.clear()
|
messagesList.clear()
|
||||||
messagesList.addAll(m)
|
messagesList.addAll(m)
|
||||||
messages.value = messagesList
|
messages.value = messagesList
|
||||||
|
buildContacts()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// add a message our GUI list of past msgs
|
/// add a message our GUI list of past msgs
|
||||||
|
|
@ -44,6 +69,7 @@ class MessagesState(private val ui: UIViewModel) : Logging {
|
||||||
messagesList.add(m)
|
messagesList.add(m)
|
||||||
|
|
||||||
messages.value = messagesList
|
messages.value = messagesList
|
||||||
|
buildContacts()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeMessage(m: DataPacket) {
|
fun removeMessage(m: DataPacket) {
|
||||||
|
|
@ -51,6 +77,7 @@ class MessagesState(private val ui: UIViewModel) : Logging {
|
||||||
|
|
||||||
messagesList.remove(m)
|
messagesList.remove(m)
|
||||||
messages.value = messagesList
|
messages.value = messagesList
|
||||||
|
buildContacts()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeAllMessages() {
|
private fun removeAllMessages() {
|
||||||
|
|
@ -58,6 +85,7 @@ class MessagesState(private val ui: UIViewModel) : Logging {
|
||||||
|
|
||||||
messagesList.clear()
|
messagesList.clear()
|
||||||
messages.value = messagesList
|
messages.value = messagesList
|
||||||
|
buildContacts()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateStatus(id: Int, status: MessageStatus) {
|
fun updateStatus(id: Int, status: MessageStatus) {
|
||||||
|
|
@ -95,12 +123,12 @@ class MessagesState(private val ui: UIViewModel) : Logging {
|
||||||
addMessage(p)
|
addMessage(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteMessage(packet: DataPacket, position: Int) {
|
fun deleteMessage(packet: DataPacket) {
|
||||||
val service = ui.meshService
|
val service = ui.meshService
|
||||||
|
|
||||||
if (service != null) {
|
if (service != null) {
|
||||||
try {
|
try {
|
||||||
service.delete(position)
|
service.deleteMessage(packet.id)
|
||||||
} catch (ex: RemoteException) {
|
} catch (ex: RemoteException) {
|
||||||
packet.errorMessage = "Error: ${ex.message}"
|
packet.errorMessage = "Error: ${ex.message}"
|
||||||
}
|
}
|
||||||
|
|
@ -116,7 +144,7 @@ class MessagesState(private val ui: UIViewModel) : Logging {
|
||||||
try {
|
try {
|
||||||
service.deleteAllMessages()
|
service.deleteAllMessages()
|
||||||
} catch (ex: RemoteException) {
|
} catch (ex: RemoteException) {
|
||||||
|
errormsg("Error: ${ex.message}")
|
||||||
}
|
}
|
||||||
removeAllMessages()
|
removeAllMessages()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import android.net.Uri
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
|
@ -20,7 +21,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
@ -74,10 +74,6 @@ class UIViewModel @Inject constructor(
|
||||||
debug("ViewModel created")
|
debug("ViewModel created")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun insertPacket(packet: Packet) = viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
repository.insert(packet)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteAllPacket() = viewModelScope.launch(Dispatchers.IO) {
|
fun deleteAllPacket() = viewModelScope.launch(Dispatchers.IO) {
|
||||||
repository.deleteAll()
|
repository.deleteAll()
|
||||||
}
|
}
|
||||||
|
|
@ -107,6 +103,20 @@ class UIViewModel @Inject constructor(
|
||||||
val channels = object : MutableLiveData<ChannelSet?>(null) {
|
val channels = object : MutableLiveData<ChannelSet?>(null) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val _requestChannelUrl = MutableLiveData<Uri?>(null)
|
||||||
|
val requestChannelUrl: LiveData<Uri?> get() = _requestChannelUrl
|
||||||
|
|
||||||
|
fun setRequestChannelUrl(channelUrl: Uri) {
|
||||||
|
_requestChannelUrl.value = channelUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called immediately after activity observes requestChannelUrl
|
||||||
|
*/
|
||||||
|
fun clearRequestChannelUrl() {
|
||||||
|
_requestChannelUrl.value = null
|
||||||
|
}
|
||||||
|
|
||||||
var positionBroadcastSecs: Int?
|
var positionBroadcastSecs: Int?
|
||||||
get() {
|
get() {
|
||||||
radioConfig.value?.preferences?.let {
|
radioConfig.value?.preferences?.let {
|
||||||
|
|
@ -229,10 +239,6 @@ class UIViewModel @Inject constructor(
|
||||||
val ownerName = object : MutableLiveData<String>("MrIDE Test") {
|
val ownerName = object : MutableLiveData<String>("MrIDE Test") {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val bluetoothEnabled = object : MutableLiveData<Boolean>(false) {
|
|
||||||
}
|
|
||||||
|
|
||||||
val provideLocation = object : MutableLiveData<Boolean>(preferences.getBoolean(MyPreferences.provideLocationKey, false)) {
|
val provideLocation = object : MutableLiveData<Boolean>(preferences.getBoolean(MyPreferences.provideLocationKey, false)) {
|
||||||
override fun setValue(value: Boolean) {
|
override fun setValue(value: Boolean) {
|
||||||
super.setValue(value)
|
super.setValue(value)
|
||||||
|
|
@ -243,9 +249,6 @@ class UIViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If the app was launched because we received a new channel intent, the Url will be here
|
|
||||||
var requestedChannelUrl: Uri? = null
|
|
||||||
|
|
||||||
// clean up all this nasty owner state management FIXME
|
// clean up all this nasty owner state management FIXME
|
||||||
fun setOwner(s: String? = null) {
|
fun setOwner(s: String? = null) {
|
||||||
|
|
||||||
|
|
@ -283,49 +286,67 @@ class UIViewModel @Inject constructor(
|
||||||
// Capture the current node value while we're still on main thread
|
// Capture the current node value while we're still on main thread
|
||||||
val nodes = nodeDB.nodes.value ?: emptyMap()
|
val nodes = nodeDB.nodes.value ?: emptyMap()
|
||||||
|
|
||||||
|
val positionToPos: (MeshProtos.Position?) -> Position? = { meshPosition ->
|
||||||
|
meshPosition?.let { Position(it) }.takeIf {
|
||||||
|
it?.isValid() == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
writeToUri(file_uri) { writer ->
|
writeToUri(file_uri) { writer ->
|
||||||
// Create a map of nodes keyed by their ID
|
// Create a map of nodes keyed by their ID
|
||||||
val nodesById = nodes.values.associateBy { it.num }
|
val nodesById = nodes.values.associateBy { it.num }.toMutableMap()
|
||||||
|
val nodePositions = mutableMapOf<Int, MeshProtos.Position?>()
|
||||||
|
|
||||||
writer.appendLine("date,time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx snr,distance,hop limit,payload")
|
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
|
// Packets are ordered by time, we keep most recent position of
|
||||||
// our device in localNodePosition.
|
// our device in localNodePosition.
|
||||||
var localNodePosition: MeshProtos.Position? = null
|
|
||||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd,HH:mm:ss", Locale.getDefault())
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd,HH:mm:ss", Locale.getDefault())
|
||||||
repository.getAllPacketsInReceiveOrder().first().forEach { packet ->
|
repository.getAllPacketsInReceiveOrder(Int.MAX_VALUE).first().forEach { packet ->
|
||||||
packet.proto?.let { proto ->
|
// If we get a NodeInfo packet, use it to update our position data (if valid)
|
||||||
|
packet.nodeInfo?.let { nodeInfo ->
|
||||||
|
positionToPos.invoke(nodeInfo.position)?.let { _ ->
|
||||||
|
nodePositions[nodeInfo.num] = nodeInfo.position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
packet.meshPacket?.let { proto ->
|
||||||
|
// If the packet contains position data then use it to update, if valid
|
||||||
packet.position?.let { position ->
|
packet.position?.let { position ->
|
||||||
if (proto.from == myNodeNum) {
|
positionToPos.invoke(position)?.let { _ ->
|
||||||
localNodePosition = position
|
nodePositions[proto.from] = position
|
||||||
} else {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out of our results any packet that doesn't report SNR. This
|
||||||
|
// is primarily ADMIN_APP.
|
||||||
|
if (proto.rxSnr > 0.0f) {
|
||||||
val rxDateTime = dateFormat.format(packet.received_date)
|
val rxDateTime = dateFormat.format(packet.received_date)
|
||||||
val rxFrom = proto.from.toUInt()
|
val rxFrom = proto.from.toUInt()
|
||||||
val senderName = nodesById[proto.from]?.user?.longName ?: ""
|
val senderName = nodesById[proto.from]?.user?.longName ?: ""
|
||||||
|
|
||||||
// sender lat & long
|
// sender lat & long
|
||||||
val senderPos = packet.position
|
val senderPosition = nodePositions[proto.from]
|
||||||
?.let { p -> Position(p) }
|
val senderPos = positionToPos.invoke(senderPosition)
|
||||||
?.takeIf { p -> p.isValid() }
|
|
||||||
val senderLat = senderPos?.latitude ?: ""
|
val senderLat = senderPos?.latitude ?: ""
|
||||||
val senderLong = senderPos?.longitude ?: ""
|
val senderLong = senderPos?.longitude ?: ""
|
||||||
|
|
||||||
// rx lat, long, and elevation
|
// rx lat, long, and elevation
|
||||||
val rxPos = localNodePosition
|
val rxPosition = nodePositions[myNodeNum]
|
||||||
?.let { p -> Position(p) }
|
val rxPos = positionToPos.invoke(rxPosition)
|
||||||
?.takeIf { p -> p.isValid() }
|
|
||||||
val rxLat = rxPos?.latitude ?: ""
|
val rxLat = rxPos?.latitude ?: ""
|
||||||
val rxLong = rxPos?.longitude ?: ""
|
val rxLong = rxPos?.longitude ?: ""
|
||||||
val rxAlt = rxPos?.altitude ?: ""
|
val rxAlt = rxPos?.altitude ?: ""
|
||||||
val rxSnr = "%f".format(proto.rxSnr)
|
val rxSnr = "%f".format(proto.rxSnr)
|
||||||
|
|
||||||
// Calculate the distance if both positions are valid
|
// Calculate the distance if both positions are valid
|
||||||
|
|
||||||
val dist = if (senderPos == null || rxPos == null) {
|
val dist = if (senderPos == null || rxPos == null) {
|
||||||
""
|
""
|
||||||
} else {
|
} else {
|
||||||
positionToMeter(
|
positionToMeter(
|
||||||
localNodePosition!!,
|
rxPosition!!, // Use rxPosition but only if rxPos was valid
|
||||||
position
|
senderPosition!! // Use senderPosition but only if senderPos was valid
|
||||||
).roundToInt().toString()
|
).roundToInt().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -347,7 +368,6 @@ class UIViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private suspend inline fun writeToUri(uri: Uri, crossinline block: suspend (BufferedWriter) -> Unit) {
|
private suspend inline fun writeToUri(uri: Uri, crossinline block: suspend (BufferedWriter) -> Unit) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.geeksville.mesh.service
|
package com.geeksville.mesh.repository.bluetooth
|
||||||
|
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
|
|
@ -6,29 +6,26 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import com.geeksville.util.exceptionReporter
|
import com.geeksville.util.exceptionReporter
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A helper class to call onChanged when bluetooth is enabled or disabled
|
* A helper class to call onChanged when bluetooth is enabled or disabled
|
||||||
*/
|
*/
|
||||||
class BluetoothStateReceiver(
|
class BluetoothBroadcastReceiver @Inject constructor(
|
||||||
private val onChanged: (Boolean) -> Unit
|
private val bluetoothRepository: BluetoothRepository
|
||||||
) : BroadcastReceiver() {
|
) : BroadcastReceiver() {
|
||||||
|
internal val intentFilter get() = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) // Can be used for registering
|
||||||
val intentFilter get() = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) // Can be used for registering
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
|
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
|
||||||
if (intent.action == BluetoothAdapter.ACTION_STATE_CHANGED) {
|
if (intent.action == BluetoothAdapter.ACTION_STATE_CHANGED) {
|
||||||
when (intent.bluetoothAdapterState) {
|
when (intent.bluetoothAdapterState) {
|
||||||
// Simulate a disconnection if the user disables bluetooth entirely
|
// Simulate a disconnection if the user disables bluetooth entirely
|
||||||
BluetoothAdapter.STATE_OFF -> onChanged(false)
|
BluetoothAdapter.STATE_OFF -> bluetoothRepository.refreshState()
|
||||||
BluetoothAdapter.STATE_ON -> onChanged(true)
|
BluetoothAdapter.STATE_ON -> bluetoothRepository.refreshState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val Intent.bluetoothAdapterState: Int
|
private val Intent.bluetoothAdapterState: Int
|
||||||
get() = getIntExtra(
|
get() = getIntExtra(BluetoothAdapter.EXTRA_STATE,-1)
|
||||||
BluetoothAdapter.EXTRA_STATE,
|
|
||||||
-1
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
package com.geeksville.mesh.repository.bluetooth
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Application
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.le.BluetoothLeScanner
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.coroutineScope
|
||||||
|
import com.geeksville.android.Logging
|
||||||
|
import com.geeksville.mesh.CoroutineDispatchers
|
||||||
|
import com.geeksville.mesh.android.hasConnectPermission
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository responsible for maintaining and updating the state of Bluetooth availability.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class BluetoothRepository @Inject constructor(
|
||||||
|
private val application: Application,
|
||||||
|
private val bluetoothAdapterLazy: dagger.Lazy<BluetoothAdapter?>,
|
||||||
|
private val bluetoothBroadcastReceiverLazy: dagger.Lazy<BluetoothBroadcastReceiver>,
|
||||||
|
private val dispatchers: CoroutineDispatchers,
|
||||||
|
private val processLifecycle: Lifecycle,
|
||||||
|
) : Logging {
|
||||||
|
private val _state = MutableStateFlow(BluetoothState(
|
||||||
|
// Assume we have permission until we get our initial state update to prevent premature
|
||||||
|
// notifications to the user.
|
||||||
|
hasPermissions = true
|
||||||
|
))
|
||||||
|
val state: StateFlow<BluetoothState> = _state.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
processLifecycle.coroutineScope.launch(dispatchers.default) {
|
||||||
|
updateBluetoothState()
|
||||||
|
bluetoothBroadcastReceiverLazy.get().let { receiver ->
|
||||||
|
application.registerReceiver(receiver, receiver.intentFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshState() {
|
||||||
|
processLifecycle.coroutineScope.launch(dispatchers.default) {
|
||||||
|
updateBluetoothState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRemoteDevice(address: String): BluetoothDevice? {
|
||||||
|
return bluetoothAdapterLazy.get()?.getRemoteDevice(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBluetoothLeScanner(): BluetoothLeScanner? {
|
||||||
|
return bluetoothAdapterLazy.get()?.bluetoothLeScanner
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
internal suspend fun updateBluetoothState() {
|
||||||
|
val newState: BluetoothState = bluetoothAdapterLazy.get()?.takeIf {
|
||||||
|
application.hasConnectPermission().also { hasPerms ->
|
||||||
|
if (!hasPerms) errormsg("Still missing needed bluetooth permissions")
|
||||||
|
}
|
||||||
|
}?.let { adapter ->
|
||||||
|
/// ask the adapter if we have access
|
||||||
|
BluetoothState(
|
||||||
|
hasPermissions = true,
|
||||||
|
enabled = adapter.isEnabled,
|
||||||
|
bondedDevices = createBondedDevicesFlow(adapter),
|
||||||
|
)
|
||||||
|
} ?: BluetoothState()
|
||||||
|
|
||||||
|
_state.emit(newState)
|
||||||
|
debug("Detected our bluetooth access=$newState")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a cold Flow used to obtain the set of bonded devices.
|
||||||
|
*/
|
||||||
|
@SuppressLint("MissingPermission") // Already checked prior to calling
|
||||||
|
private suspend fun createBondedDevicesFlow(adapter: BluetoothAdapter): Flow<Set<BluetoothDevice>>? {
|
||||||
|
return if (adapter.isEnabled) {
|
||||||
|
flow<Set<BluetoothDevice>> {
|
||||||
|
withContext(dispatchers.default) {
|
||||||
|
while (true) {
|
||||||
|
emit(adapter.bondedDevices)
|
||||||
|
delay(REFRESH_DELAY_MS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.flowOn(dispatchers.default)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val REFRESH_DELAY_MS = 1000L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.geeksville.mesh.repository.bluetooth
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothManager
|
||||||
|
import android.content.Context
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
interface BluetoothRepositoryModule {
|
||||||
|
companion object {
|
||||||
|
@Provides
|
||||||
|
fun provideBluetoothManager(application: Application): BluetoothManager? {
|
||||||
|
return application.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideBluetoothAdapter(service: BluetoothManager?): BluetoothAdapter? {
|
||||||
|
return service?.adapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.geeksville.mesh.repository.bluetooth
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A snapshot in time of the state of the bluetooth subsystem.
|
||||||
|
*/
|
||||||
|
data class BluetoothState(
|
||||||
|
/** Whether we have adequate permissions to query bluetooth state */
|
||||||
|
val hasPermissions: Boolean = false,
|
||||||
|
/** If we have adequate permissions and bluetooth is enabled */
|
||||||
|
val enabled: Boolean = false,
|
||||||
|
/** If enabled, a cold flow of the currently bonded devices */
|
||||||
|
val bondedDevices: Flow<Set<BluetoothDevice>>? = null
|
||||||
|
)
|
||||||
|
|
@ -5,10 +5,7 @@ import android.bluetooth.BluetoothAdapter
|
||||||
import android.bluetooth.BluetoothGattCharacteristic
|
import android.bluetooth.BluetoothGattCharacteristic
|
||||||
import android.bluetooth.BluetoothGattService
|
import android.bluetooth.BluetoothGattService
|
||||||
import android.bluetooth.BluetoothManager
|
import android.bluetooth.BluetoothManager
|
||||||
import android.companion.CompanionDeviceManager
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Build
|
|
||||||
import com.geeksville.android.Logging
|
import com.geeksville.android.Logging
|
||||||
import com.geeksville.concurrent.handledLaunch
|
import com.geeksville.concurrent.handledLaunch
|
||||||
import com.geeksville.util.anonymize
|
import com.geeksville.util.anonymize
|
||||||
|
|
@ -112,12 +109,6 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
|
||||||
/** Return true if this address is still acceptable. For BLE that means, still bonded */
|
/** Return true if this address is still acceptable. For BLE that means, still bonded */
|
||||||
@SuppressLint("NewApi", "MissingPermission")
|
@SuppressLint("NewApi", "MissingPermission")
|
||||||
override fun addressValid(context: Context, rest: String): Boolean {
|
override fun addressValid(context: Context, rest: String): Boolean {
|
||||||
/* val allPaired = if (hasCompanionDeviceApi(context)) {
|
|
||||||
val deviceManager: CompanionDeviceManager by lazy {
|
|
||||||
context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
|
|
||||||
}
|
|
||||||
deviceManager.associations.map { it }.toSet()
|
|
||||||
} else { */
|
|
||||||
val allPaired = getBluetoothAdapter(context)?.bondedDevices.orEmpty()
|
val allPaired = getBluetoothAdapter(context)?.bondedDevices.orEmpty()
|
||||||
.map { it.address }.toSet()
|
.map { it.address }.toSet()
|
||||||
return if (!allPaired.contains(rest)) {
|
return if (!allPaired.contains(rest)) {
|
||||||
|
|
@ -127,63 +118,6 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Return the device we are configured to use, or null for none
|
|
||||||
/*
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
fun getBondedDeviceAddress(context: Context): String? =
|
|
||||||
if (hasCompanionDeviceApi(context)) {
|
|
||||||
// Use new companion API
|
|
||||||
|
|
||||||
val deviceManager = context.getSystemService(CompanionDeviceManager::class.java)
|
|
||||||
val associations = deviceManager.associations
|
|
||||||
val result = associations.firstOrNull()
|
|
||||||
debug("reading bonded devices: $result")
|
|
||||||
result
|
|
||||||
} else {
|
|
||||||
// Use classic API and a preferences string
|
|
||||||
|
|
||||||
val allPaired =
|
|
||||||
getBluetoothAdapter(context)?.bondedDevices.orEmpty().map { it.address }.toSet()
|
|
||||||
|
|
||||||
// If the user has unpaired our device, treat things as if we don't have one
|
|
||||||
val address = InterfaceService.getPrefs(context).getString(DEVADDR_KEY, null)
|
|
||||||
|
|
||||||
if (address != null && !allPaired.contains(address)) {
|
|
||||||
warn("Ignoring stale bond to ${address.anonymize}")
|
|
||||||
null
|
|
||||||
} else
|
|
||||||
address
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/// Can we use the modern BLE scan API?
|
|
||||||
fun hasCompanionDeviceApi(context: Context): Boolean =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
val res =
|
|
||||||
context.packageManager.hasSystemFeature(PackageManager.FEATURE_COMPANION_DEVICE_SETUP)
|
|
||||||
debug("CompanionDevice API available=$res")
|
|
||||||
res
|
|
||||||
} else {
|
|
||||||
warn("CompanionDevice API not available, falling back to classic scan")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
/** FIXME - when adding companion device support back in, use this code to set companion device from setBondedDevice
|
|
||||||
* if (BluetoothInterface.hasCompanionDeviceApi(this)) {
|
|
||||||
// We only keep an association to one device at a time...
|
|
||||||
if (addr != null) {
|
|
||||||
val deviceManager = getSystemService(CompanionDeviceManager::class.java)
|
|
||||||
|
|
||||||
deviceManager.associations.forEach { old ->
|
|
||||||
if (addr != old) {
|
|
||||||
BluetoothInterface.debug("Forgetting old BLE association $old")
|
|
||||||
deviceManager.disassociate(old)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* this is created in onCreate()
|
* this is created in onCreate()
|
||||||
* We do an ugly hack of keeping it in the singleton so we can share it for the rare software update case
|
* We do an ugly hack of keeping it in the singleton so we can share it for the rare software update case
|
||||||
|
|
|
||||||
|
|
@ -696,7 +696,9 @@ class MeshService : Service(), Logging {
|
||||||
id = packet.id,
|
id = packet.id,
|
||||||
dataType = data.portnumValue,
|
dataType = data.portnumValue,
|
||||||
bytes = bytes,
|
bytes = bytes,
|
||||||
hopLimit = hopLimit
|
hopLimit = hopLimit,
|
||||||
|
channel = packet.channel,
|
||||||
|
delayed = packet.delayedValue
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -719,7 +721,7 @@ class MeshService : Service(), Logging {
|
||||||
// we only care about old text messages, we just store those...
|
// we only care about old text messages, we just store those...
|
||||||
if (dataPacket.dataType == Portnums.PortNum.TEXT_MESSAGE_APP_VALUE) {
|
if (dataPacket.dataType == Portnums.PortNum.TEXT_MESSAGE_APP_VALUE) {
|
||||||
// discard old messages if needed then add the new one
|
// discard old messages if needed then add the new one
|
||||||
while (recentDataPackets.size > 50)
|
while (recentDataPackets.size > 100)
|
||||||
recentDataPackets.removeAt(0)
|
recentDataPackets.removeAt(0)
|
||||||
|
|
||||||
// FIXME - possible kotlin bug in 1.3.72 - it seems that if we start with the (globally shared) emptyList,
|
// FIXME - possible kotlin bug in 1.3.72 - it seems that if we start with the (globally shared) emptyList,
|
||||||
|
|
@ -919,6 +921,16 @@ class MeshService : Service(), Logging {
|
||||||
p.time = System.currentTimeMillis() // update time to the actual time we started sending
|
p.time = System.currentTimeMillis() // update time to the actual time we started sending
|
||||||
// debug("Sending to radio: ${packet.toPIIString()}")
|
// debug("Sending to radio: ${packet.toPIIString()}")
|
||||||
sendToRadio(packet)
|
sendToRadio(packet)
|
||||||
|
|
||||||
|
if (packet.hasDecoded()) {
|
||||||
|
val packetToSave = Packet(
|
||||||
|
UUID.randomUUID().toString(),
|
||||||
|
"packet",
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
packet.toString()
|
||||||
|
)
|
||||||
|
insertPacket(packetToSave)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processQueuedPackets() {
|
private fun processQueuedPackets() {
|
||||||
|
|
@ -1191,8 +1203,10 @@ class MeshService : Service(), Logging {
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
RadioInterfaceService.RADIO_CONNECTED_ACTION -> {
|
RadioInterfaceService.RADIO_CONNECTED_ACTION -> {
|
||||||
try {
|
try {
|
||||||
|
// sleep now disabled by default on ESP32, permanent is true unless isPowerSaving enabled
|
||||||
|
val lsEnabled = radioConfig?.preferences?.isPowerSaving ?: false
|
||||||
val connected = intent.getBooleanExtra(EXTRA_CONNECTED, false)
|
val connected = intent.getBooleanExtra(EXTRA_CONNECTED, false)
|
||||||
val permanent = intent.getBooleanExtra(EXTRA_PERMANENT, false)
|
val permanent = intent.getBooleanExtra(EXTRA_PERMANENT, false) || !lsEnabled
|
||||||
onConnectionChanged(
|
onConnectionChanged(
|
||||||
when {
|
when {
|
||||||
connected -> ConnectionState.CONNECTED
|
connected -> ConnectionState.CONNECTED
|
||||||
|
|
@ -1309,13 +1323,14 @@ class MeshService : Service(), Logging {
|
||||||
if (asStr != null)
|
if (asStr != null)
|
||||||
hwModelStr = asStr
|
hwModelStr = asStr
|
||||||
}
|
}
|
||||||
|
setFirmwareUpdateFilename(hwModelStr)
|
||||||
val mi = with(myInfo) {
|
val mi = with(myInfo) {
|
||||||
MyNodeInfo(
|
MyNodeInfo(
|
||||||
myNodeNum,
|
myNodeNum,
|
||||||
hasGps,
|
hasGps,
|
||||||
hwModelStr,
|
hwModelStr,
|
||||||
firmwareVersion,
|
firmwareVersion,
|
||||||
firmwareUpdateFilename != null,
|
firmwareUpdateFilename?.appLoad != null && firmwareUpdateFilename?.spiffs != null,
|
||||||
isBluetoothInterface && SoftwareUpdateService.shouldUpdate(
|
isBluetoothInterface && SoftwareUpdateService.shouldUpdate(
|
||||||
this@MeshService,
|
this@MeshService,
|
||||||
DeviceVersion(firmwareVersion)
|
DeviceVersion(firmwareVersion)
|
||||||
|
|
@ -1328,9 +1343,7 @@ class MeshService : Service(), Logging {
|
||||||
airUtilTx
|
airUtilTx
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
newMyNodeInfo = mi
|
newMyNodeInfo = mi
|
||||||
setFirmwareUpdateFilename(mi)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1560,7 +1573,7 @@ class MeshService : Service(), Logging {
|
||||||
try {
|
try {
|
||||||
val mi = myNodeInfo
|
val mi = myNodeInfo
|
||||||
if (mi != null) {
|
if (mi != null) {
|
||||||
debug("Sending our position/time to=$destNum lat=$lat, lon=$lon, alt=$alt")
|
debug("Sending our position/time to=$destNum lat=${lat.anonymize}, lon=${lon.anonymize}, alt=$alt")
|
||||||
|
|
||||||
val position = MeshProtos.Position.newBuilder().also {
|
val position = MeshProtos.Position.newBuilder().also {
|
||||||
it.longitudeI = Position.degI(lon)
|
it.longitudeI = Position.degI(lon)
|
||||||
|
|
@ -1670,12 +1683,12 @@ class MeshService : Service(), Logging {
|
||||||
/***
|
/***
|
||||||
* Return the filename we will install on the device
|
* Return the filename we will install on the device
|
||||||
*/
|
*/
|
||||||
private fun setFirmwareUpdateFilename(info: MyNodeInfo) {
|
private fun setFirmwareUpdateFilename(model: String?) {
|
||||||
firmwareUpdateFilename = try {
|
firmwareUpdateFilename = try {
|
||||||
if (info.firmwareVersion != null && info.model != null)
|
if (model != null)
|
||||||
SoftwareUpdateService.getUpdateFilename(
|
SoftwareUpdateService.getUpdateFilename(
|
||||||
this,
|
this,
|
||||||
info.model
|
model
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
null
|
null
|
||||||
|
|
@ -1782,10 +1795,12 @@ class MeshService : Service(), Logging {
|
||||||
this@MeshService.setOwner(myId, longName, shortName)
|
this@MeshService.setOwner(myId, longName, shortName)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun delete(position: Int) {
|
override fun deleteMessage(packetId: Int) {
|
||||||
if (position >= 0) {
|
val packet = recentDataPackets.find {it.id == packetId}
|
||||||
recentDataPackets.removeAt(position)
|
if (packet != null) {
|
||||||
}
|
recentDataPackets.remove(packet)
|
||||||
|
debug("Deleting message id=${packet.id}")
|
||||||
|
} else debug("Nothing to delete, message id=${packetId} not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteAllMessages() {
|
override fun deleteAllMessages() {
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,27 @@ package com.geeksville.mesh.service
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.companion.CompanionDeviceManager
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.ServiceLifecycleDispatcher
|
||||||
|
import androidx.lifecycle.coroutineScope
|
||||||
import com.geeksville.android.BinaryLogFile
|
import com.geeksville.android.BinaryLogFile
|
||||||
import com.geeksville.android.GeeksvilleApplication
|
import com.geeksville.android.GeeksvilleApplication
|
||||||
import com.geeksville.android.Logging
|
import com.geeksville.android.Logging
|
||||||
import com.geeksville.concurrent.handledLaunch
|
import com.geeksville.concurrent.handledLaunch
|
||||||
import com.geeksville.mesh.IRadioInterfaceService
|
import com.geeksville.mesh.IRadioInterfaceService
|
||||||
|
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
|
||||||
import com.geeksville.util.anonymize
|
import com.geeksville.util.anonymize
|
||||||
import com.geeksville.util.ignoreException
|
import com.geeksville.util.ignoreException
|
||||||
import com.geeksville.util.toRemoteExceptions
|
import com.geeksville.util.toRemoteExceptions
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.cancel
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
open class RadioNotConnectedException(message: String = "Not connected to radio") :
|
open class RadioNotConnectedException(message: String = "Not connected to radio") :
|
||||||
|
|
@ -35,8 +38,18 @@ open class RadioNotConnectedException(message: String = "Not connected to radio"
|
||||||
* Note - this class intentionally dumb. It doesn't understand protobuf framing etc...
|
* Note - this class intentionally dumb. It doesn't understand protobuf framing etc...
|
||||||
* It is designed to be simple so it can be stubbed out with a simulated version as needed.
|
* It is designed to be simple so it can be stubbed out with a simulated version as needed.
|
||||||
*/
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
class RadioInterfaceService : Service(), Logging {
|
class RadioInterfaceService : Service(), Logging {
|
||||||
|
|
||||||
|
// The following is due to the fact that AIDL prevents us from extending from `LifecycleService`:
|
||||||
|
private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleDispatcher.lifecycle }
|
||||||
|
private val lifecycleDispatcher: ServiceLifecycleDispatcher by lazy {
|
||||||
|
ServiceLifecycleDispatcher(lifecycleOwner)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var bluetoothRepository: BluetoothRepository
|
||||||
|
|
||||||
companion object : Logging {
|
companion object : Logging {
|
||||||
/**
|
/**
|
||||||
* The RECEIVED_FROMRADIO
|
* The RECEIVED_FROMRADIO
|
||||||
|
|
@ -104,7 +117,7 @@ class RadioInterfaceService : Service(), Logging {
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi")
|
||||||
fun getBondedDeviceAddress(context: Context): String? {
|
fun getBondedDeviceAddress(context: Context): String? {
|
||||||
// If the user has unpaired our device, treat things as if we don't have one
|
// If the user has unpaired our device, treat things as if we don't have one
|
||||||
var address = getDeviceAddress(context)
|
val address = getDeviceAddress(context)
|
||||||
|
|
||||||
/// Interfaces can filter addresses to indicate that address is no longer acceptable
|
/// Interfaces can filter addresses to indicate that address is no longer acceptable
|
||||||
if (address != null) {
|
if (address != null) {
|
||||||
|
|
@ -142,16 +155,6 @@ class RadioInterfaceService : Service(), Logging {
|
||||||
/// true if our interface is currently connected to a device
|
/// true if our interface is currently connected to a device
|
||||||
private var isConnected = false
|
private var isConnected = false
|
||||||
|
|
||||||
/**
|
|
||||||
* If the user turns on bluetooth after we start, make sure to try and reconnected then
|
|
||||||
*/
|
|
||||||
private val bluetoothStateReceiver = BluetoothStateReceiver { enabled ->
|
|
||||||
if (enabled)
|
|
||||||
startInterface() // If bluetooth just got turned on, try to restart our ble link (which might be bluetooth)
|
|
||||||
else if (radioIf is BluetoothInterface)
|
|
||||||
stopInterface() // Was using bluetooth, need to shutdown
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun broadcastConnectionChanged(isConnected: Boolean, isPermanent: Boolean) {
|
private fun broadcastConnectionChanged(isConnected: Boolean, isPermanent: Boolean) {
|
||||||
debug("Broadcasting connection=$isConnected")
|
debug("Broadcasting connection=$isConnected")
|
||||||
val intent = Intent(RADIO_CONNECTED_ACTION)
|
val intent = Intent(RADIO_CONNECTED_ACTION)
|
||||||
|
|
@ -197,19 +200,35 @@ class RadioInterfaceService : Service(), Logging {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
runningService = this
|
runningService = this
|
||||||
|
lifecycleDispatcher.onServicePreSuperOnCreate()
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
registerReceiver(bluetoothStateReceiver, bluetoothStateReceiver.intentFilter)
|
|
||||||
|
lifecycleOwner.lifecycle.coroutineScope.launch {
|
||||||
|
bluetoothRepository.state.collect { state ->
|
||||||
|
if (state.enabled) {
|
||||||
|
startInterface()
|
||||||
|
} else {
|
||||||
|
stopInterface()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
lifecycleDispatcher.onServicePreSuperOnStart()
|
||||||
|
return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
unregisterReceiver(bluetoothStateReceiver)
|
|
||||||
stopInterface()
|
stopInterface()
|
||||||
serviceScope.cancel("Destroying RadioInterface")
|
serviceScope.cancel("Destroying RadioInterface")
|
||||||
runningService = null
|
runningService = null
|
||||||
|
lifecycleDispatcher.onServicePreSuperOnDestroy()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? {
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
|
lifecycleDispatcher.onServicePreSuperOnBind()
|
||||||
return binder
|
return binder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -290,9 +290,15 @@ class SoftwareUpdateService : JobIntentService(), Logging {
|
||||||
* you can use it for the software update.
|
* you can use it for the software update.
|
||||||
*/
|
*/
|
||||||
fun doUpdate(context: Context, sync: SafeBluetooth, assets: UpdateFilenames) {
|
fun doUpdate(context: Context, sync: SafeBluetooth, assets: UpdateFilenames) {
|
||||||
|
// calculate total firmware size (spiffs + appLoad)
|
||||||
|
var totalFirmwareSize = 0
|
||||||
|
if (assets.appLoad != null && assets.spiffs != null) {
|
||||||
|
totalFirmwareSize += context.assets.open(assets.appLoad).available()
|
||||||
|
totalFirmwareSize += context.assets.open(assets.spiffs).available()
|
||||||
|
}
|
||||||
// we must attempt spiffs first, because if we update the appload the device will reboot afterwards
|
// we must attempt spiffs first, because if we update the appload the device will reboot afterwards
|
||||||
try {
|
try {
|
||||||
assets.spiffs?.let { doUpdate(context, sync, it, FLASH_REGION_SPIFFS) }
|
assets.spiffs?.let { doUpdate(context, sync, it, FLASH_REGION_SPIFFS, totalFirmwareSize) }
|
||||||
} catch (_: BLECharacteristicNotFoundException) {
|
} catch (_: BLECharacteristicNotFoundException) {
|
||||||
// If we can't update spiffs (because not supported by target), do not fail
|
// If we can't update spiffs (because not supported by target), do not fail
|
||||||
errormsg("Ignoring failure to update spiffs on old appload")
|
errormsg("Ignoring failure to update spiffs on old appload")
|
||||||
|
|
@ -301,7 +307,7 @@ class SoftwareUpdateService : JobIntentService(), Logging {
|
||||||
errormsg("Device rejected invalid spiffs partition")
|
errormsg("Device rejected invalid spiffs partition")
|
||||||
}
|
}
|
||||||
|
|
||||||
assets.appLoad?.let { doUpdate(context, sync, it, FLASH_REGION_APPLOAD) }
|
assets.appLoad?.let { doUpdate(context, sync, it, FLASH_REGION_APPLOAD, totalFirmwareSize) }
|
||||||
sendProgress(context, ProgressSuccess, true)
|
sendProgress(context, ProgressSuccess, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -317,7 +323,8 @@ class SoftwareUpdateService : JobIntentService(), Logging {
|
||||||
context: Context,
|
context: Context,
|
||||||
sync: SafeBluetooth,
|
sync: SafeBluetooth,
|
||||||
assetName: String,
|
assetName: String,
|
||||||
flashRegion: Int = FLASH_REGION_APPLOAD
|
flashRegion: Int = FLASH_REGION_APPLOAD,
|
||||||
|
totalFirmwareSize: Int = 0
|
||||||
) {
|
) {
|
||||||
val isAppload = flashRegion == FLASH_REGION_APPLOAD
|
val isAppload = flashRegion == FLASH_REGION_APPLOAD
|
||||||
|
|
||||||
|
|
@ -378,13 +385,15 @@ class SoftwareUpdateService : JobIntentService(), Logging {
|
||||||
// Send all the blocks
|
// Send all the blocks
|
||||||
var oldProgress = -1 // used to limit # of log spam
|
var oldProgress = -1 // used to limit # of log spam
|
||||||
while (firmwareNumSent < firmwareSize) {
|
while (firmwareNumSent < firmwareSize) {
|
||||||
// If we are doing the spiffs partition, we limit progress to a max of 50%, so that the user doesn't think we are done
|
// If we are doing the spiffs partition, we limit progress to a max of maxProgress
|
||||||
// yet
|
// when updating the appload partition, progress from (100 - maxProgress) to 100%
|
||||||
val maxProgress = if (flashRegion != FLASH_REGION_APPLOAD)
|
// maxProgress = spiffs% = 100% - appLoad%; (int * 10 + 5) / 10 used for rounding
|
||||||
50 else 100
|
val maxProgress = ((firmwareSize * 1000 / totalFirmwareSize) + 5) / 10
|
||||||
|
val minProgress = if (flashRegion != FLASH_REGION_APPLOAD)
|
||||||
|
0 else (100 - maxProgress)
|
||||||
sendProgress(
|
sendProgress(
|
||||||
context,
|
context,
|
||||||
firmwareNumSent * maxProgress / firmwareSize,
|
minProgress + firmwareNumSent * maxProgress / firmwareSize,
|
||||||
isAppload
|
isAppload
|
||||||
)
|
)
|
||||||
if (progress != oldProgress) {
|
if (progress != oldProgress) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.geeksville.mesh.ui
|
package com.geeksville.mesh.ui
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.ColorMatrix
|
import android.graphics.ColorMatrix
|
||||||
|
|
@ -13,14 +14,15 @@ import android.view.ViewGroup
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import com.geeksville.analytics.DataPair
|
import com.geeksville.analytics.DataPair
|
||||||
import com.geeksville.android.GeeksvilleApplication
|
import com.geeksville.android.GeeksvilleApplication
|
||||||
import com.geeksville.android.Logging
|
import com.geeksville.android.Logging
|
||||||
import com.geeksville.android.hideKeyboard
|
import com.geeksville.android.hideKeyboard
|
||||||
|
import com.geeksville.android.isGooglePlayAvailable
|
||||||
import com.geeksville.mesh.AppOnlyProtos
|
import com.geeksville.mesh.AppOnlyProtos
|
||||||
import com.geeksville.mesh.ChannelProtos
|
import com.geeksville.mesh.ChannelProtos
|
||||||
import com.geeksville.mesh.MainActivity
|
|
||||||
import com.geeksville.mesh.R
|
import com.geeksville.mesh.R
|
||||||
import com.geeksville.mesh.android.hasCameraPermission
|
import com.geeksville.mesh.android.hasCameraPermission
|
||||||
import com.geeksville.mesh.databinding.ChannelFragmentBinding
|
import com.geeksville.mesh.databinding.ChannelFragmentBinding
|
||||||
|
|
@ -31,8 +33,12 @@ import com.geeksville.mesh.model.UIViewModel
|
||||||
import com.geeksville.mesh.service.MeshService
|
import com.geeksville.mesh.service.MeshService
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.mlkit.vision.barcode.common.Barcode
|
||||||
|
import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions
|
||||||
|
import com.google.mlkit.vision.codescanner.GmsBarcodeScanning
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
import com.journeyapps.barcodescanner.ScanContract
|
||||||
|
import com.journeyapps.barcodescanner.ScanOptions
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
|
|
||||||
|
|
@ -65,7 +71,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View {
|
||||||
_binding = ChannelFragmentBinding.inflate(inflater, container, false)
|
_binding = ChannelFragmentBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
@ -188,6 +194,52 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun zxingScan() {
|
||||||
|
debug("Starting zxing QR code scanner")
|
||||||
|
val zxingScan = ScanOptions()
|
||||||
|
zxingScan.setCameraId(0)
|
||||||
|
zxingScan.setPrompt("")
|
||||||
|
zxingScan.setBeepEnabled(false)
|
||||||
|
zxingScan.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||||
|
barcodeLauncher.launch(zxingScan)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestPermissionAndScan() {
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.camera_required)
|
||||||
|
.setMessage(R.string.why_camera_required)
|
||||||
|
.setNeutralButton(R.string.cancel) { _, _ ->
|
||||||
|
debug("Camera permission denied")
|
||||||
|
}
|
||||||
|
.setPositiveButton(getString(R.string.accept)) { _, _ ->
|
||||||
|
requestPermissionAndScanLauncher.launch(Manifest.permission.CAMERA)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun mlkitScan() {
|
||||||
|
debug("Starting ML Kit QR code scanner")
|
||||||
|
val options = GmsBarcodeScannerOptions.Builder()
|
||||||
|
.setBarcodeFormats(
|
||||||
|
Barcode.FORMAT_QR_CODE
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
val scanner = GmsBarcodeScanning.getClient(requireContext(), options)
|
||||||
|
scanner.startScan()
|
||||||
|
.addOnSuccessListener { barcode ->
|
||||||
|
if (barcode.rawValue != null)
|
||||||
|
model.setRequestChannelUrl(Uri.parse(barcode.rawValue))
|
||||||
|
}
|
||||||
|
.addOnFailureListener {
|
||||||
|
Snackbar.make(
|
||||||
|
requireView(),
|
||||||
|
R.string.channel_invalid,
|
||||||
|
Snackbar.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
|
@ -195,7 +247,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
||||||
requireActivity().hideKeyboard()
|
requireActivity().hideKeyboard()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.resetButton.setOnClickListener { _ ->
|
binding.resetButton.setOnClickListener {
|
||||||
// User just locked it, we should warn and then apply changes to radio
|
// User just locked it, we should warn and then apply changes to radio
|
||||||
MaterialAlertDialogBuilder(requireContext())
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
.setTitle(R.string.reset_to_defaults)
|
.setTitle(R.string.reset_to_defaults)
|
||||||
|
|
@ -211,30 +263,19 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.scanButton.setOnClickListener {
|
binding.scanButton.setOnClickListener {
|
||||||
if ((requireActivity() as MainActivity).hasCameraPermission()) {
|
if (isGooglePlayAvailable(requireContext())) {
|
||||||
debug("Starting QR code scanner")
|
mlkitScan()
|
||||||
val zxingScan = IntentIntegrator.forSupportFragment(this)
|
|
||||||
zxingScan.setCameraId(0)
|
|
||||||
zxingScan.setPrompt("")
|
|
||||||
zxingScan.setBeepEnabled(false)
|
|
||||||
zxingScan.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
|
||||||
zxingScan.initiateScan()
|
|
||||||
} else {
|
} else {
|
||||||
MaterialAlertDialogBuilder(requireContext())
|
if (requireContext().hasCameraPermission()) {
|
||||||
.setTitle(R.string.camera_required)
|
zxingScan()
|
||||||
.setMessage(R.string.why_camera_required)
|
} else {
|
||||||
.setNeutralButton(R.string.cancel) { _, _ ->
|
requestPermissionAndScan()
|
||||||
debug("Camera permission denied")
|
|
||||||
}
|
}
|
||||||
.setPositiveButton(getString(R.string.accept)) { _, _ ->
|
|
||||||
(requireActivity() as MainActivity).requestCameraPermission()
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Do not use setOnCheckedChanged here because we don't want to be called when we programmatically disable editing
|
// Note: Do not use setOnCheckedChanged here because we don't want to be called when we programmatically disable editing
|
||||||
binding.editableCheckbox.setOnClickListener { _ ->
|
binding.editableCheckbox.setOnClickListener {
|
||||||
|
|
||||||
/// We use this to determine if the user tried to install a custom name
|
/// We use this to determine if the user tried to install a custom name
|
||||||
var originalName = ""
|
var originalName = ""
|
||||||
|
|
@ -275,7 +316,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
||||||
val random = SecureRandom()
|
val random = SecureRandom()
|
||||||
val bytes = ByteArray(32)
|
val bytes = ByteArray(32)
|
||||||
random.nextBytes(bytes)
|
random.nextBytes(bytes)
|
||||||
newSettings.name = newName
|
newSettings.name = newName.take(11)
|
||||||
newSettings.psk = ByteString.copyFrom(bytes)
|
newSettings.psk = ByteString.copyFrom(bytes)
|
||||||
} else {
|
} else {
|
||||||
debug("Switching back to default channel")
|
debug("Switching back to default channel")
|
||||||
|
|
@ -299,14 +340,14 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
||||||
shareChannel()
|
shareChannel()
|
||||||
}
|
}
|
||||||
|
|
||||||
model.channels.observe(viewLifecycleOwner, {
|
model.channels.observe(viewLifecycleOwner) {
|
||||||
setGUIfromModel()
|
setGUIfromModel()
|
||||||
})
|
}
|
||||||
|
|
||||||
// If connection state changes, we might need to enable/disable buttons
|
// If connection state changes, we might need to enable/disable buttons
|
||||||
model.isConnected.observe(viewLifecycleOwner, {
|
model.isConnected.observe(viewLifecycleOwner) {
|
||||||
setGUIfromModel()
|
setGUIfromModel()
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getModemConfig(selectedChannelOptionString: String): ChannelProtos.ChannelSettings.ModemConfig {
|
private fun getModemConfig(selectedChannelOptionString: String): ChannelProtos.ChannelSettings.ModemConfig {
|
||||||
|
|
@ -314,18 +355,18 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
||||||
if (getString(item.configRes) == selectedChannelOptionString)
|
if (getString(item.configRes) == selectedChannelOptionString)
|
||||||
return item.modemConfig
|
return item.modemConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
return ChannelProtos.ChannelSettings.ModemConfig.UNRECOGNIZED
|
return ChannelProtos.ChannelSettings.ModemConfig.UNRECOGNIZED
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
private val requestPermissionAndScanLauncher =
|
||||||
val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
|
registerForActivityResult(ActivityResultContracts.RequestPermission()) { allowed ->
|
||||||
if (result != null) {
|
if (allowed) zxingScan()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register zxing launcher and result handler
|
||||||
|
private val barcodeLauncher = registerForActivityResult(ScanContract()) { result ->
|
||||||
if (result.contents != null) {
|
if (result.contents != null) {
|
||||||
((requireActivity() as MainActivity).perhapsChangeChannel(Uri.parse(result.contents)))
|
model.setRequestChannelUrl(Uri.parse(result.contents))
|
||||||
}
|
|
||||||
} else {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
358
app/src/main/java/com/geeksville/mesh/ui/ContactsFragment.kt
Normal file
358
app/src/main/java/com/geeksville/mesh/ui/ContactsFragment.kt
Normal file
|
|
@ -0,0 +1,358 @@
|
||||||
|
package com.geeksville.mesh.ui
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.*
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.fragment.app.setFragmentResult
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.geeksville.android.Logging
|
||||||
|
import com.geeksville.mesh.DataPacket
|
||||||
|
import com.geeksville.mesh.MainActivity
|
||||||
|
import com.geeksville.mesh.R
|
||||||
|
import com.geeksville.mesh.databinding.AdapterContactLayoutBinding
|
||||||
|
import com.geeksville.mesh.databinding.FragmentContactsBinding
|
||||||
|
import com.geeksville.mesh.model.UIViewModel
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class ContactsFragment : ScreenFragment("Messages"), Logging {
|
||||||
|
|
||||||
|
private var actionMode: ActionMode? = null
|
||||||
|
private var _binding: FragmentContactsBinding? = null
|
||||||
|
|
||||||
|
// This property is only valid between onCreateView and onDestroyView.
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val model: UIViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private val dateTimeFormat: DateFormat =
|
||||||
|
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT)
|
||||||
|
private val timeFormat: DateFormat =
|
||||||
|
DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||||
|
|
||||||
|
private fun getShortDateTime(time: Date): String {
|
||||||
|
// return time if within 24 hours, otherwise date/time
|
||||||
|
val oneDayMsec = 60 * 60 * 24 * 1000L
|
||||||
|
return if (System.currentTimeMillis() - time.time > oneDayMsec) {
|
||||||
|
dateTimeFormat.format(time)
|
||||||
|
} else timeFormat.format(time)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide a direct reference to each of the views within a data item
|
||||||
|
// Used to cache the views within the item layout for fast access
|
||||||
|
class ViewHolder(itemView: AdapterContactLayoutBinding) :
|
||||||
|
RecyclerView.ViewHolder(itemView.root) {
|
||||||
|
val shortName = itemView.shortName
|
||||||
|
val longName = itemView.longName
|
||||||
|
val lastMessageTime = itemView.lastMessageTime
|
||||||
|
val lastMessageText = itemView.lastMessageText
|
||||||
|
}
|
||||||
|
|
||||||
|
private val contactsAdapter = object : RecyclerView.Adapter<ViewHolder>() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when RecyclerView needs a new [ViewHolder] of the given type to represent
|
||||||
|
* an item.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* This new ViewHolder should be constructed with a new View that can represent the items
|
||||||
|
* of the given type. You can either create a new View manually or inflate it from an XML
|
||||||
|
* layout file.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* The new ViewHolder will be used to display items of the adapter using
|
||||||
|
* [.onBindViewHolder]. Since it will be re-used to display
|
||||||
|
* different items in the data set, it is a good idea to cache references to sub views of
|
||||||
|
* the View to avoid unnecessary [View.findViewById] calls.
|
||||||
|
*
|
||||||
|
* @param parent The ViewGroup into which the new View will be added after it is bound to
|
||||||
|
* an adapter position.
|
||||||
|
* @param viewType The view type of the new View.
|
||||||
|
*
|
||||||
|
* @return A new ViewHolder that holds a View of the given view type.
|
||||||
|
* @see .getItemViewType
|
||||||
|
* @see .onBindViewHolder
|
||||||
|
*/
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val inflater = LayoutInflater.from(requireContext())
|
||||||
|
|
||||||
|
// Inflate the custom layout
|
||||||
|
val contactsView = AdapterContactLayoutBinding.inflate(inflater, parent, false)
|
||||||
|
|
||||||
|
// Return a new holder instance
|
||||||
|
return ViewHolder(contactsView)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var messages = arrayOf<DataPacket>()
|
||||||
|
private var contacts = arrayOf<DataPacket>()
|
||||||
|
private var selectedList = ArrayList<String>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the total number of items in the data set held by the adapter.
|
||||||
|
*
|
||||||
|
* @return The total number of items in this adapter.
|
||||||
|
*/
|
||||||
|
override fun getItemCount(): Int = contacts.size
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by RecyclerView to display the data at the specified position. This method should
|
||||||
|
* update the contents of the [ViewHolder.itemView] to reflect the item at the given
|
||||||
|
* position.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Note that unlike [android.widget.ListView], RecyclerView will not call this method
|
||||||
|
* again if the position of the item changes in the data set unless the item itself is
|
||||||
|
* invalidated or the new position cannot be determined. For this reason, you should only
|
||||||
|
* use the `position` parameter while acquiring the related data item inside
|
||||||
|
* this method and should not keep a copy of it. If you need the position of an item later
|
||||||
|
* on (e.g. in a click listener), use [ViewHolder.getAdapterPosition] which will
|
||||||
|
* have the updated adapter position.
|
||||||
|
*
|
||||||
|
* Override [.onBindViewHolder] instead if Adapter can
|
||||||
|
* handle efficient partial bind.
|
||||||
|
*
|
||||||
|
* @param holder The ViewHolder which should be updated to represent the contents of the
|
||||||
|
* item at the given position in the data set.
|
||||||
|
* @param position The position of the item within the adapter's data set.
|
||||||
|
*/
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val contact = contacts[position]
|
||||||
|
|
||||||
|
// Determine if this is my message (originated on this device)
|
||||||
|
val isLocal = contact.from == DataPacket.ID_LOCAL
|
||||||
|
val isBroadcast = contact.to == DataPacket.ID_BROADCAST
|
||||||
|
val contactId = if (isLocal || isBroadcast) contact.to else contact.from
|
||||||
|
|
||||||
|
// grab usernames from NodeInfo
|
||||||
|
val nodes = model.nodeDB.nodes.value!!
|
||||||
|
val node = nodes[if (isLocal) contact.to else contact.from]
|
||||||
|
|
||||||
|
//grab channel names from RadioConfig
|
||||||
|
val channels = model.channels.value
|
||||||
|
val primaryChannel = channels?.primaryChannel
|
||||||
|
|
||||||
|
val shortName = node?.user?.shortName ?: "???"
|
||||||
|
val longName =
|
||||||
|
if (isBroadcast) primaryChannel?.name ?: getString(R.string.channel_name)
|
||||||
|
else node?.user?.longName ?: getString(R.string.unknown_username)
|
||||||
|
|
||||||
|
holder.shortName.text = if (isBroadcast) "All" else shortName
|
||||||
|
holder.longName.text = longName
|
||||||
|
|
||||||
|
val text = if (isLocal) contact.text else "$shortName: ${contact.text}"
|
||||||
|
holder.lastMessageText.text = text
|
||||||
|
|
||||||
|
if (contact.time != 0L) {
|
||||||
|
holder.lastMessageTime.visibility = View.VISIBLE
|
||||||
|
holder.lastMessageTime.text = getShortDateTime(Date(contact.time))
|
||||||
|
} else holder.lastMessageTime.visibility = View.INVISIBLE
|
||||||
|
|
||||||
|
holder.itemView.setOnLongClickListener {
|
||||||
|
if (actionMode == null) {
|
||||||
|
actionMode =
|
||||||
|
(activity as MainActivity).startActionMode(object : ActionMode.Callback {
|
||||||
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
mode.menuInflater.inflate(R.menu.menu_messages, menu)
|
||||||
|
mode.title = "1"
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareActionMode(
|
||||||
|
mode: ActionMode,
|
||||||
|
menu: Menu
|
||||||
|
): Boolean {
|
||||||
|
clickItem(holder, contactId)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActionItemClicked(
|
||||||
|
mode: ActionMode,
|
||||||
|
item: MenuItem
|
||||||
|
): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.deleteButton -> {
|
||||||
|
val messagesByContactId = ArrayList<DataPacket>()
|
||||||
|
selectedList.forEach { contactId ->
|
||||||
|
messagesByContactId += messages.filter {
|
||||||
|
if (contactId == DataPacket.ID_BROADCAST)
|
||||||
|
it.to == DataPacket.ID_BROADCAST
|
||||||
|
else
|
||||||
|
it.from == contactId && it.to != DataPacket.ID_BROADCAST
|
||||||
|
|| it.from == DataPacket.ID_LOCAL && it.to == contactId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val deleteMessagesString = resources.getQuantityString(
|
||||||
|
R.plurals.delete_messages,
|
||||||
|
messagesByContactId.size,
|
||||||
|
messagesByContactId.size
|
||||||
|
)
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setMessage(deleteMessagesString)
|
||||||
|
.setPositiveButton(getString(R.string.delete)) { _, _ ->
|
||||||
|
debug("User clicked deleteButton")
|
||||||
|
// all items selected --> deleteAllMessages()
|
||||||
|
if (messagesByContactId.size == messages.size) {
|
||||||
|
model.messagesState.deleteAllMessages()
|
||||||
|
} else {
|
||||||
|
messagesByContactId.forEach {
|
||||||
|
model.messagesState.deleteMessage(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mode.finish()
|
||||||
|
}
|
||||||
|
.setNeutralButton(R.string.cancel) { _, _ ->
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
R.id.selectAllButton -> {
|
||||||
|
// if all selected -> unselect all
|
||||||
|
if (selectedList.size == contacts.size) {
|
||||||
|
selectedList.clear()
|
||||||
|
mode.finish()
|
||||||
|
} else {
|
||||||
|
// else --> select all
|
||||||
|
selectedList.clear()
|
||||||
|
|
||||||
|
contacts.forEach {
|
||||||
|
if (it.from == DataPacket.ID_LOCAL || it.to == DataPacket.ID_BROADCAST)
|
||||||
|
selectedList.add(it.to!!) else selectedList.add(it.from!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
actionMode?.title = selectedList.size.toString()
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyActionMode(mode: ActionMode) {
|
||||||
|
selectedList.clear()
|
||||||
|
notifyDataSetChanged()
|
||||||
|
actionMode = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// when action mode is enabled
|
||||||
|
clickItem(holder, contactId)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
holder.itemView.setOnClickListener {
|
||||||
|
if (actionMode != null) clickItem(holder, contactId)
|
||||||
|
else {
|
||||||
|
debug("calling MessagesFragment filter:$contactId")
|
||||||
|
setFragmentResult(
|
||||||
|
"requestKey",
|
||||||
|
bundleOf("contactId" to contactId, "contactName" to longName)
|
||||||
|
)
|
||||||
|
parentFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.mainActivityLayout, MessagesFragment())
|
||||||
|
.addToBackStack(null)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedList.contains(contactId)) {
|
||||||
|
holder.itemView.background = GradientDrawable().apply {
|
||||||
|
shape = GradientDrawable.RECTANGLE
|
||||||
|
cornerRadius = 32f
|
||||||
|
setColor(Color.rgb(127, 127, 127))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
holder.itemView.background = GradientDrawable().apply {
|
||||||
|
shape = GradientDrawable.RECTANGLE
|
||||||
|
cornerRadius = 32f
|
||||||
|
setColor(
|
||||||
|
ContextCompat.getColor(
|
||||||
|
holder.itemView.context,
|
||||||
|
R.color.colorAdvancedBackground
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clickItem(
|
||||||
|
holder: ViewHolder,
|
||||||
|
contactId: String? = DataPacket.ID_BROADCAST
|
||||||
|
) {
|
||||||
|
val position = holder.bindingAdapterPosition
|
||||||
|
if (contactId != null && !selectedList.contains(contactId)) {
|
||||||
|
selectedList.add(contactId)
|
||||||
|
} else {
|
||||||
|
selectedList.remove(contactId)
|
||||||
|
}
|
||||||
|
if (selectedList.isEmpty()) {
|
||||||
|
// finish action mode when no items selected
|
||||||
|
actionMode?.finish()
|
||||||
|
} else {
|
||||||
|
// show total items selected on action mode title
|
||||||
|
actionMode?.title = selectedList.size.toString()
|
||||||
|
}
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when our contacts DB changes
|
||||||
|
fun onContactsChanged(contactsIn: Collection<DataPacket>) {
|
||||||
|
contacts = contactsIn.sortedByDescending { it.time }.toTypedArray()
|
||||||
|
notifyDataSetChanged() // FIXME, this is super expensive and redraws all nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when our message DB changes
|
||||||
|
fun onMessagesChanged(msgIn: Collection<DataPacket>) {
|
||||||
|
messages = msgIn.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onChannelsChanged() {
|
||||||
|
val oldBroadcast = contacts.find { it.to == DataPacket.ID_BROADCAST }
|
||||||
|
if (oldBroadcast != null) {
|
||||||
|
notifyItemChanged(contacts.indexOf(oldBroadcast))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
actionMode?.finish()
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentContactsBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
binding.contactsView.adapter = contactsAdapter
|
||||||
|
binding.contactsView.layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
|
||||||
|
model.channels.observe(viewLifecycleOwner) {
|
||||||
|
contactsAdapter.onChannelsChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
model.nodeDB.nodes.observe(viewLifecycleOwner) {
|
||||||
|
contactsAdapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
model.messagesState.contacts.observe(viewLifecycleOwner) {
|
||||||
|
debug("New contacts received: ${it.size}")
|
||||||
|
contactsAdapter.onContactsChanged(it.values)
|
||||||
|
}
|
||||||
|
|
||||||
|
model.messagesState.messages.observe(viewLifecycleOwner) {
|
||||||
|
contactsAdapter.onMessagesChanged(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,24 +1,24 @@
|
||||||
package com.geeksville.mesh.ui
|
package com.geeksville.mesh.ui
|
||||||
|
|
||||||
import android.app.AlertDialog
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
import android.view.LayoutInflater
|
import android.view.*
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.Observer
|
import androidx.fragment.app.setFragmentResultListener
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.geeksville.android.Logging
|
import com.geeksville.android.Logging
|
||||||
import com.geeksville.mesh.DataPacket
|
import com.geeksville.mesh.DataPacket
|
||||||
|
import com.geeksville.mesh.MainActivity
|
||||||
import com.geeksville.mesh.MessageStatus
|
import com.geeksville.mesh.MessageStatus
|
||||||
import com.geeksville.mesh.R
|
import com.geeksville.mesh.R
|
||||||
import com.geeksville.mesh.databinding.AdapterMessageLayoutBinding
|
import com.geeksville.mesh.databinding.AdapterMessageLayoutBinding
|
||||||
|
|
@ -26,6 +26,7 @@ import com.geeksville.mesh.databinding.MessagesFragmentBinding
|
||||||
import com.geeksville.mesh.model.UIViewModel
|
import com.geeksville.mesh.model.UIViewModel
|
||||||
import com.geeksville.mesh.service.MeshService
|
import com.geeksville.mesh.service.MeshService
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
@ -37,31 +38,30 @@ fun EditText.on(actionId: Int, func: () -> Unit) {
|
||||||
if (actionId == receivedActionId) {
|
if (actionId == receivedActionId) {
|
||||||
func()
|
func()
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MessagesFragment : ScreenFragment("Messages"), Logging {
|
class MessagesFragment : Fragment(), Logging {
|
||||||
|
|
||||||
|
private var actionMode: ActionMode? = null
|
||||||
private var _binding: MessagesFragmentBinding? = null
|
private var _binding: MessagesFragmentBinding? = null
|
||||||
|
|
||||||
// This property is only valid between onCreateView and onDestroyView.
|
// This property is only valid between onCreateView and onDestroyView.
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
private var contactId: String = DataPacket.ID_BROADCAST
|
||||||
|
private var contactName: String = DataPacket.ID_BROADCAST
|
||||||
|
|
||||||
private val model: UIViewModel by activityViewModels()
|
private val model: UIViewModel by activityViewModels()
|
||||||
|
|
||||||
// Allows textMultiline with IME_ACTION_SEND
|
// Allows textMultiline with IME_ACTION_SEND
|
||||||
fun EditText.onActionSend(func: () -> Unit) {
|
private fun EditText.onActionSend(func: () -> Unit) {
|
||||||
setImeOptions(EditorInfo.IME_ACTION_SEND)
|
|
||||||
setRawInputType(InputType.TYPE_CLASS_TEXT)
|
|
||||||
setOnEditorActionListener { _, actionId, _ ->
|
setOnEditorActionListener { _, actionId, _ ->
|
||||||
|
|
||||||
if (actionId == EditorInfo.IME_ACTION_SEND) {
|
if (actionId == EditorInfo.IME_ACTION_SEND) {
|
||||||
func()
|
func()
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -73,22 +73,21 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||||
|
|
||||||
private fun getShortDateTime(time: Date): String {
|
private fun getShortDateTime(time: Date): String {
|
||||||
// return time if within 24 hours, otherwise date/time
|
// return time if within 24 hours, otherwise date/time
|
||||||
val one_day = 60 * 60 * 24 * 1000
|
val oneDayMsec = 60 * 60 * 24 * 1000L
|
||||||
if (System.currentTimeMillis() - time.time > one_day) {
|
return if (System.currentTimeMillis() - time.time > oneDayMsec) {
|
||||||
return dateTimeFormat.format(time)
|
dateTimeFormat.format(time)
|
||||||
} else return timeFormat.format(time)
|
} else timeFormat.format(time)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Provide a direct reference to each of the views within a data item
|
// Provide a direct reference to each of the views within a data item
|
||||||
// Used to cache the views within the item layout for fast access
|
// Used to cache the views within the item layout for fast access
|
||||||
class ViewHolder(itemView: AdapterMessageLayoutBinding) :
|
class ViewHolder(itemView: AdapterMessageLayoutBinding) :
|
||||||
RecyclerView.ViewHolder(itemView.root) {
|
RecyclerView.ViewHolder(itemView.root) {
|
||||||
|
val card: CardView = itemView.Card
|
||||||
val username: Chip = itemView.username
|
val username: Chip = itemView.username
|
||||||
val messageText: TextView = itemView.messageText
|
val messageText: TextView = itemView.messageText
|
||||||
val messageTime: TextView = itemView.messageTime
|
val messageTime: TextView = itemView.messageTime
|
||||||
val messageStatusIcon: ImageView = itemView.messageStatusIcon
|
val messageStatusIcon: ImageView = itemView.messageStatusIcon
|
||||||
val card: CardView = itemView.Card
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val messagesAdapter = object : RecyclerView.Adapter<ViewHolder>() {
|
private val messagesAdapter = object : RecyclerView.Adapter<ViewHolder>() {
|
||||||
|
|
@ -119,8 +118,6 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
val inflater = LayoutInflater.from(requireContext())
|
val inflater = LayoutInflater.from(requireContext())
|
||||||
|
|
||||||
// Inflate the custom layout
|
|
||||||
|
|
||||||
// Inflate the custom layout
|
// Inflate the custom layout
|
||||||
val contactViewBinding = AdapterMessageLayoutBinding.inflate(inflater, parent, false)
|
val contactViewBinding = AdapterMessageLayoutBinding.inflate(inflater, parent, false)
|
||||||
|
|
||||||
|
|
@ -128,6 +125,9 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||||
return ViewHolder(contactViewBinding)
|
return ViewHolder(contactViewBinding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var messages = arrayOf<DataPacket>()
|
||||||
|
var selectedList = ArrayList<DataPacket>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the total number of items in the data set held by the adapter.
|
* Returns the total number of items in the data set held by the adapter.
|
||||||
*
|
*
|
||||||
|
|
@ -159,69 +159,56 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val msg = messages[position]
|
val msg = messages[position]
|
||||||
val nodes = model.nodeDB.nodes.value!!
|
val nodes = model.nodeDB.nodes.value!!
|
||||||
val node = nodes.get(msg.from)
|
|
||||||
// Determine if this is my message (originated on this device).
|
// Determine if this is my message (originated on this device)
|
||||||
// val isMe = model.myNodeInfo.value?.myNodeNum == node?.num
|
val isLocal = msg.from == DataPacket.ID_LOCAL
|
||||||
val isMe = msg.from == "^local"
|
val isBroadcast = (msg.to == DataPacket.ID_BROADCAST
|
||||||
|
|| msg.delayed == 1) // MeshProtos.MeshPacket.Delayed.DELAYED_BROADCAST_VALUE == 1
|
||||||
|
|
||||||
|
// Filter messages by contactId
|
||||||
|
if (contactId == DataPacket.ID_BROADCAST) {
|
||||||
|
if (isBroadcast) {
|
||||||
|
holder.card.visibility = View.VISIBLE
|
||||||
|
} else holder.card.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
if (msg.from == contactId && msg.to != DataPacket.ID_BROADCAST
|
||||||
|
|| msg.from == DataPacket.ID_LOCAL && msg.to == contactId) {
|
||||||
|
holder.card.visibility = View.VISIBLE
|
||||||
|
} else holder.card.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
// Set cardview offset and color.
|
// Set cardview offset and color.
|
||||||
val marginParams = holder.card.layoutParams as ViewGroup.MarginLayoutParams
|
val marginParams = holder.card.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
val messageOffset = resources.getDimensionPixelOffset(R.dimen.message_offset)
|
val messageOffset = resources.getDimensionPixelOffset(R.dimen.message_offset)
|
||||||
holder.card.setOnLongClickListener {
|
if (isLocal) {
|
||||||
val deleteMessageDialog = AlertDialog.Builder(context)
|
holder.messageText.textAlignment = View.TEXT_ALIGNMENT_TEXT_END
|
||||||
deleteMessageDialog.setMessage(R.string.delete_selected_message)
|
|
||||||
deleteMessageDialog.setPositiveButton(
|
|
||||||
R.string.delete
|
|
||||||
) { _, _ ->
|
|
||||||
model.messagesState.deleteMessage((messages[position]), position)
|
|
||||||
}
|
|
||||||
deleteMessageDialog.setNeutralButton(
|
|
||||||
R.string.cancel
|
|
||||||
) { _, _ ->
|
|
||||||
}
|
|
||||||
deleteMessageDialog.setNegativeButton(
|
|
||||||
R.string.delete_all_messages
|
|
||||||
) { _, _ ->
|
|
||||||
model.messagesState.deleteAllMessages()
|
|
||||||
}
|
|
||||||
deleteMessageDialog.create()
|
|
||||||
deleteMessageDialog.show()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
if (isMe) {
|
|
||||||
marginParams.leftMargin = messageOffset
|
marginParams.leftMargin = messageOffset
|
||||||
marginParams.rightMargin = 0
|
marginParams.rightMargin = 0
|
||||||
context?.let {
|
context?.let {
|
||||||
holder.card.setCardBackgroundColor(
|
holder.card.setCardBackgroundColor(ContextCompat.getColor(it, R.color.colorMyMsg))
|
||||||
ContextCompat.getColor(
|
|
||||||
it,
|
|
||||||
R.color.colorMyMsg
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
holder.messageText.textAlignment = View.TEXT_ALIGNMENT_TEXT_START
|
||||||
marginParams.rightMargin = messageOffset
|
marginParams.rightMargin = messageOffset
|
||||||
marginParams.leftMargin = 0
|
marginParams.leftMargin = 0
|
||||||
context?.let {
|
context?.let {
|
||||||
holder.card.setCardBackgroundColor(
|
holder.card.setCardBackgroundColor(ContextCompat.getColor(it, R.color.colorMsg))
|
||||||
ContextCompat.getColor(
|
|
||||||
it,
|
|
||||||
R.color.colorMsg
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Hide the username chip for my messages
|
// Hide the username chip for my messages
|
||||||
if (isMe) {
|
if (isLocal) {
|
||||||
holder.username.visibility = View.GONE
|
holder.username.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
holder.username.visibility = View.VISIBLE
|
holder.username.visibility = View.VISIBLE
|
||||||
// If we can't find the sender, just use the ID
|
// If we can't find the sender, just use the ID
|
||||||
|
val node = nodes[msg.from]
|
||||||
val user = node?.user
|
val user = node?.user
|
||||||
holder.username.text = user?.shortName ?: msg.from
|
holder.username.text = user?.shortName ?: msg.from
|
||||||
}
|
}
|
||||||
if (msg.errorMessage != null) {
|
if (msg.errorMessage != null) {
|
||||||
context?.let { holder.card.setCardBackgroundColor(Color.RED) }
|
holder.itemView.context?.let {
|
||||||
|
holder.card.setCardBackgroundColor(Color.RED)
|
||||||
|
}
|
||||||
holder.messageText.text = msg.errorMessage
|
holder.messageText.text = msg.errorMessage
|
||||||
} else {
|
} else {
|
||||||
holder.messageText.text = msg.text
|
holder.messageText.text = msg.text
|
||||||
|
|
@ -243,9 +230,122 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||||
|
|
||||||
} else
|
} else
|
||||||
holder.messageStatusIcon.visibility = View.INVISIBLE
|
holder.messageStatusIcon.visibility = View.INVISIBLE
|
||||||
|
|
||||||
|
holder.itemView.setOnLongClickListener {
|
||||||
|
if (actionMode == null) {
|
||||||
|
actionMode = (activity as MainActivity).startActionMode(object : ActionMode.Callback {
|
||||||
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
mode.menuInflater.inflate(R.menu.menu_messages, menu)
|
||||||
|
mode.title = "1"
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private var messages = arrayOf<DataPacket>()
|
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
clickItem(holder)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActionItemClicked(
|
||||||
|
mode: ActionMode,
|
||||||
|
item: MenuItem
|
||||||
|
): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.deleteButton -> {
|
||||||
|
val deleteMessagesString = resources.getQuantityString(
|
||||||
|
R.plurals.delete_messages,
|
||||||
|
selectedList.size,
|
||||||
|
selectedList.size
|
||||||
|
)
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setMessage(deleteMessagesString)
|
||||||
|
.setPositiveButton(getString(R.string.delete)) { _, _ ->
|
||||||
|
debug("User clicked deleteButton")
|
||||||
|
// all items selected --> deleteAllMessages()
|
||||||
|
if (selectedList.size == messages.size) {
|
||||||
|
model.messagesState.deleteAllMessages()
|
||||||
|
} else {
|
||||||
|
selectedList.forEach {
|
||||||
|
model.messagesState.deleteMessage(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mode.finish()
|
||||||
|
}
|
||||||
|
.setNeutralButton(R.string.cancel) { _, _ ->
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
R.id.selectAllButton -> {
|
||||||
|
// filter messages by ContactId
|
||||||
|
val messagesByContactId = messages.filter {
|
||||||
|
if (contactId == DataPacket.ID_BROADCAST)
|
||||||
|
it.to == DataPacket.ID_BROADCAST
|
||||||
|
else
|
||||||
|
it.from == contactId && it.to != DataPacket.ID_BROADCAST
|
||||||
|
|| it.from == DataPacket.ID_LOCAL && it.to == contactId
|
||||||
|
}
|
||||||
|
// if all selected -> unselect all
|
||||||
|
if (selectedList.size == messagesByContactId.size) {
|
||||||
|
selectedList.clear()
|
||||||
|
mode.finish()
|
||||||
|
} else {
|
||||||
|
// else --> select all
|
||||||
|
selectedList.clear()
|
||||||
|
selectedList.addAll(messagesByContactId)
|
||||||
|
}
|
||||||
|
actionMode?.title = selectedList.size.toString()
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyActionMode(mode: ActionMode) {
|
||||||
|
selectedList.clear()
|
||||||
|
notifyDataSetChanged()
|
||||||
|
actionMode = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// when action mode is enabled
|
||||||
|
clickItem(holder)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
holder.itemView.setOnClickListener {
|
||||||
|
if (actionMode != null) clickItem(holder)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedList.contains(msg)) {
|
||||||
|
holder.itemView.background = GradientDrawable().apply {
|
||||||
|
shape = GradientDrawable.RECTANGLE
|
||||||
|
cornerRadius = 32f
|
||||||
|
setColor(Color.rgb(127, 127, 127))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
holder.itemView.background = GradientDrawable().apply {
|
||||||
|
shape = GradientDrawable.RECTANGLE
|
||||||
|
cornerRadius = 32f
|
||||||
|
setColor(ContextCompat.getColor(holder.itemView.context, R.color.colorAdvancedBackground))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clickItem(holder: ViewHolder) {
|
||||||
|
val position = holder.bindingAdapterPosition
|
||||||
|
if (!selectedList.contains(messages[position])) {
|
||||||
|
selectedList.add(messages[position])
|
||||||
|
} else {
|
||||||
|
selectedList.remove(messages[position])
|
||||||
|
}
|
||||||
|
if (selectedList.isEmpty()) {
|
||||||
|
// finish action mode when no items selected
|
||||||
|
actionMode?.finish()
|
||||||
|
} else {
|
||||||
|
// show total items selected on action mode title
|
||||||
|
actionMode?.title = selectedList.size.toString()
|
||||||
|
}
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
|
||||||
/// Called when our node DB changes
|
/// Called when our node DB changes
|
||||||
fun onMessagesChanged(msgIn: Collection<DataPacket>) {
|
fun onMessagesChanged(msgIn: Collection<DataPacket>) {
|
||||||
|
|
@ -258,22 +358,35 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
actionMode?.finish()
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View {
|
||||||
_binding = MessagesFragmentBinding.inflate(inflater, container, false)
|
_binding = MessagesFragmentBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
setFragmentResultListener("requestKey") { _, bundle->
|
||||||
|
// get the result from bundle
|
||||||
|
contactId = bundle.getString("contactId").toString()
|
||||||
|
contactName = bundle.getString("contactName").toString()
|
||||||
|
binding.messageTitle.text = contactName
|
||||||
|
}
|
||||||
|
|
||||||
binding.sendButton.setOnClickListener {
|
binding.sendButton.setOnClickListener {
|
||||||
debug("sendButton click")
|
debug("User clicked sendButton")
|
||||||
|
|
||||||
val str = binding.messageInputText.text.toString().trim()
|
val str = binding.messageInputText.text.toString().trim()
|
||||||
if (str.isNotEmpty())
|
if (str.isNotEmpty())
|
||||||
model.messagesState.sendMessage(str)
|
model.messagesState.sendMessage(str, contactId)
|
||||||
binding.messageInputText.setText("") // blow away the string the user just entered
|
binding.messageInputText.setText("") // blow away the string the user just entered
|
||||||
|
|
||||||
// requireActivity().hideKeyboard()
|
// requireActivity().hideKeyboard()
|
||||||
|
|
@ -295,34 +408,20 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||||
layoutManager.stackFromEnd = true // We want the last rows to always be shown
|
layoutManager.stackFromEnd = true // We want the last rows to always be shown
|
||||||
binding.messageListView.layoutManager = layoutManager
|
binding.messageListView.layoutManager = layoutManager
|
||||||
|
|
||||||
model.messagesState.messages.observe(viewLifecycleOwner, Observer {
|
model.messagesState.messages.observe(viewLifecycleOwner) {
|
||||||
debug("New messages received: ${it.size}")
|
debug("New messages received: ${it.size}")
|
||||||
messagesAdapter.onMessagesChanged(it)
|
messagesAdapter.onMessagesChanged(it)
|
||||||
})
|
}
|
||||||
|
|
||||||
// If connection state _OR_ myID changes we have to fix our ability to edit outgoing messages
|
// If connection state _OR_ myID changes we have to fix our ability to edit outgoing messages
|
||||||
fun updateTextEnabled() {
|
model.isConnected.observe(viewLifecycleOwner) { connectionState ->
|
||||||
binding.textInputLayout.isEnabled =
|
// If we don't know our node ID and we are offline don't let user try to send
|
||||||
model.isConnected.value != MeshService.ConnectionState.DISCONNECTED
|
val connected = connectionState == MeshService.ConnectionState.CONNECTED
|
||||||
|
binding.textInputLayout.isEnabled = connected
|
||||||
|
binding.sendButton.isEnabled = connected
|
||||||
|
|
||||||
// Just being connected is enough to allow sending texts I think
|
// Just being connected is enough to allow sending texts I think
|
||||||
// && model.nodeDB.myId.value != null && model.radioConfig.value != null
|
// && model.nodeDB.myId.value != null && model.radioConfig.value != null
|
||||||
}
|
}
|
||||||
|
|
||||||
model.isConnected.observe(viewLifecycleOwner, Observer { _ ->
|
|
||||||
// If we don't know our node ID and we are offline don't let user try to send
|
|
||||||
updateTextEnabled()
|
|
||||||
})
|
|
||||||
|
|
||||||
/* model.nodeDB.myId.observe(viewLifecycleOwner, Observer { _ ->
|
|
||||||
// If we don't know our node ID and we are offline don't let user try to send
|
|
||||||
updateTextEnabled()
|
|
||||||
})
|
|
||||||
|
|
||||||
model.radioConfig.observe(viewLifecycleOwner, Observer { _ ->
|
|
||||||
// If we don't know our node ID and we are offline don't let user try to send
|
|
||||||
updateTextEnabled()
|
|
||||||
}) */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package com.geeksville.mesh.ui
|
package com.geeksville.mesh.ui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
|
|
@ -21,9 +20,13 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
import androidx.activity.result.IntentSenderRequest
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.geeksville.analytics.DataPair
|
||||||
import com.geeksville.android.GeeksvilleApplication
|
import com.geeksville.android.GeeksvilleApplication
|
||||||
import com.geeksville.android.Logging
|
import com.geeksville.android.Logging
|
||||||
import com.geeksville.android.hideKeyboard
|
import com.geeksville.android.hideKeyboard
|
||||||
|
|
@ -33,6 +36,7 @@ import com.geeksville.mesh.R
|
||||||
import com.geeksville.mesh.RadioConfigProtos
|
import com.geeksville.mesh.RadioConfigProtos
|
||||||
import com.geeksville.mesh.android.*
|
import com.geeksville.mesh.android.*
|
||||||
import com.geeksville.mesh.databinding.SettingsFragmentBinding
|
import com.geeksville.mesh.databinding.SettingsFragmentBinding
|
||||||
|
import com.geeksville.mesh.model.BluetoothViewModel
|
||||||
import com.geeksville.mesh.model.UIViewModel
|
import com.geeksville.mesh.model.UIViewModel
|
||||||
import com.geeksville.mesh.service.*
|
import com.geeksville.mesh.service.*
|
||||||
import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ACTION_UPDATE_PROGRESS
|
import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ACTION_UPDATE_PROGRESS
|
||||||
|
|
@ -114,20 +118,17 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
||||||
debug("BTScanModel created")
|
debug("BTScanModel created")
|
||||||
}
|
}
|
||||||
|
|
||||||
open class DeviceListEntry(val name: String, val address: String, val bonded: Boolean) {
|
/** *fullAddress* = interface prefix + address (example: "x7C:9E:BD:F0:BE:BE") */
|
||||||
val bluetoothAddress
|
open class DeviceListEntry(val name: String, val fullAddress: String, val bonded: Boolean) {
|
||||||
get() =
|
val prefix get() = fullAddress[0]
|
||||||
if (isBluetooth)
|
val address get() = fullAddress.substring(1)
|
||||||
address.substring(1)
|
|
||||||
else
|
|
||||||
null
|
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "DeviceListEntry(name=${name.anonymize}, addr=${address.anonymize})"
|
return "DeviceListEntry(name=${name.anonymize}, addr=${fullAddress.anonymize}, bonded=$bonded)"
|
||||||
}
|
}
|
||||||
|
|
||||||
val isBluetooth: Boolean get() = address[0] == 'x'
|
val isBLE: Boolean get() = prefix == 'x'
|
||||||
val isSerial: Boolean get() = address[0] == 's'
|
val isUSB: Boolean get() = prefix == 's'
|
||||||
}
|
}
|
||||||
|
|
||||||
class USBDeviceListEntry(usbManager: UsbManager, val usb: UsbSerialDriver) : DeviceListEntry(
|
class USBDeviceListEntry(usbManager: UsbManager, val usb: UsbSerialDriver) : DeviceListEntry(
|
||||||
|
|
@ -141,7 +142,10 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
||||||
debug("BTScanModel cleared")
|
debug("BTScanModel cleared")
|
||||||
}
|
}
|
||||||
|
|
||||||
val bluetoothAdapter = context.bluetoothManager?.adapter
|
private val bluetoothAdapter = context.bluetoothManager?.adapter
|
||||||
|
private val deviceManager get() = context.deviceManager
|
||||||
|
val hasCompanionDeviceApi get() = context.hasCompanionDeviceApi()
|
||||||
|
private val hasConnectPermission get() = context.hasConnectPermission()
|
||||||
private val usbManager get() = context.usbManager
|
private val usbManager get() = context.usbManager
|
||||||
|
|
||||||
var selectedAddress: String? = null
|
var selectedAddress: String? = null
|
||||||
|
|
@ -158,15 +162,6 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If this address is for a USB device, return the macaddr portion, else null
|
|
||||||
val selectedUSB: String?
|
|
||||||
get() = selectedAddress?.let { a ->
|
|
||||||
if (a[0] == 's')
|
|
||||||
a.substring(1)
|
|
||||||
else
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use the string for the NopInterface
|
/// Use the string for the NopInterface
|
||||||
val selectedNotNull: String get() = selectedAddress ?: "n"
|
val selectedNotNull: String get() = selectedAddress ?: "n"
|
||||||
|
|
||||||
|
|
@ -220,7 +215,7 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
||||||
|
|
||||||
private fun addDevice(entry: DeviceListEntry) {
|
private fun addDevice(entry: DeviceListEntry) {
|
||||||
val oldDevs = devices.value!!
|
val oldDevs = devices.value!!
|
||||||
oldDevs[entry.address] = entry // Add/replace entry
|
oldDevs[entry.fullAddress] = entry // Add/replace entry
|
||||||
devices.value = oldDevs // trigger gui updates
|
devices.value = oldDevs // trigger gui updates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -232,9 +227,11 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
||||||
scanner?.stopScan(scanCallback)
|
scanner?.stopScan(scanCallback)
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
warn("Ignoring error stopping scan, probably BT adapter was disabled suddenly: ${ex.message}")
|
warn("Ignoring error stopping scan, probably BT adapter was disabled suddenly: ${ex.message}")
|
||||||
}
|
} finally {
|
||||||
scanner = null
|
scanner = null
|
||||||
|
_spinner.value = false
|
||||||
}
|
}
|
||||||
|
} else _spinner.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -257,13 +254,13 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
||||||
DeviceListEntry("Meshtastic_32ac", "xbb", true) */
|
DeviceListEntry("Meshtastic_32ac", "xbb", true) */
|
||||||
)
|
)
|
||||||
|
|
||||||
devices.value = (testnodes.map { it.address to it }).toMap().toMutableMap()
|
devices.value = (testnodes.map { it.fullAddress to it }).toMap().toMutableMap()
|
||||||
|
|
||||||
// If nothing was selected, by default select the first thing we see
|
// If nothing was selected, by default select the first thing we see
|
||||||
if (selectedAddress == null)
|
if (selectedAddress == null)
|
||||||
changeScanSelection(
|
changeScanSelection(
|
||||||
GeeksvilleApplication.currentActivity as MainActivity,
|
GeeksvilleApplication.currentActivity as MainActivity,
|
||||||
testnodes.first().address
|
testnodes.first().fullAddress
|
||||||
)
|
)
|
||||||
|
|
||||||
true
|
true
|
||||||
|
|
@ -286,6 +283,9 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
||||||
// Include a placeholder for "None"
|
// Include a placeholder for "None"
|
||||||
addDevice(DeviceListEntry(context.getString(R.string.none), "n", true))
|
addDevice(DeviceListEntry(context.getString(R.string.none), "n", true))
|
||||||
|
|
||||||
|
// Include CompanionDeviceManager valid associations
|
||||||
|
addDeviceAssociations()
|
||||||
|
|
||||||
usbDrivers.forEach { d ->
|
usbDrivers.forEach { d ->
|
||||||
addDevice(
|
addDevice(
|
||||||
USBDeviceListEntry(usbManager, d)
|
USBDeviceListEntry(usbManager, d)
|
||||||
|
|
@ -299,13 +299,20 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
|
||||||
fun startScan () {
|
fun startScan () {
|
||||||
|
if (hasCompanionDeviceApi) {
|
||||||
|
startCompanionScan()
|
||||||
|
} else startClassicScan()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
private fun startClassicScan() {
|
||||||
/// The following call might return null if the user doesn't have bluetooth access permissions
|
/// The following call might return null if the user doesn't have bluetooth access permissions
|
||||||
val bluetoothLeScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner
|
val bluetoothLeScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner
|
||||||
|
|
||||||
if (bluetoothLeScanner != null) { // could be null if bluetooth is disabled
|
if (bluetoothLeScanner != null) { // could be null if bluetooth is disabled
|
||||||
debug("starting scan")
|
debug("starting classic scan")
|
||||||
|
_spinner.value = true
|
||||||
|
|
||||||
// filter and only accept devices that have our service
|
// filter and only accept devices that have our service
|
||||||
val filter =
|
val filter =
|
||||||
|
|
@ -324,6 +331,91 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return DeviceListEntry from full Address (prefix + address).
|
||||||
|
* If Bluetooth is enabled and BLE Address is valid, get remote device information.
|
||||||
|
*/
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
fun getDeviceListEntry(fullAddress: String, bonded: Boolean = false): DeviceListEntry {
|
||||||
|
val address = fullAddress.substring(1)
|
||||||
|
val device = bluetoothAdapter?.getRemoteDevice(address)
|
||||||
|
return if (device != null && device.name != null) {
|
||||||
|
DeviceListEntry(device.name, fullAddress, device.bondState != BluetoothDevice.BOND_NONE)
|
||||||
|
} else {
|
||||||
|
DeviceListEntry(address, fullAddress, bonded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
fun addDeviceAssociations() {
|
||||||
|
if (hasCompanionDeviceApi) deviceManager?.associations?.forEach { bleAddress ->
|
||||||
|
val bleDevice = getDeviceListEntry("x$bleAddress", true)
|
||||||
|
// Disassociate after pairing is removed (if BLE is disabled, assume bonded)
|
||||||
|
if (bleDevice.name.startsWith("Mesh") && !bleDevice.bonded) {
|
||||||
|
debug("Forgetting old BLE association ${bleAddress.anonymize}")
|
||||||
|
deviceManager?.disassociate(bleAddress)
|
||||||
|
}
|
||||||
|
addDevice(bleDevice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _spinner = MutableLiveData(false)
|
||||||
|
val spinner: LiveData<Boolean> get() = _spinner
|
||||||
|
|
||||||
|
private val _associationRequest = MutableLiveData<IntentSenderRequest?>(null)
|
||||||
|
val associationRequest: LiveData<IntentSenderRequest?> get() = _associationRequest
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called immediately after fragment observes CompanionDeviceManager activity result
|
||||||
|
*/
|
||||||
|
fun clearAssociationRequest() {
|
||||||
|
_associationRequest.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
private fun associationRequest(): AssociationRequest {
|
||||||
|
// To skip filtering based on name and supported feature flags (UUIDs),
|
||||||
|
// don't include calls to setNamePattern() and addServiceUuid(),
|
||||||
|
// respectively. This example uses Bluetooth.
|
||||||
|
// We only look for Mesh (rather than the full name) because NRF52 uses a very short name
|
||||||
|
val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder()
|
||||||
|
.setNamePattern(Pattern.compile("Mesh.*"))
|
||||||
|
// .addServiceUuid(ParcelUuid(RadioInterfaceService.BTM_SERVICE_UUID), null)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// The argument provided in setSingleDevice() determines whether a single
|
||||||
|
// device name or a list of device names is presented to the user as
|
||||||
|
// pairing options.
|
||||||
|
return AssociationRequest.Builder()
|
||||||
|
.addDeviceFilter(deviceFilter)
|
||||||
|
.setSingleDevice(false)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
private fun startCompanionScan() {
|
||||||
|
debug("starting companion scan")
|
||||||
|
_spinner.value = true
|
||||||
|
deviceManager?.associate(
|
||||||
|
associationRequest(),
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
object : CompanionDeviceManager.Callback() {
|
||||||
|
override fun onDeviceFound(chooserLauncher: IntentSender) {
|
||||||
|
debug("CompanionDeviceManager - device found")
|
||||||
|
_spinner.value = false
|
||||||
|
chooserLauncher.let {
|
||||||
|
val request: IntentSenderRequest = IntentSenderRequest.Builder(it).build()
|
||||||
|
_associationRequest.value = request
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(error: CharSequence?) {
|
||||||
|
warn("BLE selection service failed $error")
|
||||||
|
}
|
||||||
|
}, null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val devices = object : MutableLiveData<MutableMap<String, DeviceListEntry>>(mutableMapOf()) {
|
val devices = object : MutableLiveData<MutableMap<String, DeviceListEntry>>(mutableMapOf()) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -339,7 +431,7 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
||||||
*/
|
*/
|
||||||
override fun onInactive() {
|
override fun onInactive() {
|
||||||
super.onInactive()
|
super.onInactive()
|
||||||
// stopScan()
|
stopScan()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -348,25 +440,24 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
||||||
fun onSelected(activity: MainActivity, it: DeviceListEntry): Boolean {
|
fun onSelected(activity: MainActivity, it: DeviceListEntry): Boolean {
|
||||||
// If the device is paired, let user select it, otherwise start the pairing flow
|
// If the device is paired, let user select it, otherwise start the pairing flow
|
||||||
if (it.bonded) {
|
if (it.bonded) {
|
||||||
changeScanSelection(activity, it.address)
|
changeScanSelection(activity, it.fullAddress)
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
// Handle requestng USB or bluetooth permissions for the device
|
// Handle requestng USB or bluetooth permissions for the device
|
||||||
debug("Requesting permissions for the device")
|
debug("Requesting permissions for the device")
|
||||||
|
|
||||||
exceptionReporter {
|
exceptionReporter {
|
||||||
val bleAddress = it.bluetoothAddress
|
if (it.isBLE) {
|
||||||
if (bleAddress != null) {
|
|
||||||
// Request bonding for bluetooth
|
// Request bonding for bluetooth
|
||||||
// We ignore missing BT adapters, because it lets us run on the emulator
|
// We ignore missing BT adapters, because it lets us run on the emulator
|
||||||
bluetoothAdapter
|
bluetoothAdapter
|
||||||
?.getRemoteDevice(bleAddress)?.let { device ->
|
?.getRemoteDevice(it.fullAddress)?.let { device ->
|
||||||
requestBonding(activity, device) { state ->
|
requestBonding(activity, device) { state ->
|
||||||
if (state == BOND_BONDED) {
|
if (state == BOND_BONDED) {
|
||||||
errorText.value = activity.getString(R.string.pairing_completed)
|
errorText.value = activity.getString(R.string.pairing_completed)
|
||||||
changeScanSelection(
|
changeScanSelection(
|
||||||
activity,
|
activity,
|
||||||
it.address
|
it.fullAddress
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
errorText.value =
|
errorText.value =
|
||||||
|
|
@ -380,7 +471,7 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (it.isSerial) {
|
if (it.isUSB) {
|
||||||
it as USBDeviceListEntry
|
it as USBDeviceListEntry
|
||||||
|
|
||||||
val ACTION_USB_PERMISSION = "com.geeksville.mesh.USB_PERMISSION"
|
val ACTION_USB_PERMISSION = "com.geeksville.mesh.USB_PERMISSION"
|
||||||
|
|
@ -399,7 +490,7 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
info("User approved USB access")
|
info("User approved USB access")
|
||||||
changeScanSelection(activity, it.address)
|
changeScanSelection(activity, it.fullAddress)
|
||||||
|
|
||||||
// Force the GUI to redraw
|
// Force the GUI to redraw
|
||||||
devices.value = devices.value
|
devices.value = devices.value
|
||||||
|
|
@ -447,20 +538,13 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private val scanModel: BTScanModel by activityViewModels()
|
private val scanModel: BTScanModel by activityViewModels()
|
||||||
|
private val bluetoothViewModel: BluetoothViewModel by activityViewModels()
|
||||||
private val model: UIViewModel by activityViewModels()
|
private val model: UIViewModel by activityViewModels()
|
||||||
|
|
||||||
// FIXME - move this into a standard GUI helper class
|
// FIXME - move this into a standard GUI helper class
|
||||||
private val guiJob = Job()
|
private val guiJob = Job()
|
||||||
private val mainScope = CoroutineScope(Dispatchers.Main + guiJob)
|
private val mainScope = CoroutineScope(Dispatchers.Main + guiJob)
|
||||||
|
|
||||||
private val hasCompanionDeviceApi: Boolean by lazy {
|
|
||||||
BluetoothInterface.hasCompanionDeviceApi(requireContext())
|
|
||||||
}
|
|
||||||
|
|
||||||
private val deviceManager: CompanionDeviceManager by lazy {
|
|
||||||
requireContext().getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
|
|
||||||
}
|
|
||||||
|
|
||||||
private val myActivity get() = requireActivity() as MainActivity
|
private val myActivity get() = requireActivity() as MainActivity
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
|
@ -472,6 +556,10 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||||
model.meshService?.let { service ->
|
model.meshService?.let { service ->
|
||||||
|
|
||||||
debug("User started firmware update")
|
debug("User started firmware update")
|
||||||
|
GeeksvilleApplication.analytics.track(
|
||||||
|
"firmware_update",
|
||||||
|
DataPair("content_type", "start")
|
||||||
|
)
|
||||||
binding.updateFirmwareButton.isEnabled = false // Disable until things complete
|
binding.updateFirmwareButton.isEnabled = false // Disable until things complete
|
||||||
binding.updateProgressBar.visibility = View.VISIBLE
|
binding.updateProgressBar.visibility = View.VISIBLE
|
||||||
binding.updateProgressBar.progress = 0 // start from scratch
|
binding.updateProgressBar.progress = 0 // start from scratch
|
||||||
|
|
@ -513,6 +601,10 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||||
} else
|
} else
|
||||||
when (progress) {
|
when (progress) {
|
||||||
ProgressSuccess -> {
|
ProgressSuccess -> {
|
||||||
|
GeeksvilleApplication.analytics.track(
|
||||||
|
"firmware_update",
|
||||||
|
DataPair("content_type", "success")
|
||||||
|
)
|
||||||
binding.scanStatusText.setText(R.string.update_successful)
|
binding.scanStatusText.setText(R.string.update_successful)
|
||||||
binding.updateProgressBar.visibility = View.GONE
|
binding.updateProgressBar.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
@ -521,6 +613,10 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||||
binding.updateProgressBar.visibility = View.GONE
|
binding.updateProgressBar.visibility = View.GONE
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
GeeksvilleApplication.analytics.track(
|
||||||
|
"firmware_update",
|
||||||
|
DataPair("content_type", "failure")
|
||||||
|
)
|
||||||
binding.scanStatusText.setText(R.string.update_failed)
|
binding.scanStatusText.setText(R.string.update_failed)
|
||||||
binding.updateProgressBar.visibility = View.VISIBLE
|
binding.updateProgressBar.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
@ -624,9 +720,12 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||||
regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
spinner.adapter = regionAdapter
|
spinner.adapter = regionAdapter
|
||||||
|
|
||||||
model.bluetoothEnabled.observe(viewLifecycleOwner) {
|
bluetoothViewModel.enabled.observe(viewLifecycleOwner) { enabled ->
|
||||||
if (it) binding.changeRadioButton.show()
|
if (enabled) {
|
||||||
else binding.changeRadioButton.hide()
|
binding.changeRadioButton.show()
|
||||||
|
scanModel.setupScan()
|
||||||
|
if (binding.scanStatusText.text == getString(R.string.requires_bluetooth)) updateNodeInfo()
|
||||||
|
} else binding.changeRadioButton.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
model.ownerName.observe(viewLifecycleOwner) { name ->
|
model.ownerName.observe(viewLifecycleOwner) { name ->
|
||||||
|
|
@ -653,12 +752,28 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||||
updateNodeInfo()
|
updateNodeInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scanModel.devices.observe(viewLifecycleOwner) { devices ->
|
||||||
|
updateDevicesButtons(devices)
|
||||||
|
}
|
||||||
|
|
||||||
scanModel.errorText.observe(viewLifecycleOwner) { errMsg ->
|
scanModel.errorText.observe(viewLifecycleOwner) { errMsg ->
|
||||||
if (errMsg != null) {
|
if (errMsg != null) {
|
||||||
binding.scanStatusText.text = errMsg
|
binding.scanStatusText.text = errMsg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// show the spinner when [spinner] is true
|
||||||
|
scanModel.spinner.observe(viewLifecycleOwner) { show ->
|
||||||
|
binding.scanProgressBar.visibility = if (show) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
scanModel.associationRequest.observe(viewLifecycleOwner) { request ->
|
||||||
|
request?.let {
|
||||||
|
associationResultLauncher.launch(request)
|
||||||
|
scanModel.clearAssociationRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
binding.updateFirmwareButton.setOnClickListener {
|
binding.updateFirmwareButton.setOnClickListener {
|
||||||
MaterialAlertDialogBuilder(requireContext())
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
.setMessage("${getString(R.string.update_firmware)}?")
|
.setMessage("${getString(R.string.update_firmware)}?")
|
||||||
|
|
@ -741,8 +856,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||||
b.text = device.name
|
b.text = device.name
|
||||||
b.id = View.generateViewId()
|
b.id = View.generateViewId()
|
||||||
b.isEnabled = enabled
|
b.isEnabled = enabled
|
||||||
b.isChecked =
|
b.isChecked = device.fullAddress == scanModel.selectedNotNull
|
||||||
device.address == scanModel.selectedNotNull && device.bonded // Only show checkbox if device is still paired
|
|
||||||
binding.deviceRadioGroup.addView(b)
|
binding.deviceRadioGroup.addView(b)
|
||||||
|
|
||||||
b.setOnClickListener {
|
b.setOnClickListener {
|
||||||
|
|
@ -751,24 +865,18 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||||
|
|
||||||
b.isChecked =
|
b.isChecked =
|
||||||
scanModel.onSelected(myActivity, device)
|
scanModel.onSelected(myActivity, device)
|
||||||
|
|
||||||
if (!b.isSelected) {
|
|
||||||
binding.scanStatusText.text = getString(R.string.please_pair)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
|
||||||
private fun updateDevicesButtons(devices: MutableMap<String, BTScanModel.DeviceListEntry>?) {
|
private fun updateDevicesButtons(devices: MutableMap<String, BTScanModel.DeviceListEntry>?) {
|
||||||
// Remove the old radio buttons and repopulate
|
// Remove the old radio buttons and repopulate
|
||||||
binding.deviceRadioGroup.removeAllViews()
|
binding.deviceRadioGroup.removeAllViews()
|
||||||
|
|
||||||
if (devices == null) return
|
if (devices == null) return
|
||||||
|
|
||||||
val adapter = scanModel.bluetoothAdapter
|
|
||||||
var hasShownOurDevice = false
|
var hasShownOurDevice = false
|
||||||
devices.values.forEach { device ->
|
devices.values.forEach { device ->
|
||||||
if (device.address == scanModel.selectedNotNull)
|
if (device.fullAddress == scanModel.selectedNotNull)
|
||||||
hasShownOurDevice = true
|
hasShownOurDevice = true
|
||||||
addDeviceButton(device, true)
|
addDeviceButton(device, true)
|
||||||
}
|
}
|
||||||
|
|
@ -779,150 +887,79 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||||
if (!hasShownOurDevice) {
|
if (!hasShownOurDevice) {
|
||||||
// Note: we pull this into a tempvar, because otherwise some other thread can change selectedAddress after our null check
|
// Note: we pull this into a tempvar, because otherwise some other thread can change selectedAddress after our null check
|
||||||
// and before use
|
// and before use
|
||||||
val bleAddr = scanModel.selectedBluetooth
|
val curAddr = scanModel.selectedAddress
|
||||||
|
if (curAddr != null) {
|
||||||
if (bleAddr != null && adapter != null && myActivity.hasConnectPermission()) {
|
val curDevice = scanModel.getDeviceListEntry(curAddr)
|
||||||
val bDevice =
|
|
||||||
adapter.getRemoteDevice(bleAddr)
|
|
||||||
if (bDevice.name != null) { // ignore nodes that node have a name, that means we've lost them since they appeared
|
|
||||||
val curDevice = BTScanModel.DeviceListEntry(
|
|
||||||
bDevice.name,
|
|
||||||
scanModel.selectedAddress!!,
|
|
||||||
bDevice.bondState == BOND_BONDED
|
|
||||||
)
|
|
||||||
addDeviceButton(
|
addDeviceButton(
|
||||||
curDevice,
|
curDevice,
|
||||||
model.isConnected.value == MeshService.ConnectionState.CONNECTED
|
model.isConnected.value == MeshService.ConnectionState.CONNECTED
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (scanModel.selectedUSB != null) {
|
|
||||||
// Must be a USB device, show a placeholder disabled entry
|
|
||||||
val curDevice = BTScanModel.DeviceListEntry(
|
|
||||||
scanModel.selectedUSB!!,
|
|
||||||
scanModel.selectedAddress!!,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
addDeviceButton(curDevice, false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get rid of the warning text once at least one device is paired.
|
// get rid of the warning text once at least one device is paired.
|
||||||
// If we are running on an emulator, always leave this message showing so we can test the worst case layout
|
// If we are running on an emulator, always leave this message showing so we can test the worst case layout
|
||||||
val curRadio = RadioInterfaceService.getBondedDeviceAddress(requireContext())
|
val curRadio = scanModel.selectedAddress
|
||||||
|
|
||||||
if (curRadio != null && !MockInterface.addressValid(requireContext(), "")) {
|
if (curRadio != null && !MockInterface.addressValid(requireContext(), "")) {
|
||||||
binding.warningNotPaired.visibility = View.GONE
|
binding.warningNotPaired.visibility = View.GONE
|
||||||
// binding.scanStatusText.text = getString(R.string.current_pair).format(curRadio)
|
// binding.scanStatusText.text = getString(R.string.current_pair).format(curRadio)
|
||||||
} else if (model.bluetoothEnabled.value == true){
|
} else if (bluetoothViewModel.enabled.value == true){
|
||||||
binding.warningNotPaired.visibility = View.VISIBLE
|
binding.warningNotPaired.visibility = View.VISIBLE
|
||||||
binding.scanStatusText.text = getString(R.string.not_paired_yet)
|
binding.scanStatusText.text = getString(R.string.not_paired_yet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initClassicScan() {
|
|
||||||
|
|
||||||
scanModel.devices.observe(viewLifecycleOwner) { devices ->
|
|
||||||
updateDevicesButtons(devices)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.changeRadioButton.setOnClickListener {
|
|
||||||
debug("User clicked changeRadioButton")
|
|
||||||
if (!myActivity.hasScanPermission()) {
|
|
||||||
myActivity.requestScanPermission()
|
|
||||||
} else {
|
|
||||||
checkLocationEnabled()
|
|
||||||
scanLeDevice()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// per https://developer.android.com/guide/topics/connectivity/bluetooth/find-ble-devices
|
// per https://developer.android.com/guide/topics/connectivity/bluetooth/find-ble-devices
|
||||||
private fun scanLeDevice() {
|
private fun scanLeDevice() {
|
||||||
var scanning = false
|
var scanning = false
|
||||||
val SCAN_PERIOD: Long = 5000 // Stops scanning after 5 seconds
|
val SCAN_PERIOD: Long = 10000 // Stops scanning after 10 seconds
|
||||||
|
|
||||||
if (!scanning) { // Stops scanning after a pre-defined scan period.
|
if (!scanning) { // Stops scanning after a pre-defined scan period.
|
||||||
Handler(Looper.getMainLooper()).postDelayed({
|
Handler(Looper.getMainLooper()).postDelayed({
|
||||||
scanning = false
|
scanning = false
|
||||||
binding.scanProgressBar.visibility = View.GONE
|
|
||||||
scanModel.stopScan()
|
scanModel.stopScan()
|
||||||
}, SCAN_PERIOD)
|
}, SCAN_PERIOD)
|
||||||
scanning = true
|
scanning = true
|
||||||
binding.scanProgressBar.visibility = View.VISIBLE
|
|
||||||
scanModel.startScan()
|
scanModel.startScan()
|
||||||
} else {
|
} else {
|
||||||
scanning = false
|
scanning = false
|
||||||
binding.scanProgressBar.visibility = View.GONE
|
|
||||||
scanModel.stopScan()
|
scanModel.stopScan()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startCompanionScan() {
|
@SuppressLint("MissingPermission")
|
||||||
// Disable the change button until our scan has some results
|
val associationResultLauncher = registerForActivityResult(
|
||||||
binding.changeRadioButton.isEnabled = false
|
ActivityResultContracts.StartIntentSenderForResult()
|
||||||
|
) {
|
||||||
// To skip filtering based on name and supported feature flags (UUIDs),
|
it.data
|
||||||
// don't include calls to setNamePattern() and addServiceUuid(),
|
?.getParcelableExtra<BluetoothDevice>(CompanionDeviceManager.EXTRA_DEVICE)
|
||||||
// respectively. This example uses Bluetooth.
|
?.let { device ->
|
||||||
// We only look for Mesh (rather than the full name) because NRF52 uses a very short name
|
scanModel.onSelected(
|
||||||
val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder()
|
myActivity,
|
||||||
.setNamePattern(Pattern.compile("Mesh.*"))
|
BTScanModel.DeviceListEntry(
|
||||||
// .addServiceUuid(ParcelUuid(RadioInterfaceService.BTM_SERVICE_UUID), null)
|
device.name,
|
||||||
.build()
|
"x${device.address}",
|
||||||
|
device.bondState == BOND_BONDED
|
||||||
// The argument provided in setSingleDevice() determines whether a single
|
|
||||||
// device name or a list of device names is presented to the user as
|
|
||||||
// pairing options.
|
|
||||||
val pairingRequest: AssociationRequest = AssociationRequest.Builder()
|
|
||||||
.addDeviceFilter(deviceFilter)
|
|
||||||
.setSingleDevice(false)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
// When the app tries to pair with the Bluetooth device, show the
|
|
||||||
// appropriate pairing request dialog to the user.
|
|
||||||
deviceManager.associate(
|
|
||||||
pairingRequest,
|
|
||||||
object : CompanionDeviceManager.Callback() {
|
|
||||||
override fun onDeviceFound(chooserLauncher: IntentSender) {
|
|
||||||
debug("Found one device - enabling changeRadioButton")
|
|
||||||
binding.changeRadioButton.isEnabled = true
|
|
||||||
binding.changeRadioButton.setOnClickListener {
|
|
||||||
debug("User clicked changeRadioButton")
|
|
||||||
try {
|
|
||||||
startIntentSenderForResult(
|
|
||||||
chooserLauncher,
|
|
||||||
MainActivity.SELECT_DEVICE_REQUEST_CODE, null, 0, 0, 0, null
|
|
||||||
)
|
)
|
||||||
} catch (ex: Throwable) {
|
|
||||||
errormsg("CompanionDevice startIntentSenderForResult error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(error: CharSequence?) {
|
|
||||||
warn("BLE selection service failed $error")
|
|
||||||
// changeDeviceSelection(myActivity, null) // deselect any device
|
|
||||||
}
|
|
||||||
}, null
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initModernScan() {
|
|
||||||
|
|
||||||
scanModel.devices.observe(viewLifecycleOwner) { devices ->
|
|
||||||
updateDevicesButtons(devices)
|
|
||||||
startCompanionScan()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
initCommonUI()
|
initCommonUI()
|
||||||
if (hasCompanionDeviceApi)
|
|
||||||
initModernScan()
|
binding.changeRadioButton.setOnClickListener {
|
||||||
else
|
debug("User clicked changeRadioButton")
|
||||||
initClassicScan()
|
if (!myActivity.hasScanPermission()) {
|
||||||
|
myActivity.requestScanPermission()
|
||||||
|
} else {
|
||||||
|
if (!scanModel.hasCompanionDeviceApi) checkLocationEnabled()
|
||||||
|
scanLeDevice()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user has not turned on location access throw up a toast warning
|
// If the user has not turned on location access throw up a toast warning
|
||||||
|
|
@ -1025,45 +1062,14 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||||
|
|
||||||
myActivity.registerReceiver(updateProgressReceiver, updateProgressFilter)
|
myActivity.registerReceiver(updateProgressReceiver, updateProgressFilter)
|
||||||
|
|
||||||
// Keep reminding user BLE is still off
|
|
||||||
val hasUSB = SerialInterface.findDrivers(myActivity).isNotEmpty()
|
|
||||||
if (!hasUSB) {
|
|
||||||
// Warn user if BLE is disabled
|
// Warn user if BLE is disabled
|
||||||
if (scanModel.bluetoothAdapter?.isEnabled != true) {
|
if (scanModel.selectedBluetooth != null && bluetoothViewModel.enabled.value == false) {
|
||||||
showSnackbar(getString(R.string.error_bluetooth))
|
Toast.makeText(
|
||||||
} else {
|
requireContext(),
|
||||||
if (binding.provideLocationCheckbox.isChecked)
|
getString(R.string.error_bluetooth),
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
} else if (binding.provideLocationCheckbox.isChecked)
|
||||||
checkLocationEnabled(getString(R.string.location_disabled))
|
checkLocationEnabled(getString(R.string.location_disabled))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
if (hasCompanionDeviceApi && myActivity.hasConnectPermission()
|
|
||||||
&& requestCode == MainActivity.SELECT_DEVICE_REQUEST_CODE
|
|
||||||
&& resultCode == Activity.RESULT_OK
|
|
||||||
) {
|
|
||||||
val deviceToPair: BluetoothDevice =
|
|
||||||
data?.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE)!!
|
|
||||||
|
|
||||||
// We only keep an association to one device at a time...
|
|
||||||
deviceManager.associations.forEach { old ->
|
|
||||||
if (deviceToPair.address != old) {
|
|
||||||
debug("Forgetting old BLE association ${old.anonymize}")
|
|
||||||
deviceManager.disassociate(old)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scanModel.onSelected(
|
|
||||||
myActivity,
|
|
||||||
BTScanModel.DeviceListEntry(
|
|
||||||
deviceToPair.name,
|
|
||||||
"x${deviceToPair.address}",
|
|
||||||
deviceToPair.bondState == BOND_BONDED
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,10 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.fragment.app.setFragmentResult
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.geeksville.android.Logging
|
import com.geeksville.android.Logging
|
||||||
|
|
@ -165,13 +167,29 @@ class UsersFragment : ScreenFragment("Users"), Logging {
|
||||||
holder.signalView.visibility = View.VISIBLE
|
holder.signalView.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
holder.itemView.setOnLongClickListener {
|
||||||
|
if (position > 0) {
|
||||||
|
debug("calling MessagesFragment filter:${n.user?.id}")
|
||||||
|
setFragmentResult(
|
||||||
|
"requestKey",
|
||||||
|
bundleOf("contactId" to n.user?.id, "contactName" to name)
|
||||||
|
)
|
||||||
|
parentFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.mainActivityLayout, MessagesFragment())
|
||||||
|
.addToBackStack(null)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var nodes = arrayOf<NodeInfo>()
|
private var nodes = arrayOf<NodeInfo>()
|
||||||
|
|
||||||
/// Called when our node DB changes
|
/// Called when our node DB changes
|
||||||
fun onNodesChanged(nodesIn: Collection<NodeInfo>) {
|
fun onNodesChanged(nodesIn: Array<NodeInfo>) {
|
||||||
nodes = nodesIn.toTypedArray()
|
if (nodesIn.size > 1)
|
||||||
|
nodesIn.sortWith(compareByDescending { it.lastHeard }, 1)
|
||||||
|
nodes = nodesIn
|
||||||
notifyDataSetChanged() // FIXME, this is super expensive and redraws all nodes
|
notifyDataSetChanged() // FIXME, this is super expensive and redraws all nodes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -210,9 +228,9 @@ class UsersFragment : ScreenFragment("Users"), Logging {
|
||||||
binding.nodeListView.adapter = nodesAdapter
|
binding.nodeListView.adapter = nodesAdapter
|
||||||
binding.nodeListView.layoutManager = LinearLayoutManager(requireContext())
|
binding.nodeListView.layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
|
||||||
model.nodeDB.nodes.observe(viewLifecycleOwner, {
|
model.nodeDB.nodes.observe(viewLifecycleOwner) {
|
||||||
nodesAdapter.onNodesChanged(it.values)
|
nodesAdapter.onNodesChanged(it.values.toTypedArray())
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 2930129e8eac348c094bbedeb929d86efafc2b62
|
Subproject commit f1476bf2f687a3926a98a9d8c86d5c2bba99c3cf
|
||||||
15
app/src/main/res/drawable/ic_twotone_delete_24.xml
Normal file
15
app/src/main/res/drawable/ic_twotone_delete_24.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<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="M8,9h8v10H8z"
|
||||||
|
android:strokeAlpha="0.3"
|
||||||
|
android:fillAlpha="0.3"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M15.5,4l-1,-1h-5l-1,1H5v2h14V4zM6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM8,9h8v10H8V9z"/>
|
||||||
|
</vector>
|
||||||
10
app/src/main/res/drawable/ic_twotone_select_all_24.xml
Normal file
10
app/src/main/res/drawable/ic_twotone_select_all_24.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<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="M3,5h2L5,3c-1.1,0 -2,0.9 -2,2zM3,13h2v-2L3,11v2zM7,21h2v-2L7,19v2zM3,9h2L5,7L3,7v2zM13,3h-2v2h2L13,3zM19,3v2h2c0,-1.1 -0.9,-2 -2,-2zM5,21v-2L3,19c0,1.1 0.9,2 2,2zM3,17h2v-2L3,15v2zM9,3L7,3v2h2L9,3zM11,21h2v-2h-2v2zM19,13h2v-2h-2v2zM19,21c1.1,0 2,-0.9 2,-2h-2v2zM19,9h2L21,7h-2v2zM19,17h2v-2h-2v2zM15,21h2v-2h-2v2zM15,5h2L17,3h-2v2zM7,17h10L17,7L7,7v10zM9,9h6v6L9,15L9,9z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -62,11 +62,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:tabIconTint="@color/tab_color_selector"
|
app:tabIconTint="@color/tab_color_selector"
|
||||||
app:tabIndicatorColor="@color/selectedColor"
|
app:tabIndicatorColor="@color/selectedColor" />
|
||||||
>
|
|
||||||
|
|
||||||
</com.google.android.material.tabs.TabLayout>
|
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
<androidx.viewpager2.widget.ViewPager2
|
<androidx.viewpager2.widget.ViewPager2
|
||||||
|
|
@ -74,10 +70,5 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
|
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
62
app/src/main/res/layout/adapter_contact_layout.xml
Normal file
62
app/src/main/res/layout/adapter_contact_layout.xml
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout 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="wrap_content">
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
style="@style/Widget.App.CardView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:cardCornerRadius="12dp"
|
||||||
|
app:cardElevation="2dp"
|
||||||
|
app:cardUseCompatPadding="true">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<com.google.android.material.chip.Chip
|
||||||
|
android:id="@+id/shortName"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:text="@string/some_username"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/longName"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:text="@string/unknown_username"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/shortName"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/lastMessageText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:autoLink="all"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:text="@string/sample_message"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/shortName"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/longName" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/lastMessageTime"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:contentDescription="@string/message_reception_time"
|
||||||
|
android:text="3 minutes ago"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
</LinearLayout>
|
||||||
|
|
@ -42,7 +42,6 @@
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:autoLink="all"
|
android:autoLink="all"
|
||||||
android:text="@string/sample_message"
|
android:text="@string/sample_message"
|
||||||
android:textIsSelectable="true"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/username"
|
app:layout_constraintStart_toEndOf="@id/username"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
@ -62,9 +61,9 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:contentDescription="@string/message_reception_time"
|
android:contentDescription="@string/message_reception_time"
|
||||||
android:text="3 minutes ago"
|
android:text="3 minutes ago"
|
||||||
|
android:textSize="12sp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@id/messageStatusIcon"
|
app:layout_constraintEnd_toStartOf="@id/messageStatusIcon"
|
||||||
app:layout_constraintTop_toBottomOf="@id/messageText" />
|
app:layout_constraintTop_toBottomOf="@id/messageText" />
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:layout_marginEnd="64dp"
|
android:layout_marginEnd="64dp"
|
||||||
android:hint="@string/channel_name"
|
android:hint="@string/channel_name"
|
||||||
|
app:counterEnabled="true"
|
||||||
|
app:counterMaxLength="11"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
@ -23,7 +25,6 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:digits="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890- "
|
android:digits="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890- "
|
||||||
android:imeOptions="actionDone"
|
android:imeOptions="actionDone"
|
||||||
android:maxLength="15"
|
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:text="@string/unset" />
|
android:text="@string/unset" />
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
|
||||||
19
app/src/main/res/layout/fragment_contacts.xml
Normal file
19
app/src/main/res/layout/fragment_contacts.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?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">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/contactsView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
@ -1,29 +1,53 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/colorAdvancedBackground">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
style="@style/MyToolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="@color/colorPrimary"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/messageTitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/channel_name"
|
||||||
|
android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</com.google.android.material.appbar.MaterialToolbar>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/messageListView"
|
android:id="@+id/messageListView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_margin="8dp"
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:contentDescription="@string/text_messages"
|
android:contentDescription="@string/text_messages"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/textInputLayout"
|
app:layout_constraintBottom_toTopOf="@+id/textInputLayout"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toBottomOf="@id/toolbar" />
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/textInputLayout"
|
android:id="@+id/textInputLayout"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="4dp"
|
||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="4dp"
|
||||||
android:hint="@string/send_text"
|
android:hint="@string/send_text"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/sendButton"
|
app:layout_constraintEnd_toStartOf="@+id/sendButton"
|
||||||
|
|
@ -33,7 +57,7 @@
|
||||||
android:id="@+id/messageInputText"
|
android:id="@+id/messageInputText"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inputType="textMultiLine"
|
android:inputType="textMultiLine|textCapSentences"
|
||||||
android:maxLength="228"
|
android:maxLength="228"
|
||||||
android:text="" />
|
android:text="" />
|
||||||
|
|
||||||
|
|
@ -43,7 +67,6 @@
|
||||||
android:id="@+id/sendButton"
|
android:id="@+id/sendButton"
|
||||||
android:layout_width="64dp"
|
android:layout_width="64dp"
|
||||||
android:layout_height="64dp"
|
android:layout_height="64dp"
|
||||||
android:layout_marginBottom="4dp"
|
|
||||||
android:contentDescription="@string/send_text"
|
android:contentDescription="@string/send_text"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
|
|
||||||
14
app/src/main/res/menu/menu_messages.xml
Normal file
14
app/src/main/res/menu/menu_messages.xml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item
|
||||||
|
android:id="@+id/deleteButton"
|
||||||
|
android:icon="@drawable/ic_twotone_delete_24"
|
||||||
|
android:title="@string/delete"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/selectAllButton"
|
||||||
|
android:icon="@drawable/ic_twotone_select_all_24"
|
||||||
|
android:title="@string/select_all"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
</menu>
|
||||||
|
|
@ -48,11 +48,11 @@
|
||||||
<string name="share">Sdílet</string>
|
<string name="share">Sdílet</string>
|
||||||
<string name="disconnected">Odpojeno</string>
|
<string name="disconnected">Odpojeno</string>
|
||||||
<string name="device_sleeping">Zařízení spí</string>
|
<string name="device_sleeping">Zařízení spí</string>
|
||||||
<string name="connected_count">Pripojeno: %s z %s je online</string>
|
<string name="connected_count">Pripojeno: %1$s z %2$s je online</string>
|
||||||
<string name="list_of_nodes">Seznam vysílačů v síti</string>
|
<string name="list_of_nodes">Seznam vysílačů v síti</string>
|
||||||
<string name="update_firmware">Aktualizace softwaru</string>
|
<string name="update_firmware">Aktualizace softwaru</string>
|
||||||
<string name="connected_to">Připojeno k vysílači (%s)</string>
|
<string name="connected_to">Připojeno k vysílači (%s)</string>
|
||||||
<string name="not_connected">Nepřipojeno, zvolte si vysílač</string>
|
<string name="not_connected">Nepřipojeno</string>
|
||||||
<string name="connected_sleeping">Připojené k uspanému vysílači.</string>
|
<string name="connected_sleeping">Připojené k uspanému vysílači.</string>
|
||||||
<string name="update_to">Aktualizovat na %s</string>
|
<string name="update_to">Aktualizovat na %s</string>
|
||||||
<string name="app_too_old">Aplikace je příliš stará</string>
|
<string name="app_too_old">Aplikace je příliš stará</string>
|
||||||
|
|
|
||||||
|
|
@ -1,142 +1,73 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<string name="action_settings">Ρυθμίσεις</string>
|
<string name="action_settings">Ρυθμίσεις</string>
|
||||||
|
|
||||||
<string name="channel_name">Όνομα Καναλιού</string>
|
<string name="channel_name">Όνομα Καναλιού</string>
|
||||||
|
|
||||||
<string name="channel_options">Επιλογές Καναλιού</string>
|
<string name="channel_options">Επιλογές Καναλιού</string>
|
||||||
|
|
||||||
<string name="share_button">Κοινή χρήση</string>
|
<string name="share_button">Κοινή χρήση</string>
|
||||||
|
|
||||||
<string name="qr_code">Κώδικας QR</string>
|
<string name="qr_code">Κώδικας QR</string>
|
||||||
|
|
||||||
<string name="unset">Αναίρεση</string>
|
<string name="unset">Αναίρεση</string>
|
||||||
|
|
||||||
<string name="connection_status">Κατάσταση Σύνδεσης</string>
|
<string name="connection_status">Κατάσταση Σύνδεσης</string>
|
||||||
|
|
||||||
<string name="application_icon">Εικονίδιο εφαρμογής </string>
|
<string name="application_icon">Εικονίδιο εφαρμογής </string>
|
||||||
|
|
||||||
<string name="unknown_username">Άγνωστο Όνομα Χρήστη</string>
|
<string name="unknown_username">Άγνωστο Όνομα Χρήστη</string>
|
||||||
|
|
||||||
<string name="user_avatar">Avatar Χρήστη</string>
|
<string name="user_avatar">Avatar Χρήστη</string>
|
||||||
|
|
||||||
|
|
||||||
<string name="sample_message">Βρήκα το σημείο, είναι διπλα στη τίγρη. Φοβάμαι λίγο.</string>
|
<string name="sample_message">Βρήκα το σημείο, είναι διπλα στη τίγρη. Φοβάμαι λίγο.</string>
|
||||||
|
|
||||||
<string name="send_text">Αποστολή κειμένου</string>
|
<string name="send_text">Αποστολή κειμένου</string>
|
||||||
|
|
||||||
<string name="warning_not_paired">Δεν έχετε κάνει ακόμη pair μια συσκευή συμβατή με Meshtastic με το τηλέφωνο. Παρακαλώ κάντε pair μια συσκευή και ορίστε το όνομα χρήστη.\n\nΗ εφαρμογή ανοιχτού κώδικα βρίσκεται σε alpha-testing, αν εντοπίσετε προβλήματα παρακαλώ δημοσιεύστε τα στο forum: meshtastic.discourse.group.\n\nΠερισσότερες πληροφορίες στην ιστοσελίδα - www.meshtastic.org.</string>
|
<string name="warning_not_paired">Δεν έχετε κάνει ακόμη pair μια συσκευή συμβατή με Meshtastic με το τηλέφωνο. Παρακαλώ κάντε pair μια συσκευή και ορίστε το όνομα χρήστη.\n\nΗ εφαρμογή ανοιχτού κώδικα βρίσκεται σε alpha-testing, αν εντοπίσετε προβλήματα παρακαλώ δημοσιεύστε τα στο forum: meshtastic.discourse.group.\n\nΠερισσότερες πληροφορίες στην ιστοσελίδα - www.meshtastic.org.</string>
|
||||||
|
|
||||||
<string name="username_unset">Όνομα Χρήστη αναιρέθηκε</string>
|
<string name="username_unset">Όνομα Χρήστη αναιρέθηκε</string>
|
||||||
|
|
||||||
<string name="your_name">Όνομα</string>
|
<string name="your_name">Όνομα</string>
|
||||||
|
|
||||||
<string name="analytics_okay">Ανώνυμα στατιστικά χρήσης και αναφορές crash.</string>
|
<string name="analytics_okay">Ανώνυμα στατιστικά χρήσης και αναφορές crash.</string>
|
||||||
|
|
||||||
<string name="looking_for_meshtastic_devices">Αναζήτηση συσκευών Meshtastic …</string>
|
<string name="looking_for_meshtastic_devices">Αναζήτηση συσκευών Meshtastic …</string>
|
||||||
|
|
||||||
<string name="requires_bluetooth">Η εφαρμογή απαιτεί bluetooth πρόσβαση. Παρακαλώ παρέχεται σχετική άδεια χρήσης στις ρυθμίσεις του android.</string>
|
<string name="requires_bluetooth">Η εφαρμογή απαιτεί bluetooth πρόσβαση. Παρακαλώ παρέχεται σχετική άδεια χρήσης στις ρυθμίσεις του android.</string>
|
||||||
|
|
||||||
<string name="error_bluetooth">Σφάλμα - η εφαρμογή απαιτεί bluetooth</string>
|
<string name="error_bluetooth">Σφάλμα - η εφαρμογή απαιτεί bluetooth</string>
|
||||||
|
|
||||||
<string name="starting_pairing">Αρχή pairing</string>
|
<string name="starting_pairing">Αρχή pairing</string>
|
||||||
|
|
||||||
<string name="pairing_failed">Pairing απέτυχε</string>
|
<string name="pairing_failed">Pairing απέτυχε</string>
|
||||||
|
|
||||||
<string name="url_for_join">Διεύθυνση URL για συμμετοχή σε Meshtastic mesh</string>
|
<string name="url_for_join">Διεύθυνση URL για συμμετοχή σε Meshtastic mesh</string>
|
||||||
|
|
||||||
<string name="accept">Αποδοχή</string>
|
<string name="accept">Αποδοχή</string>
|
||||||
|
|
||||||
<string name="cancel">Ακύρωση</string>
|
<string name="cancel">Ακύρωση</string>
|
||||||
|
|
||||||
<string name="change_channel">Αλλαγή καναλιού</string>
|
<string name="change_channel">Αλλαγή καναλιού</string>
|
||||||
|
|
||||||
<string name="are_you_sure_channel">Είστε βέβαιοι ότι θέλετε να αλλάξετε κανάλι? Η επικοινωνία με άλλες συσκευές θα σταματήσεις μέχρι να μοιραστείτε τις ρυθμίσεις του νέου καναλιού.</string>
|
<string name="are_you_sure_channel">Είστε βέβαιοι ότι θέλετε να αλλάξετε κανάλι? Η επικοινωνία με άλλες συσκευές θα σταματήσεις μέχρι να μοιραστείτε τις ρυθμίσεις του νέου καναλιού.</string>
|
||||||
|
|
||||||
<string name="new_channel_rcvd">Λήψη URL νέου καναλιού</string>
|
<string name="new_channel_rcvd">Λήψη URL νέου καναλιού</string>
|
||||||
|
|
||||||
<string name="do_you_want_switch">Θέλετε να αλλάξετε ‘%s’ κανάλι?</string>
|
<string name="do_you_want_switch">Θέλετε να αλλάξετε ‘%s’ κανάλι?</string>
|
||||||
|
|
||||||
<string name="map_not_allowed">Έχετε απενεργοποιήσει την ανάλυση δεδομένων. Δυστυχώς ο πάροχος χαρτών μας (mapbox) απαιτεί η ανάλυση δεδομένων να επιτρέπεται στο ‘δωρεάν’ πακέτο. Επομένως έχουμε απενεργοποιήσει το χάρτη.\n\n
|
<string name="map_not_allowed">Έχετε απενεργοποιήσει την ανάλυση δεδομένων. Δυστυχώς ο πάροχος χαρτών μας (mapbox) απαιτεί η ανάλυση δεδομένων να επιτρέπεται στο ‘δωρεάν’ πακέτο. Επομένως έχουμε απενεργοποιήσει το χάρτη.\n\n
|
||||||
Αν επιθυμείτε να δείτε το χάρτη, θα πρέπει να ενεργοποιήσετε την ανάλυση δεδομένων στις Ρυθμίσεις (επίσης - προσωρινά - θα πρέπει να επανεκκινήσετε την εφαρμογή).\n\n
|
Αν επιθυμείτε να δείτε το χάρτη, θα πρέπει να ενεργοποιήσετε την ανάλυση δεδομένων στις Ρυθμίσεις (επίσης - προσωρινά - θα πρέπει να επανεκκινήσετε την εφαρμογή).\n\n
|
||||||
Αν θεωρείτε ότι πρέπει να πληρώνουμε το mapbox (η να αλλάξουμε πάροχο χαρτών), παρακαλώ δημοσιεύστε στο meshtastic.discourse.group</string>
|
Αν θεωρείτε ότι πρέπει να πληρώνουμε το mapbox (η να αλλάξουμε πάροχο χαρτών), παρακαλώ δημοσιεύστε στο meshtastic.discourse.group</string>
|
||||||
|
|
||||||
<string name="permission_missing">Λείπει μια απαιτούμενη άδεια, Meshtastic δεν θα λειτοργεί σωστά. Ενεργοποιήστε τις ρυθμίσεις εφαρμογής Android.</string>
|
<string name="permission_missing">Λείπει μια απαιτούμενη άδεια, Meshtastic δεν θα λειτοργεί σωστά. Ενεργοποιήστε τις ρυθμίσεις εφαρμογής Android.</string>
|
||||||
|
|
||||||
<string name="radio_sleeping">Radio σε κατάσταση ύπνου, δεν γίνεται αλλαγή καναλιού</string>
|
<string name="radio_sleeping">Radio σε κατάσταση ύπνου, δεν γίνεται αλλαγή καναλιού</string>
|
||||||
|
|
||||||
<string name="report_bug">Αναφορά Bug</string>
|
<string name="report_bug">Αναφορά Bug</string>
|
||||||
|
|
||||||
<string name="report_a_bug">Αναφέρετε ένα bug</string>
|
<string name="report_a_bug">Αναφέρετε ένα bug</string>
|
||||||
|
|
||||||
<string name="report_bug_text">Είστε σίγουροι ότι θέλετε να αναφέρετε ένα bug? Μετά την αναφορά δημοσιεύστε στο meshtastic.discourse.group ώστε να συνδέσουμε την αναφορά με το συμβάν.</string>
|
<string name="report_bug_text">Είστε σίγουροι ότι θέλετε να αναφέρετε ένα bug? Μετά την αναφορά δημοσιεύστε στο meshtastic.discourse.group ώστε να συνδέσουμε την αναφορά με το συμβάν.</string>
|
||||||
|
|
||||||
<string name="report">Αναφορά</string>
|
<string name="report">Αναφορά</string>
|
||||||
|
|
||||||
<string name="select_radio">Επιλογή radio</string>
|
<string name="select_radio">Επιλογή radio</string>
|
||||||
|
|
||||||
<string name="current_pair">Έχετε κάνει pair με radio %s</string>
|
<string name="current_pair">Έχετε κάνει pair με radio %s</string>
|
||||||
|
|
||||||
<string name="not_paired_yet">Δεν έχετε κάνει pair με radio ακόμη.</string>
|
<string name="not_paired_yet">Δεν έχετε κάνει pair με radio ακόμη.</string>
|
||||||
|
|
||||||
<string name="change_radio">Αλλαγή radio</string>
|
<string name="change_radio">Αλλαγή radio</string>
|
||||||
|
|
||||||
<string name="please_pair">Παρακαλώ κάντε pair μια συσκευή στις ρυθμίσεις Android.</string>
|
<string name="please_pair">Παρακαλώ κάντε pair μια συσκευή στις ρυθμίσεις Android.</string>
|
||||||
|
|
||||||
<string name="pairing_completed">Η διαδικασία pairing ολοκληρώθηκε, εκκίνηση υπηρεσίας</string>
|
<string name="pairing_completed">Η διαδικασία pairing ολοκληρώθηκε, εκκίνηση υπηρεσίας</string>
|
||||||
|
|
||||||
<string name="pairing_failed_try_again">Η διαδικασία pairing απέτυχε, παρακαλώ επιλέξτε πάλι</string>
|
<string name="pairing_failed_try_again">Η διαδικασία pairing απέτυχε, παρακαλώ επιλέξτε πάλι</string>
|
||||||
|
|
||||||
<string name="location_disabled">Ο εντοπισμός τοποθεσίας είναι απενεργοποιημένος, δε μπορούμε να μοιραστούμε τη θέση σας με το mesh.</string>
|
<string name="location_disabled">Ο εντοπισμός τοποθεσίας είναι απενεργοποιημένος, δε μπορούμε να μοιραστούμε τη θέση σας με το mesh.</string>
|
||||||
|
|
||||||
<string name="share">Κοινοποίηση</string>
|
<string name="share">Κοινοποίηση</string>
|
||||||
|
|
||||||
<string name="disconnected">Αποσυνδεδεμένο</string>
|
<string name="disconnected">Αποσυνδεδεμένο</string>
|
||||||
|
|
||||||
<string name="device_sleeping">Συσκευή σε ύπνωση</string>
|
<string name="device_sleeping">Συσκευή σε ύπνωση</string>
|
||||||
|
<string name="connected_count">Συνδεδεμένος: %1$s από %2$s online</string>
|
||||||
<string name="connected_count">Συνδεδεμένος: %s από %s online</string>
|
|
||||||
|
|
||||||
<string name="list_of_nodes">Λίστα κόμβων δικτύου</string>
|
<string name="list_of_nodes">Λίστα κόμβων δικτύου</string>
|
||||||
|
|
||||||
<string name="update_firmware">Αναβάθμιση Firmware</string>
|
<string name="update_firmware">Αναβάθμιση Firmware</string>
|
||||||
|
|
||||||
<string name="connected">Συνδεδεμένο στο radio</string>
|
<string name="connected">Συνδεδεμένο στο radio</string>
|
||||||
|
|
||||||
<string name="connected_to">Συνδεδεμένο στο radio (%s)</string>
|
<string name="connected_to">Συνδεδεμένο στο radio (%s)</string>
|
||||||
|
<string name="not_connected">Αποσυνδεδεμένο</string>
|
||||||
<string name="not_connected">Αποσυνδεδεμένο, επιλέξτε radio </string>
|
|
||||||
|
|
||||||
<string name="connected_sleeping">Συνδεδεμένο στο radio, αλλά βρίσκεται σε ύπνωση</string>
|
<string name="connected_sleeping">Συνδεδεμένο στο radio, αλλά βρίσκεται σε ύπνωση</string>
|
||||||
|
|
||||||
<string name="update_to">Αναβάθμιση σε %s</string>
|
<string name="update_to">Αναβάθμιση σε %s</string>
|
||||||
|
|
||||||
<string name="app_too_old">Εφαρμογή πολύ παλαιά</string>
|
<string name="app_too_old">Εφαρμογή πολύ παλαιά</string>
|
||||||
|
|
||||||
<string name="must_update">Πρέπει να ενημερώσετε την εφαρμογή μέσω Google Play store (ή Github). Είναι πολύ παλαιά ώστε να συνδεθεί με το radio.</string>
|
<string name="must_update">Πρέπει να ενημερώσετε την εφαρμογή μέσω Google Play store (ή Github). Είναι πολύ παλαιά ώστε να συνδεθεί με το radio.</string>
|
||||||
|
|
||||||
<string name="none">Κανένα (απενεργοποιημένο)</string>
|
<string name="none">Κανένα (απενεργοποιημένο)</string>
|
||||||
|
|
||||||
<string name="modem_config_short">Μικρή εμβέλεια (αλλά γρήγορο)</string>
|
<string name="modem_config_short">Μικρή εμβέλεια (αλλά γρήγορο)</string>
|
||||||
|
|
||||||
<string name="modem_config_medium">Μεσαία εμβέλεια (αλλά γρήγορο)</string>
|
<string name="modem_config_medium">Μεσαία εμβέλεια (αλλά γρήγορο)</string>
|
||||||
|
|
||||||
<string name="modem_config_long">Μεγάλη εμβέλεια (αλλά αργό)</string>
|
<string name="modem_config_long">Μεγάλη εμβέλεια (αλλά αργό)</string>
|
||||||
|
|
||||||
<string name="modem_config_very_long">Πολύ μεγάλη εμβέλεια (αλλά αργό)</string>
|
<string name="modem_config_very_long">Πολύ μεγάλη εμβέλεια (αλλά αργό)</string>
|
||||||
|
|
||||||
<string name="modem_config_unrecognized">ΜΗ ΑΝΑΓΝΩΡΙΣΙΜΟ</string>
|
<string name="modem_config_unrecognized">ΜΗ ΑΝΑΓΝΩΡΙΣΙΜΟ</string>
|
||||||
|
|
||||||
<string name="meshtastic_service_notifications">Ειδοποιήσεις Υπηρεσίας Meshtastic</string>
|
<string name="meshtastic_service_notifications">Ειδοποιήσεις Υπηρεσίας Meshtastic</string>
|
||||||
|
|
||||||
<string name="location_disabled_warning">Πρέπει να ενεργοποιήσετε τις υπηρεσίες εντοπισμού τοποθεσίας στις ρυθμίσεις Android</string>
|
<string name="location_disabled_warning">Πρέπει να ενεργοποιήσετε τις υπηρεσίες εντοπισμού τοποθεσίας στις ρυθμίσεις Android</string>
|
||||||
|
|
||||||
<string name="about">Σχετικά</string>
|
<string name="about">Σχετικά</string>
|
||||||
|
|
||||||
<string name="a_list_of_nodes_in_the_mesh">Λίστα κόμβων στο mesh</string>
|
<string name="a_list_of_nodes_in_the_mesh">Λίστα κόμβων στο mesh</string>
|
||||||
|
|
||||||
<string name="text_messages">Μηνύματα</string>
|
<string name="text_messages">Μηνύματα</string>
|
||||||
|
|
||||||
<string name="channel_invalid">Αυτό το κανάλι URL δεν είναι ορθό και δεν μπορεί να χρησιμοποιηθεί</string>
|
<string name="channel_invalid">Αυτό το κανάλι URL δεν είναι ορθό και δεν μπορεί να χρησιμοποιηθεί</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -45,12 +45,12 @@
|
||||||
<string name="share">Compartir</string>
|
<string name="share">Compartir</string>
|
||||||
<string name="disconnected">Desconectado</string>
|
<string name="disconnected">Desconectado</string>
|
||||||
<string name="device_sleeping">Dispositivo en reposo</string>
|
<string name="device_sleeping">Dispositivo en reposo</string>
|
||||||
<string name="connected_count">Conectado: %s de %s en línea</string>
|
<string name="connected_count">Conectado: %1$s de %2$s en línea</string>
|
||||||
<string name="list_of_nodes">Una lista de nodos en la red</string>
|
<string name="list_of_nodes">Una lista de nodos en la red</string>
|
||||||
<string name="update_firmware">Actualizar el firmware</string>
|
<string name="update_firmware">Actualizar el firmware</string>
|
||||||
<string name="connected">Conectado a la radio</string>
|
<string name="connected">Conectado a la radio</string>
|
||||||
<string name="connected_to">Conectado a la radio (%s)</string>
|
<string name="connected_to">Conectado a la radio (%s)</string>
|
||||||
<string name="not_connected">No está conectado seleccione la radio de abajo</string>
|
<string name="not_connected">No está conectado</string>
|
||||||
<string name="connected_sleeping">Conectado a la radio pero está en reposo</string>
|
<string name="connected_sleeping">Conectado a la radio pero está en reposo</string>
|
||||||
<string name="update_to">Actualizar a %s</string>
|
<string name="update_to">Actualizar a %s</string>
|
||||||
<string name="app_too_old">Es necesario actualizar la aplicación</string>
|
<string name="app_too_old">Es necesario actualizar la aplicación</string>
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,12 @@
|
||||||
<string name="share">Partager</string>
|
<string name="share">Partager</string>
|
||||||
<string name="disconnected">Déconnecté</string>
|
<string name="disconnected">Déconnecté</string>
|
||||||
<string name="device_sleeping">Appareil en veille</string>
|
<string name="device_sleeping">Appareil en veille</string>
|
||||||
<string name="connected_count">Connecté: %s sur %s en ligne</string>
|
<string name="connected_count">Connecté: %1$s sur %2$s en ligne</string>
|
||||||
<string name="list_of_nodes">Une liste de nœuds dans le réseau</string>
|
<string name="list_of_nodes">Une liste de nœuds dans le réseau</string>
|
||||||
<string name="update_firmware">Mise à jour du Firmware</string>
|
<string name="update_firmware">Mise à jour du Firmware</string>
|
||||||
<string name="connected">Connecté à une radio</string>
|
<string name="connected">Connecté à une radio</string>
|
||||||
<string name="connected_to">Connecté à la radio (%s)</string>
|
<string name="connected_to">Connecté à la radio (%s)</string>
|
||||||
<string name="not_connected">Non connecté, veuillez sélectionner une radio ci-dessous</string>
|
<string name="not_connected">Non connecté</string>
|
||||||
<string name="connected_sleeping">Connecté à la radio, mais en mode veille</string>
|
<string name="connected_sleeping">Connecté à la radio, mais en mode veille</string>
|
||||||
<string name="none">Aucun (désactivé)</string>
|
<string name="none">Aucun (désactivé)</string>
|
||||||
<string name="must_update">Vous devez mettre à jour l\'application sur le Google Play Store (ou Github). Cette version n\'est plus compatible avec la radio.</string>
|
<string name="must_update">Vous devez mettre à jour l\'application sur le Google Play Store (ou Github). Cette version n\'est plus compatible avec la radio.</string>
|
||||||
|
|
|
||||||
|
|
@ -46,12 +46,12 @@
|
||||||
<string name="share">Pataje</string>
|
<string name="share">Pataje</string>
|
||||||
<string name="disconnected">Dekonekte</string>
|
<string name="disconnected">Dekonekte</string>
|
||||||
<string name="device_sleeping">Aparèy ap dòmi</string>
|
<string name="device_sleeping">Aparèy ap dòmi</string>
|
||||||
<string name="connected_count">Konekte: %s nan %s disponib</string>
|
<string name="connected_count">Konekte: %1$s nan %2$s disponib</string>
|
||||||
<string name="list_of_nodes">Yon lis ne elektwonik nan rezo a</string>
|
<string name="list_of_nodes">Yon lis ne elektwonik nan rezo a</string>
|
||||||
<string name="update_firmware">Mete ajou mikrolojisyèl</string>
|
<string name="update_firmware">Mete ajou mikrolojisyèl</string>
|
||||||
<string name="connected">Konekte ak radyo</string>
|
<string name="connected">Konekte ak radyo</string>
|
||||||
<string name="connected_to">Konekte ak radyo (%s)</string>
|
<string name="connected_to">Konekte ak radyo (%s)</string>
|
||||||
<string name="not_connected">Pa konekte, chwazi radyo anba a</string>
|
<string name="not_connected">Pa konekte</string>
|
||||||
<string name="connected_sleeping">Konekte ak radyo, men li ap dòmi</string>
|
<string name="connected_sleeping">Konekte ak radyo, men li ap dòmi</string>
|
||||||
<string name="update_to">Mizajou %s</string>
|
<string name="update_to">Mizajou %s</string>
|
||||||
<string name="app_too_old">Aplikasyon twò ansyen</string>
|
<string name="app_too_old">Aplikasyon twò ansyen</string>
|
||||||
|
|
|
||||||
|
|
@ -47,16 +47,16 @@
|
||||||
<string name="share">Megosztás</string>
|
<string name="share">Megosztás</string>
|
||||||
<string name="disconnected">Szétkapcsolva</string>
|
<string name="disconnected">Szétkapcsolva</string>
|
||||||
<string name="device_sleeping">Az eszköz alszik</string>
|
<string name="device_sleeping">Az eszköz alszik</string>
|
||||||
<string name="connected_count">Kapcsolódva: %s a %s-ból(ből) elérhető</string>
|
<string name="connected_count">Kapcsolódva: %1$s a %2$s-ból(ből) elérhető</string>
|
||||||
<string name="list_of_nodes">Hálózati állomások listája</string>
|
<string name="list_of_nodes">Hálózati állomások listája</string>
|
||||||
<string name="update_firmware">Firmware frissítés</string>
|
<string name="update_firmware">Firmware frissítés</string>
|
||||||
<string name="connected">Kapcsolódva a rádióhoz</string>
|
<string name="connected">Kapcsolódva a rádióhoz</string>
|
||||||
<string name="connected_to">Kapcsolódva a(z) %s rádióhoz</string>
|
<string name="connected_to">Kapcsolódva a(z) %s rádióhoz</string>
|
||||||
<string name="not_connected">Nincs kapcsolat, válasszon egy rádiót alább</string>
|
<string name="not_connected">Nincs kapcsolat</string>
|
||||||
<string name="connected_sleeping">Kapcsolódva a rádióhoz, de az alvó üzemmódban van</string>
|
<string name="connected_sleeping">Kapcsolódva a rádióhoz, de az alvó üzemmódban van</string>
|
||||||
<string name="update_to">Frissítés %s verzióra</string>
|
<string name="update_to">Frissítés %s verzióra</string>
|
||||||
<string name="app_too_old">Az alkalmazás frissítése szükséges</string>
|
<string name="app_too_old">Az alkalmazás frissítése szükséges</string>
|
||||||
<string name="must_update">Frissítenie kell ezt az alkalmazást a Google Play áruházban (vagy a GitHub-ról), mert túl régi, hogy kommunikálni tudjob ezzel a rádió firmware-rel. Kérem olvassa el a tudnivalókat ebből a <a href="https://www.meshtastic.org/software/android-too-old.html">wiki</a>-ből.</string>
|
<string name="must_update">Frissítenie kell ezt az alkalmazást a Google Play áruházban (vagy a GitHub-ról), mert túl régi, hogy kommunikálni tudjob ezzel a rádió firmware-rel. Kérem olvassa el a tudnivalókat ebből a <a href="https://meshtastic.org/docs/software/android/android-installation">docs</a>-ből.</string>
|
||||||
<string name="none">Egyik sem (letiltás)</string>
|
<string name="none">Egyik sem (letiltás)</string>
|
||||||
<string name="modem_config_short">Rövid hatótáv (nagyon gyors)</string>
|
<string name="modem_config_short">Rövid hatótáv (nagyon gyors)</string>
|
||||||
<string name="modem_config_medium">Közepes hatótáv (gyors)</string>
|
<string name="modem_config_medium">Közepes hatótáv (gyors)</string>
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,11 @@ mapboxの有償プラン(または代替地図プロバイダ)を検討さ
|
||||||
<string name="share">シェア</string>
|
<string name="share">シェア</string>
|
||||||
<string name="disconnected">切断</string>
|
<string name="disconnected">切断</string>
|
||||||
<string name="device_sleeping">スリープ</string>
|
<string name="device_sleeping">スリープ</string>
|
||||||
<string name="connected_count">接続済み:%s人オンライン%s人中</string>
|
<string name="connected_count">接続済み:%1$s人オンライン%2$s人中</string>
|
||||||
<string name="list_of_nodes">ネットワーク内のノードリスト</string>
|
<string name="list_of_nodes">ネットワーク内のノードリスト</string>
|
||||||
<string name="update_firmware">ファームウェアアップデート</string>
|
<string name="update_firmware">ファームウェアアップデート</string>
|
||||||
<string name="connected_to">Meshtasticデバイスに接続しました。(%s)</string>
|
<string name="connected_to">Meshtasticデバイスに接続しました。(%s)</string>
|
||||||
<string name="not_connected">接続されていません。下記のMeshtasticデバイスを選択してください。</string>
|
<string name="not_connected">接続されていません</string>
|
||||||
<string name="connected_sleeping">接続しましたが、Meshtasticデバイスはスリープ状態です。</string>
|
<string name="connected_sleeping">接続しましたが、Meshtasticデバイスはスリープ状態です。</string>
|
||||||
<string name="update_to">%s更新</string>
|
<string name="update_to">%s更新</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -46,12 +46,12 @@
|
||||||
<string name="share">공유</string>
|
<string name="share">공유</string>
|
||||||
<string name="disconnected">연결 해제</string>
|
<string name="disconnected">연결 해제</string>
|
||||||
<string name="device_sleeping">장치 잠자기</string>
|
<string name="device_sleeping">장치 잠자기</string>
|
||||||
<string name="connected_count">연결: %s 온라인( 전체 %s)</string>
|
<string name="connected_count">연결: %1$s 온라인( 전체 %2$s)</string>
|
||||||
<string name="list_of_nodes">네트워크안은 모든 노드의 목록</string>
|
<string name="list_of_nodes">네트워크안은 모든 노드의 목록</string>
|
||||||
<string name="update_firmware">펌웨어 업데이트</string>
|
<string name="update_firmware">펌웨어 업데이트</string>
|
||||||
<string name="connected">라디오로 연결됨</string>
|
<string name="connected">라디오로 연결됨</string>
|
||||||
<string name="connected_to">라디오로 연결됨 (%s)</string>
|
<string name="connected_to">라디오로 연결됨 (%s)</string>
|
||||||
<string name="not_connected">연결되지 않음, 아래에서 라이오를 선택하세요.</string>
|
<string name="not_connected">연결되지 않음</string>
|
||||||
<string name="connected_sleeping">라디오에 연결됨, 해당 라이도는 잠자기중.</string>
|
<string name="connected_sleeping">라디오에 연결됨, 해당 라이도는 잠자기중.</string>
|
||||||
<string name="update_to">%s로 업데이트</string>
|
<string name="update_to">%s로 업데이트</string>
|
||||||
<string name="app_too_old">너무 오래된 앱</string>
|
<string name="app_too_old">너무 오래된 앱</string>
|
||||||
|
|
|
||||||
|
|
@ -48,12 +48,12 @@
|
||||||
<string name="share">Deel</string>
|
<string name="share">Deel</string>
|
||||||
<string name="disconnected">Niet verbonden</string>
|
<string name="disconnected">Niet verbonden</string>
|
||||||
<string name="device_sleeping">Apparaat in slaapstand</string>
|
<string name="device_sleeping">Apparaat in slaapstand</string>
|
||||||
<string name="connected_count">Verbonden: %s van %s online</string>
|
<string name="connected_count">Verbonden: %1$s van %2$s online</string>
|
||||||
<string name="list_of_nodes">Een lijst van de aansluitpunten in het netwerk</string>
|
<string name="list_of_nodes">Een lijst van de aansluitpunten in het netwerk</string>
|
||||||
<string name="update_firmware">Programma Updaten</string>
|
<string name="update_firmware">Programma Updaten</string>
|
||||||
<string name="connected">Verbonden met een radio</string>
|
<string name="connected">Verbonden met een radio</string>
|
||||||
<string name="connected_to">Verbonden met radio (%s)</string>
|
<string name="connected_to">Verbonden met radio (%s)</string>
|
||||||
<string name="not_connected">Niet verbonden, selecteer radio hieronder</string>
|
<string name="not_connected">Niet verbonden</string>
|
||||||
<string name="connected_sleeping">Verbonden met radio in slaapstand</string>
|
<string name="connected_sleeping">Verbonden met radio in slaapstand</string>
|
||||||
<string name="update_to">Updaten naar %s</string>
|
<string name="update_to">Updaten naar %s</string>
|
||||||
<string name="app_too_old">Applicatie te oud</string>
|
<string name="app_too_old">Applicatie te oud</string>
|
||||||
|
|
|
||||||
|
|
@ -48,12 +48,12 @@
|
||||||
<string name="share">Del</string>
|
<string name="share">Del</string>
|
||||||
<string name="disconnected">Frakoblet</string>
|
<string name="disconnected">Frakoblet</string>
|
||||||
<string name="device_sleeping">Enhet sover</string>
|
<string name="device_sleeping">Enhet sover</string>
|
||||||
<string name="connected_count">Tilkoblet: %s av %s på nett</string>
|
<string name="connected_count">Tilkoblet: %1$s av %2$s på nett</string>
|
||||||
<string name="list_of_nodes">En liste over noder i nettverket</string>
|
<string name="list_of_nodes">En liste over noder i nettverket</string>
|
||||||
<string name="update_firmware">Oppdater Firmware</string>
|
<string name="update_firmware">Oppdater Firmware</string>
|
||||||
<string name="connected">Tilkoblet radio</string>
|
<string name="connected">Tilkoblet radio</string>
|
||||||
<string name="connected_to">Tilkoblet til radio (%s)</string>
|
<string name="connected_to">Tilkoblet til radio (%s)</string>
|
||||||
<string name="not_connected">Ikke tilkoblet. velg radio nedenfor</string>
|
<string name="not_connected">Ikke tilkoblet</string>
|
||||||
<string name="connected_sleeping">Tilkoblet radio, men den sover</string>
|
<string name="connected_sleeping">Tilkoblet radio, men den sover</string>
|
||||||
<string name="update_to">Oppdater til %s</string>
|
<string name="update_to">Oppdater til %s</string>
|
||||||
<string name="app_too_old">Applikasjon for gammel</string>
|
<string name="app_too_old">Applikasjon for gammel</string>
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@
|
||||||
<string name="share">Udostępnij</string>
|
<string name="share">Udostępnij</string>
|
||||||
<string name="disconnected">Rozłączone</string>
|
<string name="disconnected">Rozłączone</string>
|
||||||
<string name="device_sleeping">Urządzenie uśpione.</string>
|
<string name="device_sleeping">Urządzenie uśpione.</string>
|
||||||
<string name="connected_count">Połączono: %s of %s online</string>
|
<string name="connected_count">Połączono: %1$s of %2$s online</string>
|
||||||
<string name="list_of_nodes">Lista użytkowników w sieci</string>
|
<string name="list_of_nodes">Lista użytkowników w sieci</string>
|
||||||
<string name="update_firmware">Aktualizuj oprogramowanie.</string>
|
<string name="update_firmware">Aktualizuj oprogramowanie.</string>
|
||||||
<string name="connected">Połączony z urządzeniem</string>
|
<string name="connected">Połączony z urządzeniem</string>
|
||||||
|
|
@ -62,7 +62,7 @@
|
||||||
<string name="connected_sleeping">Połączony z urządzeniem, ale jest w trybie uśpienia.</string>
|
<string name="connected_sleeping">Połączony z urządzeniem, ale jest w trybie uśpienia.</string>
|
||||||
<string name="update_to">Zaktualizuj do %s.</string>
|
<string name="update_to">Zaktualizuj do %s.</string>
|
||||||
<string name="app_too_old">Wymagana jest aktualizacja aplikacji.</string>
|
<string name="app_too_old">Wymagana jest aktualizacja aplikacji.</string>
|
||||||
<string name="must_update">Musisz zaktualizować tę aplikację w Google Play store (lub ręcznie przez Github). Wersja aplikacji jest za stara, aby komunikować się z urządzeniem. Więcej informacji na ten temat znajdziesz tu: <a href="https://www.meshtastic.org/software/android-too-old.html">wiki</a></string>
|
<string name="must_update">Musisz zaktualizować tę aplikację w Google Play store (lub ręcznie przez Github). Wersja aplikacji jest za stara, aby komunikować się z urządzeniem. Więcej informacji na ten temat znajdziesz tu: <a href="https://meshtastic.org/docs/software/android/android-installation">docs</a></string>
|
||||||
<string name="none">Żadne (Wyłącz)</string>
|
<string name="none">Żadne (Wyłącz)</string>
|
||||||
<string name="modem_config_short">Bliski zasięg (ale szybki transfer)</string>
|
<string name="modem_config_short">Bliski zasięg (ale szybki transfer)</string>
|
||||||
<string name="modem_config_medium">Średni zasięg (ale szybki transfer)</string>
|
<string name="modem_config_medium">Średni zasięg (ale szybki transfer)</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<string name="action_settings">Configurações</string>
|
<string name="action_settings">Configurações</string>
|
||||||
<string name="channel_name">Nome do canal</string>
|
<string name="channel_name">Nome do canal</string>
|
||||||
<string name="channel_options">Opções do canal</string>
|
<string name="channel_options">Opções do canal</string>
|
||||||
|
|
@ -48,16 +48,16 @@
|
||||||
<string name="share">Compartilhar</string>
|
<string name="share">Compartilhar</string>
|
||||||
<string name="disconnected">Desconectado</string>
|
<string name="disconnected">Desconectado</string>
|
||||||
<string name="device_sleeping">Dispositivo em suspensão (sleep)</string>
|
<string name="device_sleeping">Dispositivo em suspensão (sleep)</string>
|
||||||
<string name="connected_count">Conectado: %s de %s online</string>
|
<string name="connected_count">Conectado: %1$s de %2$s online</string>
|
||||||
<string name="list_of_nodes">Lista de dispositivos na rede</string>
|
<string name="list_of_nodes">Lista de dispositivos na rede</string>
|
||||||
<string name="update_firmware">Atualizar Firmware</string>
|
<string name="update_firmware">Atualizar Firmware</string>
|
||||||
<string name="connected">Conectado ao rádio</string>
|
<string name="connected">Conectado ao rádio</string>
|
||||||
<string name="connected_to">Conectado ao rádio (%s)</string>
|
<string name="connected_to">Conectado ao rádio (%s)</string>
|
||||||
<string name="not_connected">Não conectado, selecione um rádio abaixo</string>
|
<string name="not_connected">Não conectado</string>
|
||||||
<string name="connected_sleeping">Conectado ao rádio, mas ele está em suspensão (sleep)</string>
|
<string name="connected_sleeping">Conectado ao rádio, mas ele está em suspensão (sleep)</string>
|
||||||
<string name="update_to">Atualização para %s</string>
|
<string name="update_to">Atualização para %s</string>
|
||||||
<string name="app_too_old">Atualização do aplicativo necessária</string>
|
<string name="app_too_old">Atualização do aplicativo necessária</string>
|
||||||
<string name="must_update">Será necessário atualizar este aplicativo no Google Play (ou Github). Versão muito antiga para comunicar com o firmware do rádio. Favor consultar <a href="https://www.meshtastic.org/software/android-too-old.html">wiki</a>.</string>
|
<string name="must_update">Será necessário atualizar este aplicativo no Google Play (ou Github). Versão muito antiga para comunicar com o firmware do rádio. Favor consultar <a href="https://meshtastic.org/docs/software/android/android-installation">docs</a>.</string>
|
||||||
<string name="none">Nenhum (desabilitado)</string>
|
<string name="none">Nenhum (desabilitado)</string>
|
||||||
<string name="modem_config_short">Curto alcance / rápido</string>
|
<string name="modem_config_short">Curto alcance / rápido</string>
|
||||||
<string name="modem_config_medium">Médio alcance / rápido</string>
|
<string name="modem_config_medium">Médio alcance / rápido</string>
|
||||||
|
|
@ -113,7 +113,13 @@
|
||||||
<string name="allow_will_show">Permitir (exibe diálogo)</string>
|
<string name="allow_will_show">Permitir (exibe diálogo)</string>
|
||||||
<string name="provide_location_to_mesh">Fornecer localização para mesh</string>
|
<string name="provide_location_to_mesh">Fornecer localização para mesh</string>
|
||||||
<string name="camera_required">Permissão da câmera</string>
|
<string name="camera_required">Permissão da câmera</string>
|
||||||
<string name="why_camera_required">Precisamos acessar a câmera para escanear códigos QR. Nenhuma foto ou video são armazenados.</string>
|
<string name="why_camera_required">Precisamos acessar a câmera para escanear códigos QR. Nenhuma foto ou vídeo são armazenados.</string>
|
||||||
<string name="modem_config_slow_short">Curto alcance / lento</string>
|
<string name="modem_config_slow_short">Curto alcance / lento</string>
|
||||||
<string name="modem_config_slow_medium">Médio alcance / lento</string>
|
<string name="modem_config_slow_medium">Médio alcance / lento</string>
|
||||||
|
<plurals name="delete_messages">
|
||||||
|
<item quantity="one" tools:ignore="ImpliedQuantity">Excluir mensagem?</item>
|
||||||
|
<item quantity="other">Excluir %s mensagens?</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="delete">Excluir</string>
|
||||||
|
<string name="select_all">Selecionar tudo</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<resources>
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<string name="action_settings">Configurações</string>
|
<string name="action_settings">Configurações</string>
|
||||||
<string name="channel_name">Nome do Canal</string>
|
<string name="channel_name">Nome do Canal</string>
|
||||||
<string name="channel_options">Opções do Canal</string>
|
<string name="channel_options">Opções do Canal</string>
|
||||||
|
|
@ -47,12 +47,12 @@
|
||||||
<string name="share">Partilha</string>
|
<string name="share">Partilha</string>
|
||||||
<string name="disconnected">Desconectado</string>
|
<string name="disconnected">Desconectado</string>
|
||||||
<string name="device_sleeping">Dispositivo a dormir</string>
|
<string name="device_sleeping">Dispositivo a dormir</string>
|
||||||
<string name="connected_count">Conectado: %s de %s online</string>
|
<string name="connected_count">Conectado: %1$s de %2$s online</string>
|
||||||
<string name="list_of_nodes">Lista de nós na rede</string>
|
<string name="list_of_nodes">Lista de nós na rede</string>
|
||||||
<string name="update_firmware">Atualizar Firmware</string>
|
<string name="update_firmware">Atualizar Firmware</string>
|
||||||
<string name="connected">Conectado ao rádio</string>
|
<string name="connected">Conectado ao rádio</string>
|
||||||
<string name="connected_to">Conectado ao rádio (%s)</string>
|
<string name="connected_to">Conectado ao rádio (%s)</string>
|
||||||
<string name="not_connected">Não conectado, escolha um rádio em baixo</string>
|
<string name="not_connected">Não conectado</string>
|
||||||
<string name="connected_sleeping">Conectado ao rádio, mas está a dormir</string>
|
<string name="connected_sleeping">Conectado ao rádio, mas está a dormir</string>
|
||||||
<string name="update_to">Atualização para %s</string>
|
<string name="update_to">Atualização para %s</string>
|
||||||
<string name="app_too_old">A aplicação é muito antiga</string>
|
<string name="app_too_old">A aplicação é muito antiga</string>
|
||||||
|
|
@ -112,8 +112,14 @@
|
||||||
<string name="cancel_no_radio">Cancelar (sem acesso ao rádio)</string>
|
<string name="cancel_no_radio">Cancelar (sem acesso ao rádio)</string>
|
||||||
<string name="allow_will_show">Permitir (exibe diálogo)</string>
|
<string name="allow_will_show">Permitir (exibe diálogo)</string>
|
||||||
<string name="provide_location_to_mesh">Fornecer localização para mesh</string>
|
<string name="provide_location_to_mesh">Fornecer localização para mesh</string>
|
||||||
<string name="why_camera_required">Precisamos acessar a câmera para escanear códigos QR. Nenhuma foto ou video são armazenados.</string>
|
<string name="why_camera_required">Precisamos acessar a câmera para escanear códigos QR. Nenhuma foto ou vídeo são armazenados.</string>
|
||||||
<string name="camera_required">Permissão da câmera</string>
|
<string name="camera_required">Permissão da câmera</string>
|
||||||
<string name="modem_config_slow_short">Curto alcance / lento</string>
|
<string name="modem_config_slow_short">Curto alcance / lento</string>
|
||||||
<string name="modem_config_slow_medium">Médio alcance / lento</string>
|
<string name="modem_config_slow_medium">Médio alcance / lento</string>
|
||||||
|
<plurals name="delete_messages">
|
||||||
|
<item quantity="one" tools:ignore="ImpliedQuantity">Excluir mensagem?</item>
|
||||||
|
<item quantity="other">Excluir %s mensagens?</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="delete">Excluir</string>
|
||||||
|
<string name="select_all">Selecionar tudo</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -48,12 +48,12 @@
|
||||||
<string name="share">Distribuie</string>
|
<string name="share">Distribuie</string>
|
||||||
<string name="disconnected">Deconectat</string>
|
<string name="disconnected">Deconectat</string>
|
||||||
<string name="device_sleeping">Dispozitiv în sleep mode</string>
|
<string name="device_sleeping">Dispozitiv în sleep mode</string>
|
||||||
<string name="connected_count">Connectat: %s din %s online</string>
|
<string name="connected_count">Connectat: %1$s din %2$s online</string>
|
||||||
<string name="list_of_nodes">O lista cu nodurile din rețea</string>
|
<string name="list_of_nodes">O lista cu nodurile din rețea</string>
|
||||||
<string name="update_firmware">Updateaza firmware-ul</string>
|
<string name="update_firmware">Updateaza firmware-ul</string>
|
||||||
<string name="connected">Connectat la dispozitiv</string>
|
<string name="connected">Connectat la dispozitiv</string>
|
||||||
<string name="connected_to">Conectat la dispozitivul (%s)</string>
|
<string name="connected_to">Conectat la dispozitivul (%s)</string>
|
||||||
<string name="not_connected">Neconectat, selectează dispozitivul din lista de jos</string>
|
<string name="not_connected">Neconectat</string>
|
||||||
<string name="connected_sleeping">Connectat la dispozitivi, dar e în modul de sleep</string>
|
<string name="connected_sleeping">Connectat la dispozitivi, dar e în modul de sleep</string>
|
||||||
<string name="update_to">Updateaza către %s</string>
|
<string name="update_to">Updateaza către %s</string>
|
||||||
<string name="app_too_old">Aplicație prea veche</string>
|
<string name="app_too_old">Aplicație prea veche</string>
|
||||||
|
|
|
||||||
|
|
@ -48,16 +48,16 @@
|
||||||
<string name="share">Zdieľať</string>
|
<string name="share">Zdieľať</string>
|
||||||
<string name="disconnected">Odpojené</string>
|
<string name="disconnected">Odpojené</string>
|
||||||
<string name="device_sleeping">Vysielač uspatý</string>
|
<string name="device_sleeping">Vysielač uspatý</string>
|
||||||
<string name="connected_count">Pripojený: %s z %s je online</string>
|
<string name="connected_count">Pripojený: %1$s z %2$s je online</string>
|
||||||
<string name="list_of_nodes">Zoznam vysielačov v sieti</string>
|
<string name="list_of_nodes">Zoznam vysielačov v sieti</string>
|
||||||
<string name="update_firmware">Aktualizácia firmvéru</string>
|
<string name="update_firmware">Aktualizácia firmvéru</string>
|
||||||
<string name="connected">Pripojené k vysielaču</string>
|
<string name="connected">Pripojené k vysielaču</string>
|
||||||
<string name="connected_to">Pripojené k vysielaču (%s)</string>
|
<string name="connected_to">Pripojené k vysielaču (%s)</string>
|
||||||
<string name="not_connected">Nepripojené, zvoľte si vysielač.</string>
|
<string name="not_connected">Nepripojené</string>
|
||||||
<string name="connected_sleeping">Pripojené k uspatému vysielaču.</string>
|
<string name="connected_sleeping">Pripojené k uspatému vysielaču.</string>
|
||||||
<string name="update_to">Aktualizovať na %s</string>
|
<string name="update_to">Aktualizovať na %s</string>
|
||||||
<string name="app_too_old">Aplikácia je príliš stará</string>
|
<string name="app_too_old">Aplikácia je príliš stará</string>
|
||||||
<string name="must_update">Musíte aktualizovať aplikáciu na Google Play store (alebo z Github). Je príliš stará pre komunikáciu s touto verziou firmvéru vysielača. Viac informácií k tejto téme nájdete na <a href="https://www.meshtastic.org/software/android-too-old.html">Meshtastic wiki</a>.</string>
|
<string name="must_update">Musíte aktualizovať aplikáciu na Google Play store (alebo z Github). Je príliš stará pre komunikáciu s touto verziou firmvéru vysielača. Viac informácií k tejto téme nájdete na <a href="https://meshtastic.org/docs/software/android/android-installation">Meshtastic docs</a>.</string>
|
||||||
<string name="none">Žiaden (zakázať)</string>
|
<string name="none">Žiaden (zakázať)</string>
|
||||||
<string name="rate_dialog_no_en">Nie, ďakujem</string>
|
<string name="rate_dialog_no_en">Nie, ďakujem</string>
|
||||||
<string name="rate_dialog_cancel_en">Pripomenúť neskôr</string>
|
<string name="rate_dialog_cancel_en">Pripomenúť neskôr</string>
|
||||||
|
|
|
||||||
|
|
@ -46,12 +46,12 @@
|
||||||
<string name="share">Deliti</string>
|
<string name="share">Deliti</string>
|
||||||
<string name="disconnected">Prekinjeno</string>
|
<string name="disconnected">Prekinjeno</string>
|
||||||
<string name="device_sleeping">Naprava je v "spanju"</string>
|
<string name="device_sleeping">Naprava je v "spanju"</string>
|
||||||
<string name="connected_count">Povezano: %s od %s je na mreži</string>
|
<string name="connected_count">Povezano: %1$s od %2$s je na mreži</string>
|
||||||
<string name="list_of_nodes">Seznam vozlišč v omrežju</string>
|
<string name="list_of_nodes">Seznam vozlišč v omrežju</string>
|
||||||
<string name="update_firmware">Posodobite vdelano programsko opremo</string>
|
<string name="update_firmware">Posodobite vdelano programsko opremo</string>
|
||||||
<string name="connected">Povezana z radiem</string>
|
<string name="connected">Povezana z radiem</string>
|
||||||
<string name="connected_to">Povezana z radiem (%s)</string>
|
<string name="connected_to">Povezana z radiem (%s)</string>
|
||||||
<string name="not_connected">Ni povezano. Izberite radio spodaj</string>
|
<string name="not_connected">Ni povezano</string>
|
||||||
<string name="connected_sleeping">Povezan z radiem, vendar radio "spi"</string>
|
<string name="connected_sleeping">Povezan z radiem, vendar radio "spi"</string>
|
||||||
<string name="update_to">Posodobi v %s</string>
|
<string name="update_to">Posodobi v %s</string>
|
||||||
<string name="app_too_old">Aplikacija je prestara</string>
|
<string name="app_too_old">Aplikacija je prestara</string>
|
||||||
|
|
|
||||||
|
|
@ -48,12 +48,12 @@
|
||||||
<string name="share">Paylaş</string>
|
<string name="share">Paylaş</string>
|
||||||
<string name="disconnected">Bağlantı sonlandı</string>
|
<string name="disconnected">Bağlantı sonlandı</string>
|
||||||
<string name="device_sleeping">Cihaz uyku durumunda</string>
|
<string name="device_sleeping">Cihaz uyku durumunda</string>
|
||||||
<string name="connected_count">Bağlandı: %s / %s online</string>
|
<string name="connected_count">Bağlandı: %1$s / %2$s online</string>
|
||||||
<string name="list_of_nodes">Ağdaki node listesi</string>
|
<string name="list_of_nodes">Ağdaki node listesi</string>
|
||||||
<string name="update_firmware">Yazılım güncelle</string>
|
<string name="update_firmware">Yazılım güncelle</string>
|
||||||
<string name="connected">Radyoya bağlandı</string>
|
<string name="connected">Radyoya bağlandı</string>
|
||||||
<string name="connected_to">(%s) telsizine bağlandı </string>
|
<string name="connected_to">(%s) telsizine bağlandı </string>
|
||||||
<string name="not_connected">Bağlı değil, aşağıdan bir radyo seçiniz</string>
|
<string name="not_connected">Bağlı değil</string>
|
||||||
<string name="connected_sleeping">Telsize bağlandı, ancak uyku durumunda</string>
|
<string name="connected_sleeping">Telsize bağlandı, ancak uyku durumunda</string>
|
||||||
<string name="update_to">%s\'e güncelle</string>
|
<string name="update_to">%s\'e güncelle</string>
|
||||||
<string name="app_too_old">Uygulama çok eski</string>
|
<string name="app_too_old">Uygulama çok eski</string>
|
||||||
|
|
|
||||||
|
|
@ -48,16 +48,16 @@
|
||||||
<string name="share">分享</string>
|
<string name="share">分享</string>
|
||||||
<string name="disconnected">断开连接</string>
|
<string name="disconnected">断开连接</string>
|
||||||
<string name="device_sleeping">设备休眠中</string>
|
<string name="device_sleeping">设备休眠中</string>
|
||||||
<string name="connected_count">连接: %s 中 %s 在线</string>
|
<string name="connected_count">连接: %1$s 中 %2$s 在线</string>
|
||||||
<string name="list_of_nodes">网络中节点列表</string>
|
<string name="list_of_nodes">网络中节点列表</string>
|
||||||
<string name="update_firmware">更新固件</string>
|
<string name="update_firmware">更新固件</string>
|
||||||
<string name="connected">连接设备</string>
|
<string name="connected">连接设备</string>
|
||||||
<string name="connected_to">连接到设备(%s)</string>
|
<string name="connected_to">连接到设备(%s)</string>
|
||||||
<string name="not_connected">未连接,请选择下方的设备</string>
|
<string name="not_connected">未连接</string>
|
||||||
<string name="connected_sleeping">已连接到设备,正在休眠中</string>
|
<string name="connected_sleeping">已连接到设备,正在休眠中</string>
|
||||||
<string name="update_to">更新到%s</string>
|
<string name="update_to">更新到%s</string>
|
||||||
<string name="app_too_old">需要应用程序更新</string>
|
<string name="app_too_old">需要应用程序更新</string>
|
||||||
<string name="must_update">您必须在 Google Play或Github上更新此应用程序.固件太旧,请阅读我们的 <a href="https://www.meshtastic.org/software/android-too-old.html">wiki</a> 这个话题.</string>
|
<string name="must_update">您必须在 Google Play或Github上更新此应用程序.固件太旧,请阅读我们的 <a href="https://meshtastic.org/docs/software/android/android-installation">docs</a> 这个话题.</string>
|
||||||
<string name="none">无(禁用)</string>
|
<string name="none">无(禁用)</string>
|
||||||
<string name="modem_config_short">短距离(速度快)</string>
|
<string name="modem_config_short">短距离(速度快)</string>
|
||||||
<string name="modem_config_medium">中等距离(速度快)</string>
|
<string name="modem_config_medium">中等距离(速度快)</string>
|
||||||
|
|
|
||||||
|
|
@ -52,16 +52,16 @@
|
||||||
<string name="share">Share</string>
|
<string name="share">Share</string>
|
||||||
<string name="disconnected">Disconnected</string>
|
<string name="disconnected">Disconnected</string>
|
||||||
<string name="device_sleeping">Device sleeping</string>
|
<string name="device_sleeping">Device sleeping</string>
|
||||||
<string name="connected_count">Connected: %s of %s online</string>
|
<string name="connected_count">Connected: %1$s of %2$s online</string>
|
||||||
<string name="list_of_nodes">A list of nodes in the network</string>
|
<string name="list_of_nodes">A list of nodes in the network</string>
|
||||||
<string name="update_firmware">Update Firmware</string>
|
<string name="update_firmware">Update Firmware</string>
|
||||||
<string name="connected">Connected to radio</string>
|
<string name="connected">Connected to radio</string>
|
||||||
<string name="connected_to">Connected to radio (%s)</string>
|
<string name="connected_to">Connected to radio (%s)</string>
|
||||||
<string name="not_connected">Not connected, select radio below</string>
|
<string name="not_connected">Not connected</string>
|
||||||
<string name="connected_sleeping">Connected to radio, but it is sleeping</string>
|
<string name="connected_sleeping">Connected to radio, but it is sleeping</string>
|
||||||
<string name="update_to">Update to %s</string>
|
<string name="update_to">Update to %s</string>
|
||||||
<string name="app_too_old">Application update required</string>
|
<string name="app_too_old">Application update required</string>
|
||||||
<string name="must_update">You must update this application on the app store (or Github). It is too old to talk to this radio firmware. Please read our <a href="https://www.meshtastic.org/software/android-too-old.html">wiki</a> on this topic.</string>
|
<string name="must_update">You must update this application on the app store (or Github). It is too old to talk to this radio firmware. Please read our <a href="https://meshtastic.org/docs/software/android/android-installation">docs</a> on this topic.</string>
|
||||||
<string name="none">None (disable)</string>
|
<string name="none">None (disable)</string>
|
||||||
<string name="modem_config_short">Short Range / Fast</string>
|
<string name="modem_config_short">Short Range / Fast</string>
|
||||||
<string name="modem_config_medium">Medium Range / Fast</string>
|
<string name="modem_config_medium">Medium Range / Fast</string>
|
||||||
|
|
@ -120,7 +120,10 @@
|
||||||
<string name="why_camera_required">We must be granted access to the camera to read QR codes. No pictures or videos will be saved.</string>
|
<string name="why_camera_required">We must be granted access to the camera to read QR codes. No pictures or videos will be saved.</string>
|
||||||
<string name="modem_config_slow_short">Short Range / Slow</string>
|
<string name="modem_config_slow_short">Short Range / Slow</string>
|
||||||
<string name="modem_config_slow_medium">Medium Range / Slow</string>
|
<string name="modem_config_slow_medium">Medium Range / Slow</string>
|
||||||
<string name="delete_selected_message">Delete selected message?</string>
|
<plurals name="delete_messages">
|
||||||
|
<item quantity="one">Delete message?</item>
|
||||||
|
<item quantity="other">Delete %s messages?</item>
|
||||||
|
</plurals>
|
||||||
<string name="delete">Delete</string>
|
<string name="delete">Delete</string>
|
||||||
<string name="delete_all_messages">Delete All Messages</string>
|
<string name="select_all">Select all</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -11,7 +11,8 @@
|
||||||
|
|
||||||
<item name="windowNoTitle">true</item>
|
<item name="windowNoTitle">true</item>
|
||||||
<item name="android:itemTextAppearance">@style/menu_item_color</item>
|
<item name="android:itemTextAppearance">@style/menu_item_color</item>
|
||||||
|
<item name="actionModeStyle">@style/MyActionMode</item>
|
||||||
|
<item name="windowActionModeOverlay">true</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme.Spinner">
|
<style name="AppTheme.Spinner">
|
||||||
|
|
@ -73,6 +74,12 @@
|
||||||
<item name="materialThemeOverlay">@style/MyThemeOverlay_Toolbar</item>
|
<item name="materialThemeOverlay">@style/MyThemeOverlay_Toolbar</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="MyActionMode" parent="Base.Widget.AppCompat.ActionMode">
|
||||||
|
<item name="background">@color/colorPrimary</item>
|
||||||
|
<item name="android:textSize">16sp</item>
|
||||||
|
<item name="android:textColorPrimary">@color/colorOnPrimary</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
|
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
|
||||||
// Set the splash screen background, animated icon, and animation duration.
|
// Set the splash screen background, animated icon, and animation duration.
|
||||||
<item name="windowSplashScreenBackground">@color/selectedColor</item>
|
<item name="windowSplashScreenBackground">@color/selectedColor</item>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.6.10'
|
ext.kotlin_version = '1.6.10'
|
||||||
ext.coroutines_version = "1.5.2"
|
ext.coroutines_version = "1.6.0"
|
||||||
ext.hilt_version = '2.40.5'
|
ext.hilt_version = '2.40.5'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
|
@ -10,7 +10,7 @@ buildscript {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.1.1'
|
classpath 'com.android.tools.build:gradle:7.1.2'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue