diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 000000000..f25903e74
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,9 @@
+## Thank you for sending in a pull request, here's some tips to get started!
+
+(Please delete all these tips and replace with your text)
+
+- Mention "#(issue)" in the description, when applicable
+- Please do not check in files that don't have real changes
+- Please do not reformat lines that you didn't have to change the code on
+- If your other co-developers have comments on your PR please tweak as needed
+- Do not use any external image service, just paste or drag and drop the image here and it will be uploaded automatically
diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index 09eb3acf5..46c175f61 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -17,18 +17,17 @@ jobs:
- name: Load secrets
run: |
- rm ./app/google-services.json
- cp ./app/google-services-example.json ./app/google-services.json
rm ./app/src/main/res/values/mapbox-token.xml
echo -e "\n $MAPBOXTOKEN\n" > ./app/src/main/res/values/mapbox-token.xml
mkdir -p ~/.gradle
echo "MAPBOX_DOWNLOADS_TOKEN=$MAPBOXTOKEN" >>~/.gradle/gradle.properties
env:
- GSERVICES: ${{ secrets.GSERVICES }}
MAPBOXTOKEN: ${{ secrets.MAPBOXTOKEN }}
- - name: Mock curfirmware version for CI
+ - name: Mock files for CI
run: |
+ rm ./app/google-services.json
+ cp ./app/google-services-example.json ./app/google-services.json
rm ./app/src/main/res/values/curfirmwareversion.xml
cp ./app/special/curfirmwareversion.xml ./app/src/main/res/values/
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 1917dc546..8c8d7336e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -98,7 +98,7 @@ jobs:
with:
draft: true
prerelease: true
- release_name: ${{ github.event.inputs.version}} alpha
+ release_name: Meshtastic Android ${{ github.event.inputs.version}} alpha
tag_name: ${{ github.event.inputs.version}}
body: |
Autogenerated by github action, developer should edit as required before publishing...
diff --git a/README.md b/README.md
index 51122d3a9..95ee92947 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,7 @@ cp ./app/special/curfirmwareversion.xml ./app/src/main/res/values/
cat ~/.gradle/gradle.properties
MAPBOX_DOWNLOADS_TOKEN=sk.yourtokenherexxx
```
+- (optional) to run CI tests on your fork: 1) allow GitHub Actions; 2) add your token at: Settings > Secrets > Actions > New repository secret: Name: MAPBOXTOKEN Value: sk.yourtokenherexxx
- Now you should be able to select "Run / Run" in the IDE and it will happily start running on your phone
or the emulator. Note: The emulators don't support bluetooth, so some features can not be used in
diff --git a/app/build.gradle b/app/build.gradle
index 796d04a2e..316d6f1c4 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -42,8 +42,8 @@ android {
applicationId "com.geeksville.mesh"
minSdkVersion 21 // The oldest emulator image I have tried is 22 (though 21 probably works)
targetSdkVersion 30 // 30 can't work until an explicit location permissions dialog is added
- versionCode 20255 // format is Mmmss (where M is 1+the numeric major number
- versionName "1.2.55"
+ versionCode 20256 // format is Mmmss (where M is 1+the numeric major number
+ versionName "1.2.56"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// per https://developer.android.com/studio/write/vector-asset-studio
@@ -199,5 +199,7 @@ dependencies {
// implementation "androidx.work:work-runtime:$work_version"
implementation "androidx.work:work-runtime-ktx:$work_version"
+ implementation "androidx.core:core-splashscreen:1.0.0-beta01"
+
implementation project(':geeksville-androidlib')
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 94c6aab71..c33d08766 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -128,7 +128,7 @@
android:label="@string/app_name"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateAlwaysHidden"
- android:theme="@style/AppTheme"
+ android:theme="@style/Theme.App.Starting"
android:exported="true">
diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
index 9839c59c7..3acc7c012 100644
--- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt
+++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
@@ -33,6 +33,7 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.Toolbar
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
@@ -43,7 +44,6 @@ import com.geeksville.android.Logging
import com.geeksville.android.ServiceClient
import com.geeksville.concurrent.handledLaunch
import com.geeksville.mesh.android.*
-import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.databinding.ActivityMainBinding
import com.geeksville.mesh.model.ChannelSet
import com.geeksville.mesh.model.DeviceVersion
@@ -60,16 +60,16 @@ import com.google.android.gms.tasks.Task
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayoutMediator
-import com.google.protobuf.InvalidProtocolBufferException
import com.vorlonsoft.android.rate.AppRate
import com.vorlonsoft.android.rate.StoreType
-import kotlinx.coroutines.*
-import java.io.FileOutputStream
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
import java.nio.charset.Charset
import java.text.DateFormat
import java.util.*
import java.util.regex.Pattern
-import kotlin.math.roundToInt
/*
@@ -388,12 +388,7 @@ class MainActivity : AppCompatActivity(), Logging,
return if (message != null) {
errormsg("Denied permissions: $message")
- Snackbar.make(findViewById(android.R.id.content), message, Snackbar.LENGTH_INDEFINITE)
- .apply { view.findViewById(R.id.snackbar_text).isSingleLine = false }
- .setAction(R.string.okay) {
- // dismiss
- }
- .show()
+ showSnackbar(message)
true
} else
false
@@ -483,6 +478,7 @@ class MainActivity : AppCompatActivity(), Logging,
}
override fun onCreate(savedInstanceState: Bundle?) {
+ installSplashScreen()
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
@@ -661,20 +657,7 @@ class MainActivity : AppCompatActivity(), Logging,
}
CREATE_CSV_FILE -> {
if (resultCode == Activity.RESULT_OK) {
- data?.data?.let { file_uri ->
- // model.allPackets is a result of a query, so we need to use observer for
- // the query to materialize
- model.allPackets.observe(this, { packets ->
- if (packets != null) {
- // no need for observer once got non-null list
- model.allPackets.removeObservers(this)
- // execute on the default thread pool to not block the main thread
- CoroutineScope(Dispatchers.Default + Job()).handledLaunch {
- saveMessagesCSV(file_uri, packets)
- }
- }
- })
- }
+ data?.data?.let { file_uri -> model.saveMessagesCSV(file_uri) }
}
}
}
@@ -814,30 +797,35 @@ class MainActivity : AppCompatActivity(), Logging,
}
}
- private fun showToast(msgId: Int) {
- Toast.makeText(
- this,
+ private fun showSnackbar(msgId: Int) {
+ Snackbar.make(
+ findViewById(android.R.id.content),
msgId,
- Toast.LENGTH_LONG
+ Snackbar.LENGTH_LONG
).show()
}
- private fun showToast(msg: String) {
- Toast.makeText(
- this,
+ private fun showSnackbar(msg: String) {
+ Snackbar.make(
+ findViewById(android.R.id.content),
msg,
- Toast.LENGTH_LONG
- ).show()
+ Snackbar.LENGTH_INDEFINITE
+ )
+ .apply { view.findViewById(R.id.snackbar_text).isSingleLine = false }
+ .setAction(R.string.okay) {
+ // dismiss
+ }
+ .show()
}
- private fun perhapsChangeChannel() {
+ fun perhapsChangeChannel(url: Uri? = requestedChannelUrl) {
// If the is opening a channel URL, handle it now
- requestedChannelUrl?.let { url ->
+ if (url != null) {
try {
val channels = ChannelSet(url)
val primary = channels.primaryChannel
if (primary == null)
- showToast(R.string.channel_invalid)
+ showSnackbar(R.string.channel_invalid)
else {
requestedChannelUrl = null
@@ -853,13 +841,14 @@ class MainActivity : AppCompatActivity(), Logging,
model.setChannels(channels)
} catch (ex: RemoteException) {
errormsg("Couldn't change channel ${ex.message}")
- showToast(R.string.cant_change_no_radio)
+ showSnackbar(R.string.cant_change_no_radio)
}
}
.show()
}
- } catch (ex: InvalidProtocolBufferException) {
- showToast(R.string.channel_invalid)
+ } catch (ex: Throwable) {
+ errormsg("Channel url error: ${ex.message}")
+ showSnackbar("${getString(R.string.channel_invalid)}: ${ex.message}")
}
}
}
@@ -1184,46 +1173,12 @@ class MainActivity : AppCompatActivity(), Logging,
try {
val packageInfo: PackageInfo = packageManager.getPackageInfo(packageName, 0)
val versionName = packageInfo.versionName
- showToast(versionName)
+ Toast.makeText(this, versionName, Toast.LENGTH_LONG).show()
} catch (e: PackageManager.NameNotFoundException) {
errormsg("Can not find the version: ${e.message}")
}
}
- private fun saveMessagesCSV(file_uri: Uri, packets: List) {
- // Extract distances to this device from position messages and put (node,SNR,distance) in
- // the file_uri
- val myNodeNum = model.myNodeInfo.value?.myNodeNum ?: return
-
- applicationContext.contentResolver.openFileDescriptor(file_uri, "w")?.use {
- FileOutputStream(it.fileDescriptor).use { fs ->
- // Write header
- fs.write(("from,rssi,snr,time,dist\n").toByteArray())
- // Packets are ordered by time, we keep most recent position of
- // our device in my_position.
- var my_position: MeshProtos.Position? = null
- packets.forEach {
- it.proto?.let { packet_proto ->
- it.position?.let { position ->
- if (packet_proto.from == myNodeNum) {
- my_position = position
- } else if (my_position != null) {
- val dist = positionToMeter(my_position!!, position).roundToInt()
- fs.write(
- "%x,%d,%f,%d,%d\n".format(
- packet_proto.from, packet_proto.rxRssi,
- packet_proto.rxSnr, packet_proto.rxTime, dist
- ).toByteArray()
- )
- }
- }
- }
- }
- }
- }
- }
-
-
/// Theme functions
private fun chooseThemeDialog() {
diff --git a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt
index ef72d478e..6b1f8ad68 100644
--- a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt
+++ b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt
@@ -71,6 +71,13 @@ data class Position(
/// @return bearing to the other position in degrees
fun bearing(o: Position) = bearing(latitude, longitude, o.latitude, o.longitude)
+ // If GPS gives a crap position don't crash our app
+ fun isValid(): Boolean {
+ return (latitude <= 90.0 && latitude >= -90) &&
+ latitude != 0.0 &&
+ longitude != 0.0
+ }
+
override fun toString(): String {
return "Position(lat=${latitude.anonymize}, lon=${longitude.anonymize}, alt=${altitude.anonymize}, time=${time}, batteryPctLevel=${batteryPctLevel})"
}
@@ -112,11 +119,7 @@ data class NodeInfo(
/// return the position if it is valid, else null
val validPosition: Position?
get() {
- return position?.takeIf {
- (it.latitude <= 90.0 && it.latitude >= -90) && // If GPS gives a crap position don't crash our app
- it.latitude != 0.0 &&
- it.longitude != 0.0
- }
+ return position?.takeIf { it.isValid() }
}
/// @return distance in meters to some other node (or null if unknown)
diff --git a/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt b/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt
index f82240b24..060cc446e 100644
--- a/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt
+++ b/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt
@@ -3,9 +3,11 @@ package com.geeksville.mesh.database
import androidx.lifecycle.LiveData
import com.geeksville.mesh.database.dao.PacketDao
import com.geeksville.mesh.database.entity.Packet
+import kotlinx.coroutines.flow.Flow
class PacketRepository(private val packetDao : PacketDao) {
- val allPackets : LiveData> = packetDao.getAllPacket(500)
+ val allPackets : LiveData> = packetDao.getAllPacket(MAX_ITEMS)
+ val allPacketsInReceiveOrder : Flow> = packetDao.getAllPacketsInReceiveOrder(MAX_ITEMS)
suspend fun insert(packet: Packet) {
packetDao.insert(packet)
@@ -14,4 +16,9 @@ class PacketRepository(private val packetDao : PacketDao) {
suspend fun deleteAll() {
packetDao.deleteAll()
}
+
+ companion object {
+ private const val MAX_ITEMS = 500
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/database/dao/PacketDao.kt b/app/src/main/java/com/geeksville/mesh/database/dao/PacketDao.kt
index a02016a94..55d33d7b8 100644
--- a/app/src/main/java/com/geeksville/mesh/database/dao/PacketDao.kt
+++ b/app/src/main/java/com/geeksville/mesh/database/dao/PacketDao.kt
@@ -5,6 +5,7 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import com.geeksville.mesh.database.entity.Packet
+import kotlinx.coroutines.flow.Flow
@Dao
interface PacketDao {
@@ -12,6 +13,9 @@ interface PacketDao {
@Query("Select * from packet order by received_date desc limit 0,:maxItem")
fun getAllPacket(maxItem: Int): LiveData>
+ @Query("Select * from packet order by received_date asc limit 0,:maxItem")
+ fun getAllPacketsInReceiveOrder(maxItem: Int): Flow>
+
@Insert
fun insert(packet: Packet)
diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
index b33f2bfd3..ec77dd70a 100644
--- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
@@ -12,15 +12,21 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.geeksville.android.Logging
-import com.geeksville.mesh.IMeshService
-import com.geeksville.mesh.MyNodeInfo
-import com.geeksville.mesh.RadioConfigProtos
+import com.geeksville.mesh.*
import com.geeksville.mesh.database.MeshtasticDatabase
import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.service.MeshService
+import com.geeksville.mesh.ui.positionToMeter
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.BufferedWriter
+import java.io.FileWriter
+import java.text.SimpleDateFormat
+import java.util.*
+import kotlin.math.roundToInt
/// Given a human name, strip out the first letter of the first three words and return that as the initials for
/// that user. If the original name is only one word, strip vowels from the original name and if the result is
@@ -257,5 +263,96 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging
errormsg("Can't set username on device, is device offline? ${ex.message}")
}
}
+
+ /**
+ * Write the persisted packet data out to a CSV file in the specified location.
+ */
+ fun saveMessagesCSV(file_uri: Uri) {
+ viewModelScope.launch(Dispatchers.Main) {
+ // Extract distances to this device from position messages and put (node,SNR,distance) in
+ // the file_uri
+ val myNodeNum = myNodeInfo.value?.myNodeNum ?: return@launch
+
+ // Capture the current node value while we're still on main thread
+ val nodes = nodeDB.nodes.value ?: emptyMap()
+
+ writeToUri(file_uri) { writer ->
+ // Create a map of nodes keyed by their ID
+ val nodesById = nodes.values.associateBy { it.num }
+
+ writer.appendLine("date,time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx snr,distance,hop limit,payload")
+
+ // Packets are ordered by time, we keep most recent position of
+ // our device in localNodePosition.
+ var localNodePosition: MeshProtos.Position? = null
+ val dateFormat = SimpleDateFormat("yyyy-MM-dd,HH:mm:ss", Locale.getDefault())
+ repository.allPacketsInReceiveOrder.first().forEach { packet ->
+ packet.proto?.let { proto ->
+ packet.position?.let { position ->
+ if (proto.from == myNodeNum) {
+ localNodePosition = position
+ } else {
+ val rxDateTime = dateFormat.format(packet.received_date)
+ val rxFrom = proto.from.toUInt()
+ val senderName = nodesById[proto.from]?.user?.longName ?: ""
+
+ // sender lat & long
+ val senderPos = packet.position
+ ?.let { p -> Position(p) }
+ ?.takeIf { p -> p.isValid() }
+ val senderLat = senderPos?.latitude ?: ""
+ val senderLong = senderPos?.longitude ?: ""
+
+ // rx lat, long, and elevation
+ val rxPos = localNodePosition
+ ?.let { p -> Position(p) }
+ ?.takeIf { p -> p.isValid() }
+ val rxLat = rxPos?.latitude ?: ""
+ val rxLong = rxPos?.longitude ?: ""
+ val rxAlt = rxPos?.altitude ?: ""
+ val rxSnr = "%f".format(proto.rxSnr)
+
+ // Calculate the distance if both positions are valid
+ val dist = if (senderPos == null || rxPos == null) {
+ ""
+ } else {
+ positionToMeter(
+ localNodePosition!!,
+ position
+ ).roundToInt().toString()
+ }
+
+ val hopLimit = proto.hopLimit
+
+ val payload = when {
+ proto.decoded.portnumValue != Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> "<${proto.decoded.portnum}>"
+ proto.hasDecoded() -> "\"" + proto.decoded.payload.toStringUtf8()
+ .replace("\"", "\\\"") + "\""
+ proto.hasEncrypted() -> "${proto.encrypted.size()} encrypted bytes"
+ else -> ""
+ }
+
+ // date,time,from,sender name,sender lat,sender long,rx lat,rx long,rx elevation,rx snr,distance,hop limit,payload
+ writer.appendLine("$rxDateTime,$rxFrom,$senderName,$senderLat,$senderLong,$rxLat,$rxLong,$rxAlt,$rxSnr,$dist,$hopLimit,$payload")
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private suspend inline fun writeToUri(uri: Uri, crossinline block: suspend (BufferedWriter) -> Unit) {
+ withContext(Dispatchers.IO) {
+ app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
+ FileWriter(parcelFileDescriptor.fileDescriptor).use { fileWriter ->
+ BufferedWriter(fileWriter).use { writer ->
+ block.invoke(writer)
+ }
+ }
+ }
+ }
+ }
+
}
diff --git a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt
index 52f6b57cc..c5f1deabc 100644
--- a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt
@@ -2,6 +2,7 @@ package com.geeksville.mesh.service
import android.annotation.SuppressLint
import android.app.Service
+import android.companion.CompanionDeviceManager
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
@@ -285,6 +286,22 @@ class RadioInterfaceService : Service(), Logging {
debug("Setting bonded device to ${address.anonymize}")
+ // We only keep an association to one device at a time... (move to BluetoothInterface?)
+ if (BluetoothInterface.hasCompanionDeviceApi(this)) {
+ if (address != null) {
+ val deviceManager = getSystemService(CompanionDeviceManager::class.java)
+ val c = address[0]
+ val rest = address.substring(1)
+
+ deviceManager.associations.forEach { old ->
+ if (rest != old) {
+ debug("Forgetting old BLE association ${old.anonymize}")
+ deviceManager.disassociate(old)
+ }
+ }
+ }
+ }
+
getPrefs(this).edit(commit = true) {
if (address == null)
this.remove(DEVADDR_KEY)
diff --git a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
index 73b644550..daf62a33c 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
@@ -33,7 +33,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.protobuf.ByteString
import com.google.zxing.integration.android.IntentIntegrator
-import java.net.MalformedURLException
import java.security.SecureRandom
@@ -320,18 +319,8 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
if (result != null) {
- if (result.contents == null) {
- Snackbar.make(binding.scanButton, R.string.channel_invalid, Snackbar.LENGTH_LONG).show()
- } else {
- try {
- val intent = Intent(Intent.ACTION_VIEW)
- intent.data = ChannelSet(Uri.parse(result.contents)).getChannelUrl(false)
- startActivity(intent)
- } catch (ex: ActivityNotFoundException) {
- Snackbar.make(binding.scanButton, R.string.channel_invalid, Snackbar.LENGTH_LONG).show()
- } catch (ex: MalformedURLException) {
- Snackbar.make(binding.scanButton, R.string.channel_invalid, Snackbar.LENGTH_LONG).show()
- }
+ if (result.contents != null) {
+ ((requireActivity() as MainActivity).perhapsChangeChannel(Uri.parse(result.contents)))
}
} else {
super.onActivityResult(requestCode, resultCode, data)
diff --git a/app/src/main/res/drawable/ic_twotone_public_24.xml b/app/src/main/res/drawable/ic_twotone_public_24.xml
deleted file mode 100644
index 403051e61..000000000
--- a/app/src/main/res/drawable/ic_twotone_public_24.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
diff --git a/app/src/main/res/layout/channel_fragment.xml b/app/src/main/res/layout/channel_fragment.xml
index d916757ca..77b252e7d 100644
--- a/app/src/main/res/layout/channel_fragment.xml
+++ b/app/src/main/res/layout/channel_fragment.xml
@@ -100,7 +100,6 @@
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:text="@string/reset"
- app:icon="@drawable/ic_twotone_public_24"
app:layout_constraintBottom_toBottomOf="@id/bottomButtonsGuideline"
app:layout_constraintEnd_toStartOf="@id/editableCheckbox" />
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 07e362752..664b688ac 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -73,4 +73,18 @@
- @style/MyThemeOverlay_Toolbar
+
+