Merge branch 'master' into dev-app-intro

pull/460/head
Andre Kirchhoff 2022-08-16 19:13:19 -03:00 zatwierdzone przez GitHub
commit 25de235a93
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
28 zmienionych plików z 830 dodań i 126 usunięć

Wyświetl plik

@ -14,14 +14,10 @@ The production version of our application is here:
[![Download at https://play.google.com/store/apps/details?id=com.geeksville.mesh](https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png)](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: [![Amazon appstore link](https://raw.githubusercontent.com/meshtastic/Meshtastic-device/master/images/amazon-fire-button.png)](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:

Wyświetl plik

@ -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

Wyświetl plik

@ -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>

Wyświetl plik

@ -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)
}

Wyświetl plik

@ -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
*/

Wyświetl plik

@ -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()
}
}

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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)
}
}

Wyświetl plik

@ -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)
}

Wyświetl plik

@ -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,
}
}

Wyświetl plik

@ -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)
}
}
}
}

Wyświetl plik

@ -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()

Wyświetl plik

@ -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()
}
}

Wyświetl plik

@ -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")
}
}

Wyświetl plik

@ -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)
}
}
}
}

Wyświetl plik

@ -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()
}
}

Wyświetl plik

@ -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)
}
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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"

Wyświetl plik

@ -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>

Wyświetl plik

@ -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"

Wyświetl plik

@ -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>