kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
Merge branch 'master' into dev-app-intro
commit
25de235a93
14
README.md
14
README.md
|
@ -14,14 +14,10 @@ The production version of our application is here:
|
|||
|
||||
[](https://play.google.com/store/apps/details?id=com.geeksville.mesh&referrer=utm_source%3Dgithub-android-readme)
|
||||
|
||||
But if you want the beta-test app now, we'd love to have your help testing. Three steps to opt-in to the test:
|
||||
To join the beta program for the app go to this [URL](https://play.google.com/apps/testing/com.geeksville.mesh) to opt-in to the alpha/beta test.
|
||||
If you encounter any problems or have questions, [post in the forum](https://meshtastic.discourse.group/) and we'll help.
|
||||
|
||||
1. Join [this Google group](https://groups.google.com/forum/#!forum/meshtastic-alpha-testers) with the account you use in Google Play. **Optional** - if you just want 'beta builds'
|
||||
not bleeding edge alpha test builds skip to the next step.
|
||||
2. Go to this [URL](https://play.google.com/apps/testing/com.geeksville.mesh) to opt-in to the alpha/beta test.
|
||||
3. If you encounter any problems or have questions, [post in the forum](https://meshtastic.discourse.group/) and we'll help.
|
||||
|
||||
The app is also distributed for Amazon Fire devices via the Amazon appstore: [](https://www.amazon.com/Geeksville-Industries-Meshtastic/dp/B08CY9394Q)
|
||||
The app is also distributed via F-Droid repo: [https://mesh.tastic.app/fdroid/repo](https://mesh.tastic.app/fdroid/repo)
|
||||
|
||||
However, if you must use 'raw' APKs you can get them from our [github releases](https://github.com/meshtastic/Meshtastic-Android/releases). This is not recommended because if you manually install an APK it will not automatically update.
|
||||
|
||||
|
@ -73,10 +69,6 @@ for verbose logging:
|
|||
adb shell setprop log.tag.FA VERBOSE
|
||||
```
|
||||
|
||||
## Publishing to google play
|
||||
|
||||
(Only supported if you are a core developer that needs to do releases)
|
||||
|
||||
# Credits
|
||||
|
||||
This project is the work of volunteers:
|
||||
|
|
|
@ -43,8 +43,8 @@ android {
|
|||
applicationId "com.geeksville.mesh"
|
||||
minSdkVersion 21 // The oldest emulator image I have tried is 22 (though 21 probably works)
|
||||
targetSdkVersion 30 // 30 can't work until an explicit location permissions dialog is added
|
||||
versionCode 20328 // format is Mmmss (where M is 1+the numeric major number
|
||||
versionName "1.3.28"
|
||||
versionCode 20330 // format is Mmmss (where M is 1+the numeric major number
|
||||
versionName "1.3.30"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
// per https://developer.android.com/studio/write/vector-asset-studio
|
||||
|
|
|
@ -140,7 +140,7 @@
|
|||
<!-- The QR codes to share channel settings are shared as meshtastic URLS
|
||||
|
||||
an approximate example:
|
||||
http://www.meshtastic.org/d/YXNkZnF3ZXJhc2RmcXdlcmFzZGZxd2Vy
|
||||
http://www.meshtastic.org/e/YXNkZnF3ZXJhc2RmcXdlcmFzZGZxd2Vy
|
||||
-->
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
|
@ -150,11 +150,11 @@
|
|||
<data
|
||||
android:scheme="https"
|
||||
android:host="www.meshtastic.org"
|
||||
android:pathPrefix="/d/" />
|
||||
android:pathPrefix="/e/" />
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="www.meshtastic.org"
|
||||
android:pathPrefix="/D/" />
|
||||
android:pathPrefix="/E/" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
|
|
|
@ -1080,6 +1080,15 @@ class MainActivity : BaseActivity(), Logging,
|
|||
R.id.show_intro -> {
|
||||
startActivity(Intent(this, AppIntroduction::class.java))
|
||||
return true
|
||||
}
|
||||
R.id.preferences_quick_chat -> {
|
||||
val fragmentManager: FragmentManager = supportFragmentManager
|
||||
val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction()
|
||||
val nameFragment = QuickChatSettingsFragment()
|
||||
fragmentTransaction.add(R.id.mainActivityLayout, nameFragment)
|
||||
fragmentTransaction.addToBackStack(null)
|
||||
fragmentTransaction.commit()
|
||||
return true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
|
|
@ -46,6 +46,16 @@ fun Context.hasCompanionDeviceApi(): Boolean =
|
|||
fun Context.hasGps(): Boolean =
|
||||
packageManager.hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS)
|
||||
|
||||
/**
|
||||
* return app install source (sideload = null)
|
||||
*/
|
||||
fun Context.installSource(): String? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
|
||||
packageManager.getInstallSourceInfo(packageName).installingPackageName
|
||||
else
|
||||
packageManager.getInstallerPackageName(packageName)
|
||||
}
|
||||
|
||||
/**
|
||||
* return a list of the permissions we don't have
|
||||
*/
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.geeksville.mesh.database
|
|||
|
||||
import android.app.Application
|
||||
import com.geeksville.mesh.database.dao.PacketDao
|
||||
import com.geeksville.mesh.database.dao.QuickChatActionDao
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
@ -20,4 +21,9 @@ class DatabaseModule {
|
|||
fun providePacketDao(database: MeshtasticDatabase): PacketDao {
|
||||
return database.packetDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideQuickChatActionDao(database: MeshtasticDatabase): QuickChatActionDao {
|
||||
return database.quickChatActionDao()
|
||||
}
|
||||
}
|
|
@ -5,11 +5,14 @@ import androidx.room.Database
|
|||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import com.geeksville.mesh.database.dao.PacketDao
|
||||
import com.geeksville.mesh.database.dao.QuickChatActionDao
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
|
||||
@Database(entities = [Packet::class], version = 1, exportSchema = false)
|
||||
@Database(entities = [Packet::class, QuickChatAction::class], version = 2, exportSchema = false)
|
||||
abstract class MeshtasticDatabase : RoomDatabase() {
|
||||
abstract fun packetDao(): PacketDao
|
||||
abstract fun quickChatActionDao(): QuickChatActionDao
|
||||
|
||||
companion object {
|
||||
fun getDatabase(context: Context): MeshtasticDatabase {
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
package com.geeksville.mesh.database
|
||||
|
||||
import com.geeksville.mesh.database.dao.QuickChatActionDao
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class QuickChatActionRepository @Inject constructor(private val quickChatDaoLazy: dagger.Lazy<QuickChatActionDao>) {
|
||||
private val quickChatActionDao by lazy {
|
||||
quickChatDaoLazy.get()
|
||||
}
|
||||
|
||||
suspend fun getAllActions(): Flow<List<QuickChatAction>> = withContext(Dispatchers.IO) {
|
||||
quickChatActionDao.getAll()
|
||||
}
|
||||
|
||||
suspend fun insert(action: QuickChatAction) = withContext(Dispatchers.IO) {
|
||||
quickChatActionDao.insert(action)
|
||||
}
|
||||
|
||||
suspend fun deleteAll() = withContext(Dispatchers.IO) {
|
||||
quickChatActionDao.deleteAll()
|
||||
}
|
||||
|
||||
suspend fun delete(action: QuickChatAction) = withContext(Dispatchers.IO) {
|
||||
quickChatActionDao.delete(action)
|
||||
}
|
||||
|
||||
suspend fun update(action: QuickChatAction) = withContext(Dispatchers.IO) {
|
||||
quickChatActionDao.update(action)
|
||||
}
|
||||
|
||||
suspend fun setItemPosition(uuid: Long, newPos: Int) = withContext(Dispatchers.IO) {
|
||||
quickChatActionDao.updateActionPosition(uuid, newPos)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package com.geeksville.mesh.database.dao
|
||||
|
||||
import androidx.room.*
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface QuickChatActionDao {
|
||||
|
||||
@Query("Select * from quick_chat order by position asc")
|
||||
fun getAll(): Flow<List<QuickChatAction>>
|
||||
|
||||
@Insert
|
||||
fun insert(action: QuickChatAction)
|
||||
|
||||
@Query("Delete from quick_chat")
|
||||
fun deleteAll()
|
||||
|
||||
@Query("Delete from quick_chat where uuid=:uuid")
|
||||
fun _delete(uuid: Long)
|
||||
|
||||
@Transaction
|
||||
fun delete(action: QuickChatAction) {
|
||||
_delete(action.uuid)
|
||||
decrementPositionsAfter(action.position)
|
||||
}
|
||||
|
||||
@Update
|
||||
fun update(action: QuickChatAction)
|
||||
|
||||
@Query("Update quick_chat set position=:position WHERE uuid=:uuid")
|
||||
fun updateActionPosition(uuid: Long, position: Int)
|
||||
|
||||
@Query("Update quick_chat set position=position-1 where position>=:position")
|
||||
fun decrementPositionsAfter(position: Int)
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package com.geeksville.mesh.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "quick_chat")
|
||||
data class QuickChatAction(
|
||||
@PrimaryKey(autoGenerate = true) val uuid: Long,
|
||||
@ColumnInfo(name="name") val name: String,
|
||||
@ColumnInfo(name="message") val message: String,
|
||||
@ColumnInfo(name="mode") val mode: Mode,
|
||||
@ColumnInfo(name="position") val position: Int) {
|
||||
enum class Mode {
|
||||
Append,
|
||||
Instant,
|
||||
}
|
||||
}
|
|
@ -14,11 +14,14 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.geeksville.android.Logging
|
||||
import com.geeksville.mesh.*
|
||||
import com.geeksville.mesh.database.PacketRepository
|
||||
import com.geeksville.mesh.database.QuickChatActionRepository
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import com.geeksville.mesh.repository.datastore.LocalConfigRepository
|
||||
import com.geeksville.mesh.service.MeshService
|
||||
import com.geeksville.mesh.util.positionToMeter
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
@ -61,6 +64,7 @@ class UIViewModel @Inject constructor(
|
|||
private val app: Application,
|
||||
private val packetRepository: PacketRepository,
|
||||
private val localConfigRepository: LocalConfigRepository,
|
||||
private val quickChatActionRepository: QuickChatActionRepository,
|
||||
private val preferences: SharedPreferences
|
||||
) : ViewModel(), Logging {
|
||||
|
||||
|
@ -70,6 +74,12 @@ class UIViewModel @Inject constructor(
|
|||
private val _localConfig = MutableLiveData<LocalOnlyProtos.LocalConfig?>()
|
||||
val localConfig: LiveData<LocalOnlyProtos.LocalConfig?> get() = _localConfig
|
||||
|
||||
private val _quickChatActions =
|
||||
MutableStateFlow<List<com.geeksville.mesh.database.entity.QuickChatAction>>(
|
||||
emptyList()
|
||||
)
|
||||
val quickChatActions: StateFlow<List<QuickChatAction>> = _quickChatActions
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
packetRepository.getAllPackets().collect { packets ->
|
||||
|
@ -81,6 +91,11 @@ class UIViewModel @Inject constructor(
|
|||
_localConfig.value = config
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
quickChatActionRepository.getAllActions().collect { actions ->
|
||||
_quickChatActions.value = actions
|
||||
}
|
||||
}
|
||||
debug("ViewModel created")
|
||||
}
|
||||
|
||||
|
@ -445,5 +460,43 @@ class UIViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun addQuickChatAction(name: String, value: String, mode: QuickChatAction.Mode) {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
val action = QuickChatAction(0, name, value, mode, _quickChatActions.value.size)
|
||||
quickChatActionRepository.insert(action)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteQuickChatAction(action: QuickChatAction) {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
quickChatActionRepository.delete(action)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateQuickChatAction(
|
||||
action: QuickChatAction,
|
||||
name: String?,
|
||||
message: String?,
|
||||
mode: QuickChatAction.Mode?
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
val newAction = QuickChatAction(
|
||||
action.uuid,
|
||||
name ?: action.name,
|
||||
message ?: action.message,
|
||||
mode ?: action.mode,
|
||||
action.position
|
||||
)
|
||||
quickChatActionRepository.update(newAction)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateActionPositions(actions: List<QuickChatAction>) {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
for (position in actions.indices) {
|
||||
quickChatActionRepository.setItemPosition(actions[position].uuid, position)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1290,7 +1290,7 @@ class MeshService : Service(), Logging {
|
|||
private fun fixupChannelList(lIn: List<ChannelProtos.Channel>): Array<ChannelProtos.Channel> {
|
||||
// When updating old firmware, we will briefly be told that there is zero channels
|
||||
val maxChannels =
|
||||
max(myNodeInfo?.maxChannels ?: 10, 10) // If we don't have my node info, assume 10 channels
|
||||
max(myNodeInfo?.maxChannels ?: 8, 8) // If we don't have my node info, assume 8 channels (source: apponly.options)
|
||||
val l = lIn.toMutableList()
|
||||
while (l.size < maxChannels) {
|
||||
val b = ChannelProtos.Channel.newBuilder()
|
||||
|
|
|
@ -19,13 +19,13 @@ import com.geeksville.analytics.DataPair
|
|||
import com.geeksville.android.GeeksvilleApplication
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.android.hideKeyboard
|
||||
import com.geeksville.android.isGooglePlayAvailable
|
||||
import com.geeksville.mesh.AppOnlyProtos
|
||||
import com.geeksville.mesh.ChannelProtos
|
||||
import com.geeksville.mesh.ConfigProtos
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.android.getCameraPermissions
|
||||
import com.geeksville.mesh.android.hasCameraPermission
|
||||
import com.geeksville.mesh.android.installSource
|
||||
import com.geeksville.mesh.databinding.ChannelFragmentBinding
|
||||
import com.geeksville.mesh.model.Channel
|
||||
import com.geeksville.mesh.model.ChannelOption
|
||||
|
@ -68,17 +68,6 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
|||
|
||||
private val model: UIViewModel by activityViewModels()
|
||||
|
||||
private val requestPermissionAndScanLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
|
||||
if (permissions.entries.all { it.value }) zxingScan()
|
||||
}
|
||||
|
||||
private val barcodeLauncher = registerForActivityResult(ScanContract()) { result ->
|
||||
if (result.contents != null) {
|
||||
model.setRequestChannelUrl(Uri.parse(result.contents))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
|
@ -212,54 +201,61 @@ 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(requireContext().getCameraPermissions())
|
||||
}
|
||||
.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?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val barcodeLauncher = registerForActivityResult(ScanContract()) { result ->
|
||||
if (result.contents != null) {
|
||||
model.setRequestChannelUrl(Uri.parse(result.contents))
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
val requestPermissionAndScanLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
|
||||
if (permissions.entries.all { it.value }) zxingScan()
|
||||
}
|
||||
|
||||
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(requireContext().getCameraPermissions())
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
fun mlkitScan() {
|
||||
debug("Starting ML Kit 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 { ex ->
|
||||
errormsg("code scanner failed: ${ex.message}")
|
||||
if (requireContext().hasCameraPermission()) zxingScan()
|
||||
else requestPermissionAndScan()
|
||||
}
|
||||
}
|
||||
|
||||
binding.channelNameEdit.on(EditorInfo.IME_ACTION_DONE) {
|
||||
requireActivity().hideKeyboard()
|
||||
}
|
||||
|
@ -283,14 +279,12 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
|||
}
|
||||
|
||||
binding.scanButton.setOnClickListener {
|
||||
if (isGooglePlayAvailable(requireContext())) {
|
||||
// only use ML Kit for play store installs
|
||||
if (requireContext().installSource() == "com.android.vending") {
|
||||
mlkitScan()
|
||||
} else {
|
||||
if (requireContext().hasCameraPermission()) {
|
||||
zxingScan()
|
||||
} else {
|
||||
requestPermissionAndScan()
|
||||
}
|
||||
if (requireContext().hasCameraPermission()) zxingScan()
|
||||
else requestPermissionAndScan()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package com.geeksville.mesh.ui
|
||||
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class DragManageAdapter(var adapter: SwapAdapter, dragDirs: Int, swipeDirs: Int) :
|
||||
ItemTouchHelper.SimpleCallback(dragDirs, swipeDirs) {
|
||||
interface SwapAdapter {
|
||||
fun swapItems(fromPosition: Int, toPosition: Int)
|
||||
fun commitSwaps()
|
||||
}
|
||||
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
adapter.swapItems(viewHolder.absoluteAdapterPosition, target.absoluteAdapterPosition)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
||||
super.onSelectedChanged(viewHolder, actionState)
|
||||
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_IDLE) {
|
||||
adapter.commitSwaps()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
|
@ -3,25 +3,25 @@ package com.geeksville.mesh.ui
|
|||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.view.*
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.allViews
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.MessageStatus
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import com.geeksville.mesh.databinding.AdapterMessageLayoutBinding
|
||||
import com.geeksville.mesh.databinding.MessagesFragmentBinding
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
|
@ -57,6 +57,8 @@ class MessagesFragment : Fragment(), Logging {
|
|||
|
||||
private val model: UIViewModel by activityViewModels()
|
||||
|
||||
private var isConnected = false
|
||||
|
||||
// Allows textMultiline with IME_ACTION_SEND
|
||||
private fun EditText.onActionSend(func: () -> Unit) {
|
||||
setOnEditorActionListener { _, actionId, _ ->
|
||||
|
@ -293,9 +295,45 @@ class MessagesFragment : Fragment(), Logging {
|
|||
// If connection state _OR_ myID changes we have to fix our ability to edit outgoing messages
|
||||
model.connectionState.observe(viewLifecycleOwner) { connectionState ->
|
||||
// If we don't know our node ID and we are offline don't let user try to send
|
||||
val connected = connectionState == MeshService.ConnectionState.CONNECTED
|
||||
binding.textInputLayout.isEnabled = connected
|
||||
binding.sendButton.isEnabled = connected
|
||||
isConnected = connectionState == MeshService.ConnectionState.CONNECTED
|
||||
binding.textInputLayout.isEnabled = isConnected
|
||||
binding.sendButton.isEnabled = isConnected
|
||||
for (subView: View in binding.quickChatLayout.allViews) {
|
||||
if (subView is Button) {
|
||||
subView.isEnabled = isConnected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
model.quickChatActions.asLiveData().observe(viewLifecycleOwner) { actions ->
|
||||
actions?.let {
|
||||
// This seems kinda hacky it might be better to replace with a recycler view
|
||||
binding.quickChatLayout.removeAllViews()
|
||||
for (action in actions) {
|
||||
val button = Button(context)
|
||||
button.setText(action.name)
|
||||
button.isEnabled = isConnected
|
||||
if (action.mode == QuickChatAction.Mode.Instant) {
|
||||
button.backgroundTintList = ContextCompat.getColorStateList(requireActivity(), R.color.colorMyMsg)
|
||||
}
|
||||
button.setOnClickListener {
|
||||
if (action.mode == QuickChatAction.Mode.Append) {
|
||||
val originalText = binding.messageInputText.text ?: ""
|
||||
val needsSpace = !originalText.endsWith(' ') && originalText.isNotEmpty()
|
||||
val newText = buildString {
|
||||
append(originalText)
|
||||
if (needsSpace) append(' ')
|
||||
append(action.message)
|
||||
}
|
||||
binding.messageInputText.setText(newText)
|
||||
binding.messageInputText.setSelection(newText.length)
|
||||
} else {
|
||||
model.messagesState.sendMessage(action.message, contactId)
|
||||
}
|
||||
}
|
||||
binding.quickChatLayout.addView(button)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
package com.geeksville.mesh.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
|
||||
class QuickChatActionAdapter internal constructor(
|
||||
private val context: Context,
|
||||
private val onEdit: (action: QuickChatAction) -> Unit,
|
||||
private val repositionAction: (fromPos: Int, toPos: Int) -> Unit,
|
||||
private val commitAction: () -> Unit,
|
||||
) : RecyclerView.Adapter<QuickChatActionAdapter.ActionViewHolder>(), DragManageAdapter.SwapAdapter {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
private var actions = emptyList<QuickChatAction>()
|
||||
|
||||
inner class ActionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val container: View = itemView.findViewById(R.id.quickChatActionContainer)
|
||||
val actionName: TextView = itemView.findViewById(R.id.quickChatActionName)
|
||||
val actionValue: TextView = itemView.findViewById(R.id.quickChatActionValue)
|
||||
val actionEdit: View = itemView.findViewById(R.id.quickChatActionEdit)
|
||||
val actionInstant: View = itemView.findViewById(R.id.quickChatActionInstant)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ActionViewHolder {
|
||||
val itemView = inflater.inflate(R.layout.adapter_quick_chat_action_layout, parent, false)
|
||||
return ActionViewHolder(itemView)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ActionViewHolder, position: Int) {
|
||||
val current = actions[position]
|
||||
holder.actionName.text = current.name
|
||||
holder.actionValue.text = current.message
|
||||
val isInstant = current.mode == QuickChatAction.Mode.Instant
|
||||
holder.actionInstant.visibility = if (isInstant) View.VISIBLE else View.INVISIBLE
|
||||
if (isInstant) {
|
||||
holder.container.backgroundTintList = ContextCompat.getColorStateList(context, R.color.colorMyMsg)
|
||||
} else {
|
||||
holder.container.backgroundTintList = null
|
||||
}
|
||||
holder.actionEdit.setOnClickListener {
|
||||
onEdit(current)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun setActions(actions: List<QuickChatAction>) {
|
||||
this.actions = actions
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun getItemCount() = actions.size
|
||||
|
||||
override fun swapItems(fromPosition: Int, toPosition: Int) {
|
||||
repositionAction(fromPosition, toPosition)
|
||||
notifyItemMoved(fromPosition, toPosition)
|
||||
}
|
||||
|
||||
override fun commitSwaps() {
|
||||
commitAction()
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
package com.geeksville.mesh.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import com.geeksville.mesh.databinding.QuickChatSettingsFragmentBinding
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.util.*
|
||||
|
||||
@AndroidEntryPoint
|
||||
class QuickChatSettingsFragment : ScreenFragment("Quick Chat settings"), Logging {
|
||||
private var _binding: QuickChatSettingsFragmentBinding? = null
|
||||
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val model: UIViewModel by activityViewModels()
|
||||
|
||||
private lateinit var actions: List<QuickChatAction>
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = QuickChatSettingsFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.quickChatSettingsCreateButton.setOnClickListener {
|
||||
val builder = createEditDialog(requireContext(), "New quick chat")
|
||||
|
||||
builder.builder.setPositiveButton("Add") { view, x ->
|
||||
|
||||
val name = builder.nameInput.text.toString().trim()
|
||||
val message = builder.messageInput.text.toString()
|
||||
if (builder.isNotEmpty())
|
||||
model.addQuickChatAction(
|
||||
name, message,
|
||||
if (builder.modeSwitch.isChecked) QuickChatAction.Mode.Instant else QuickChatAction.Mode.Append
|
||||
)
|
||||
}
|
||||
|
||||
val dialog = builder.builder.create()
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
val quickChatActionAdapter =
|
||||
QuickChatActionAdapter(requireContext(), { action: QuickChatAction ->
|
||||
val builder = createEditDialog(requireContext(), "Edit quick chat")
|
||||
builder.nameInput.setText(action.name)
|
||||
builder.messageInput.setText(action.message)
|
||||
val isInstant = action.mode == QuickChatAction.Mode.Instant
|
||||
builder.modeSwitch.isChecked = isInstant
|
||||
builder.instantImage.visibility = if (isInstant) View.VISIBLE else View.INVISIBLE
|
||||
|
||||
builder.builder.setNegativeButton(R.string.delete) { _, _ ->
|
||||
model.deleteQuickChatAction(action)
|
||||
}
|
||||
builder.builder.setPositiveButton(R.string.save_btn) { _, _ ->
|
||||
if (builder.isNotEmpty()) {
|
||||
model.updateQuickChatAction(
|
||||
action,
|
||||
builder.nameInput.text.toString(),
|
||||
builder.messageInput.text.toString(),
|
||||
if (builder.modeSwitch.isChecked) QuickChatAction.Mode.Instant else QuickChatAction.Mode.Append
|
||||
)
|
||||
}
|
||||
}
|
||||
val dialog = builder.builder.create()
|
||||
dialog.show()
|
||||
}, { fromPos, toPos ->
|
||||
Collections.swap(actions, fromPos, toPos)
|
||||
}, {
|
||||
model.updateActionPositions(actions)
|
||||
})
|
||||
|
||||
val dragCallback =
|
||||
DragManageAdapter(quickChatActionAdapter, ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
|
||||
val helper = ItemTouchHelper(dragCallback)
|
||||
|
||||
binding.quickChatSettingsView.apply {
|
||||
this.layoutManager = LinearLayoutManager(requireContext())
|
||||
this.adapter = quickChatActionAdapter
|
||||
helper.attachToRecyclerView(this)
|
||||
}
|
||||
|
||||
model.quickChatActions.asLiveData().observe(viewLifecycleOwner) { actions ->
|
||||
actions?.let {
|
||||
quickChatActionAdapter.setActions(actions)
|
||||
this.actions = actions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class DialogBuilder(
|
||||
val builder: MaterialAlertDialogBuilder,
|
||||
val nameInput: EditText,
|
||||
val messageInput: EditText,
|
||||
val modeSwitch: SwitchMaterial,
|
||||
val instantImage: ImageView
|
||||
) {
|
||||
fun isNotEmpty(): Boolean = nameInput.text.isNotEmpty() and messageInput.text.isNotEmpty()
|
||||
}
|
||||
|
||||
private fun getMessageName(message: String): String {
|
||||
return if (message.length <= 3) {
|
||||
message.uppercase()
|
||||
} else {
|
||||
buildString {
|
||||
append(message.first().uppercase())
|
||||
append(message[message.length / 2].uppercase())
|
||||
append(message.last().uppercase())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createEditDialog(context: Context, title: String): DialogBuilder {
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
builder.setTitle(title)
|
||||
|
||||
val layout =
|
||||
LayoutInflater.from(requireContext()).inflate(R.layout.dialog_add_quick_chat, null)
|
||||
|
||||
val nameInput: EditText = layout.findViewById(R.id.addQuickChatName)
|
||||
val messageInput: EditText = layout.findViewById(R.id.addQuickChatMessage)
|
||||
val modeSwitch: SwitchMaterial = layout.findViewById(R.id.addQuickChatMode)
|
||||
val instantImage: ImageView = layout.findViewById(R.id.addQuickChatInsant)
|
||||
instantImage.visibility = if (modeSwitch.isChecked) View.VISIBLE else View.INVISIBLE
|
||||
|
||||
var nameHasChanged = false
|
||||
|
||||
modeSwitch.setOnCheckedChangeListener { _, _ ->
|
||||
if (modeSwitch.isChecked) {
|
||||
modeSwitch.setText(R.string.mode_instant)
|
||||
instantImage.visibility = View.VISIBLE
|
||||
} else {
|
||||
modeSwitch.setText(R.string.mode_append)
|
||||
instantImage.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
messageInput.addTextChangedListener { text ->
|
||||
if (!nameHasChanged) {
|
||||
nameInput.setText(getMessageName(text.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
nameInput.addTextChangedListener {
|
||||
if (nameInput.isFocused) nameHasChanged = true
|
||||
}
|
||||
|
||||
builder.setView(layout)
|
||||
|
||||
return DialogBuilder(builder, nameInput, messageInput, modeSwitch, instantImage)
|
||||
}
|
||||
}
|
|
@ -61,40 +61,6 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
|
||||
private val myActivity get() = requireActivity() as MainActivity
|
||||
|
||||
private val associationResultLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartIntentSenderForResult()
|
||||
) {
|
||||
it.data
|
||||
?.getParcelableExtra<BluetoothDevice>(CompanionDeviceManager.EXTRA_DEVICE)
|
||||
?.let { device ->
|
||||
scanModel.onSelected(
|
||||
myActivity,
|
||||
BTScanModel.DeviceListEntry(
|
||||
device.name,
|
||||
"x${device.address}",
|
||||
device.bondState == BluetoothDevice.BOND_BONDED
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val requestLocationAndBackgroundLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
|
||||
if (permissions.entries.all { it.value }) {
|
||||
// Older versions of android only need Location permission
|
||||
if (myActivity.hasBackgroundPermission()) {
|
||||
binding.provideLocationCheckbox.isChecked = true
|
||||
} else requestBackgroundAndCheckLauncher.launch(myActivity.getBackgroundPermissions())
|
||||
}
|
||||
}
|
||||
|
||||
private val requestBackgroundAndCheckLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
|
||||
if (permissions.entries.all { it.value }) {
|
||||
binding.provideLocationCheckbox.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun doFirmwareUpdate() {
|
||||
model.meshService?.let { service ->
|
||||
|
||||
|
@ -257,6 +223,41 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
}.sorted()
|
||||
|
||||
private fun initCommonUI() {
|
||||
|
||||
val associationResultLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartIntentSenderForResult()
|
||||
) {
|
||||
it.data
|
||||
?.getParcelableExtra<BluetoothDevice>(CompanionDeviceManager.EXTRA_DEVICE)
|
||||
?.let { device ->
|
||||
scanModel.onSelected(
|
||||
myActivity,
|
||||
BTScanModel.DeviceListEntry(
|
||||
device.name,
|
||||
"x${device.address}",
|
||||
device.bondState == BluetoothDevice.BOND_BONDED
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val requestBackgroundAndCheckLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
|
||||
if (permissions.entries.all { it.value }) {
|
||||
binding.provideLocationCheckbox.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
val requestLocationAndBackgroundLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
|
||||
if (permissions.entries.all { it.value }) {
|
||||
// Older versions of android only need Location permission
|
||||
if (myActivity.hasBackgroundPermission()) {
|
||||
binding.provideLocationCheckbox.isChecked = true
|
||||
} else requestBackgroundAndCheckLauncher.launch(myActivity.getBackgroundPermissions())
|
||||
}
|
||||
}
|
||||
|
||||
// init our region spinner
|
||||
val spinner = binding.regionSpinner
|
||||
val regionAdapter =
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 11d94c9b15e9085b0f2516735ad816a3a35d5680
|
||||
Subproject commit cb2fb77bd8c2751e82b1017cd7545789a037fb7d
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20,9H4v2h16V9zM4,15h16v-2H4V15z"/>
|
||||
</vector>
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
|
||||
</vector>
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M4,18l8.5,-6L4,6v12zM13,6v12l8.5,-6L13,6z"/>
|
||||
</vector>
|
|
@ -0,0 +1,73 @@
|
|||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="@style/Widget.App.CardView"
|
||||
android:id="@+id/quickChatActionContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/quickChatActionInstant"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_baseline_fast_forward_24" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/quickChatActionName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textSize="20sp"
|
||||
app:layout_constraintStart_toEndOf="@+id/quickChatActionInstant"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/quickChatActionValue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/quickChatActionInstant"
|
||||
app:layout_constraintTop_toBottomOf="@+id/quickChatActionName" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/quickChatActionEdit"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/quickChatActionDrag"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_baseline_edit_24" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/quickChatActionDrag"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_baseline_drag_handle_24" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
|
@ -0,0 +1,65 @@
|
|||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="16dp">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/addQuickChatMessage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:ems="10"
|
||||
android:hint="@string/message"
|
||||
android:inputType="textShortMessage"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/addQuickChatName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:ems="10"
|
||||
android:hint="@string/name"
|
||||
android:inputType="textShortMessage"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/addQuickChatInsant"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_baseline_fast_forward_24" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/addQuickChatMode"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/mode_append"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/addQuickChatInsant"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/colorAdvancedBackground">
|
||||
|
@ -37,10 +38,30 @@
|
|||
android:layout_height="0dp"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/text_messages"
|
||||
app:layout_constraintBottom_toTopOf="@+id/textInputLayout"
|
||||
app:layout_constraintBottom_toTopOf="@+id/quickChatView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar" />
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar" >
|
||||
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/quickChatView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/textInputLayout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/quickChatLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal" />
|
||||
</HorizontalScrollView>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/textInputLayout"
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/colorAdvancedBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/quickChatSettingsTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="Quick chat options"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/quickChatSettingsView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="16dp"
|
||||
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_toBottomOf="@id/quickChatSettingsTitle"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/quickChatSettingsCreateButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:clickable="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:srcCompat="@drawable/ic_twotone_add_24" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -40,6 +40,10 @@
|
|||
android:id="@+id/show_intro"
|
||||
android:title="@string/show_intro"
|
||||
app:showAsAction="withText" />
|
||||
<item
|
||||
android:id="@+id/preferences_quick_chat"
|
||||
android:title="Quick chat options"
|
||||
app:showAsAction="withText" />
|
||||
<item
|
||||
android:id="@+id/about"
|
||||
android:title="@string/about"
|
||||
|
|
|
@ -144,7 +144,6 @@
|
|||
<string name="resend">Resend</string>
|
||||
<string name="shutdown">Shutdown</string>
|
||||
<string name="reboot">Reboot</string>
|
||||
<!-- TODO: Remove or change this placeholder text -->
|
||||
<string name="hello_blank_fragment">Hello blank fragment</string>
|
||||
<string name="show_intro">Show Introduction</string>
|
||||
<string name="intro_welcome_title">Welcome to Meshtastic</string>
|
||||
|
@ -153,4 +152,7 @@
|
|||
<string name="intro_started_text">Connect your meshtastic device by using either Bluetooth, Serial or WiFi. \n\nYou can see which devices are compatible at www.meshtastic.org/docs/hardware</string>
|
||||
<string name="intro_encryption_title">"Setting up encryption"</string>
|
||||
<string name="intro_encryption_text">As standard, a default encryption key is set. To enable your own channel and enhanced encryption, go to the channel tab and change the channel name, this will set a random key for AES256 encryption. \n\nTo communicate with other devices they will need to scan your QR code or follow the shared link to configure the channel settings.</string>
|
||||
<string name="message">Message</string>
|
||||
<string name="mode_append">Append to message</string>
|
||||
<string name="mode_instant">Instantly send</string>
|
||||
</resources>
|
||||
|
|
Ładowanie…
Reference in New Issue