Sleep GPS near homepoints, wait for significant motion sensor.

First attempts, will test for a few days.
master
voussoir 2023-03-30 21:49:26 -07:00
parent 1ef59f49f5
commit 67382ce90a
7 changed files with 277 additions and 134 deletions

View File

@ -30,9 +30,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.*
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
@ -62,13 +60,9 @@ class MapFragment : Fragment()
private var bound: Boolean = false
val handler: Handler = Handler(Looper.getMainLooper())
private var trackingState: Int = Keys.STATE_TRACKING_STOPPED
private var gpsProviderActive: Boolean = false
private var networkProviderActive: Boolean = false
private lateinit var currentBestLocation: Location
var continuous_auto_center: Boolean = true
private lateinit var trackerService: TrackerService
private var trackerService: TrackerService? = null
private lateinit var database_changed_listener: DatabaseChangedListener
var thismapfragment: MapFragment? = null
@ -79,6 +73,8 @@ class MapFragment : Fragment()
lateinit var zoom_in_button: FloatingActionButton
lateinit var zoom_out_button: FloatingActionButton
lateinit var currentLocationButton: FloatingActionButton
lateinit var map_current_time: TextView
lateinit var power_level_indicator: ImageButton
private var current_track_overlay: Polyline? = null
private var current_position_overlays = ArrayList<Overlay>()
private var homepoints_overlays = ArrayList<Overlay>()
@ -107,8 +103,6 @@ class MapFragment : Fragment()
update_main_button()
}
}
currentBestLocation = getLastKnownLocation(requireContext())
trackingState = PreferencesHelper.loadTrackingState()
}
/* Overrides onStop from Fragment */
@ -121,6 +115,8 @@ class MapFragment : Fragment()
currentLocationButton = rootView.findViewById(R.id.location_button)
zoom_in_button = rootView.findViewById(R.id.zoom_in_button)
zoom_out_button = rootView.findViewById(R.id.zoom_out_button)
map_current_time = rootView.findViewById(R.id.map_current_time)
power_level_indicator = rootView.findViewById(R.id.power_level_indicator)
mainButton = rootView.findViewById(R.id.main_button)
locationErrorBar = Snackbar.make(mapView, String(), Snackbar.LENGTH_INDEFINITE)
@ -200,9 +196,7 @@ class MapFragment : Fragment()
trackbook.database_changed_listeners.add(database_changed_listener)
}
create_current_position_overlays(currentBestLocation, trackingState)
centerMap(currentBestLocation)
centerMap(getLastKnownLocation(requireContext()))
current_track_overlay = null
@ -213,9 +207,14 @@ class MapFragment : Fragment()
update_main_button()
mainButton.setOnClickListener {
if (trackingState == Keys.STATE_TRACKING_ACTIVE)
val tracker = trackerService
if (tracker == null)
{
trackerService.stopTracking()
return@setOnClickListener
}
if (tracker.trackingState == Keys.STATE_TRACKING_ACTIVE)
{
tracker.stopTracking()
}
else
{
@ -224,7 +223,12 @@ class MapFragment : Fragment()
handler.postDelayed(location_update_redraw, 0)
}
currentLocationButton.setOnClickListener {
centerMap(currentBestLocation, animated=true)
val tracker = trackerService
if (tracker == null)
{
return@setOnClickListener
}
centerMap(tracker.currentBestLocation, animated=true)
}
zoom_in_button.setOnClickListener {
mapView.controller.setZoom(mapView.zoomLevelDouble + 0.5)
@ -256,6 +260,7 @@ class MapFragment : Fragment()
{
Log.i("VOUSSOIR", "MapFragment.onResume")
super.onResume()
redraw()
requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
// if (bound) {
// trackerService.addGpsLocationListener()
@ -268,26 +273,31 @@ class MapFragment : Fragment()
{
Log.i("VOUSSOIR", "MapFragment.onPause")
super.onPause()
if (::trackerService.isInitialized)
{
trackerService.mapfragment = null
}
saveBestLocationState(currentBestLocation)
if (bound && trackingState != Keys.STATE_TRACKING_ACTIVE) {
trackerService.removeGpsLocationListener()
trackerService.removeNetworkLocationListener()
trackerService.trackbook.database.commit()
}
requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
val tracker = trackerService
if (tracker == null)
{
return
}
saveBestLocationState(tracker.currentBestLocation)
tracker.mapfragment = null
if (bound && tracker.trackingState != Keys.STATE_TRACKING_ACTIVE)
{
tracker.removeGpsLocationListener()
tracker.removeNetworkLocationListener()
tracker.trackbook.database.commit()
}
}
/* Overrides onStop from Fragment */
override fun onStop()
{
super.onStop()
if (::trackerService.isInitialized)
val tracker = trackerService
if (tracker != null)
{
trackerService.mapfragment = null
tracker.mapfragment = null
}
// unbind from TrackerService
if (bound)
@ -311,7 +321,10 @@ class MapFragment : Fragment()
{
Log.i("VOUSSOIR", "MapFragment.onDestroy")
super.onDestroy()
trackerService.mapfragment = null
if (trackerService != null)
{
trackerService!!.mapfragment = null
}
requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
@ -328,6 +341,8 @@ class MapFragment : Fragment()
// permission denied - unbind service
activity?.unbindService(connection)
}
val gpsProviderActive = if (trackerService == null) false else trackerService!!.gpsProviderActive
val networkProviderActive = if (trackerService == null) false else trackerService!!.networkProviderActive
toggleLocationErrorBar(gpsProviderActive, networkProviderActive)
}
@ -344,7 +359,10 @@ class MapFragment : Fragment()
{
activity?.startService(intent)
}
trackerService.startTracking()
if (trackerService != null)
{
trackerService!!.startTracking()
}
}
@ -354,24 +372,14 @@ class MapFragment : Fragment()
bound = false
// unregister listener for changes in shared preferences
PreferencesHelper.unregisterPreferenceChangeListener(sharedPreferenceChangeListener)
if (::trackerService.isInitialized)
if (trackerService != null)
{
trackerService.mapfragment = null
trackerService!!.mapfragment = null
}
}
private val sharedPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
when (key)
{
Keys.PREF_TRACKING_STATE ->
{
if (activity != null)
{
trackingState = PreferencesHelper.loadTrackingState()
}
}
}
update_main_button()
redraw()
}
fun centerMap(location: Location, animated: Boolean = false) {
@ -557,6 +565,7 @@ class MapFragment : Fragment()
fun update_main_button()
{
val tracker = trackerService
mainButton.isEnabled = trackbook.database.ready
currentLocationButton.isVisible = true
if (! trackbook.database.ready)
@ -564,13 +573,13 @@ class MapFragment : Fragment()
mainButton.text = requireContext().getString(R.string.button_not_ready)
mainButton.icon = null
}
else if (trackingState == Keys.STATE_TRACKING_STOPPED)
else if (tracker == null || tracker.trackingState == Keys.STATE_TRACKING_STOPPED)
{
mainButton.setIconResource(R.drawable.ic_fiber_manual_record_inactive_24dp)
mainButton.text = requireContext().getString(R.string.button_start)
mainButton.contentDescription = requireContext().getString(R.string.descr_button_start)
}
else if (trackingState == Keys.STATE_TRACKING_ACTIVE)
else if (tracker.trackingState == Keys.STATE_TRACKING_ACTIVE)
{
mainButton.setIconResource(R.drawable.ic_fiber_manual_stop_24dp)
mainButton.text = requireContext().getString(R.string.button_pause)
@ -609,11 +618,9 @@ class MapFragment : Fragment()
}
bound = true
trackerService = serviceref
trackerService.mapfragment = thismapfragment
trackerService!!.mapfragment = thismapfragment
// get state of tracking and update button if necessary
trackingState = trackerService.trackingState
update_main_button()
handler.postDelayed(location_update_redraw, 0)
redraw()
// register listener for changes in shared preferences
PreferencesHelper.registerPreferenceChangeListener(sharedPreferenceChangeListener)
// start listening for location updates
@ -625,27 +632,57 @@ class MapFragment : Fragment()
}
}
val location_update_redraw: Runnable = object : Runnable
fun redraw()
{
override fun run()
// Log.i("VOUSSOIR", "MapFragment.redraw")
update_main_button()
val tracker = trackerService
if (tracker == null)
{
Log.i("VOUSSOIR", "MapFragment.location_update_redraw")
currentBestLocation = trackerService.currentBestLocation
gpsProviderActive = trackerService.gpsProviderActive
networkProviderActive = trackerService.networkProviderActive
trackingState = trackerService.trackingState
create_current_position_overlays(currentBestLocation, trackingState)
return
}
create_current_position_overlays(tracker.currentBestLocation, tracker.trackingState)
if (current_track_overlay == null)
{
create_track_overlay()
}
current_track_overlay!!.setPoints(trackerService.recent_trackpoints_for_mapview)
current_track_overlay!!.setPoints(tracker.recent_trackpoints_for_mapview)
if (continuous_auto_center)
{
centerMap(currentBestLocation, animated=false)
centerMap(tracker.currentBestLocation, animated=false)
}
map_current_time.text = iso8601_local_noms(tracker.currentBestLocation.time)
if (tracker.arrived_at_home == 0L)
{
power_level_indicator.setImageResource(R.drawable.ic_satellite_24dp)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
power_level_indicator.tooltipText = "GPS tracking at full power"
}
}
else if (tracker.location_interval == tracker.LOCATION_INTERVAL_SLEEP)
{
power_level_indicator.setImageResource(R.drawable.ic_sleep_24dp)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
power_level_indicator.tooltipText = "GPS sleeping until movement"
}
}
else
{
power_level_indicator.setImageResource(R.drawable.ic_homepoint_24dp)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
power_level_indicator.tooltipText = "You are at home"
}
}
}
val location_update_redraw: Runnable = object : Runnable
{
override fun run()
{
redraw()
}
}
}

View File

@ -57,7 +57,6 @@ import org.osmdroid.views.overlay.TilesOverlay
import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlay
import org.osmdroid.views.overlay.simplefastpoint.SimpleFastPointOverlayOptions
import org.osmdroid.views.overlay.simplefastpoint.SimplePointTheme
import java.text.SimpleDateFormat
import java.util.*
class TrackFragment : Fragment(), MapListener, YesNoDialog.YesNoDialogListener

View File

@ -31,6 +31,10 @@ import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.hardware.Sensor
import android.hardware.SensorManager
import android.hardware.TriggerEvent
import android.hardware.TriggerEventListener
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
@ -39,23 +43,14 @@ import android.media.ToneGenerator
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.os.Vibrator
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import net.voussoir.trkpt.helpers.PreferencesHelper
import net.voussoir.trkpt.helpers.getDefaultLocation
import net.voussoir.trkpt.helpers.getLastKnownLocation
import net.voussoir.trkpt.helpers.isAccurateEnough
import net.voussoir.trkpt.helpers.isBetterLocation
import net.voussoir.trkpt.helpers.isDifferentEnough
import net.voussoir.trkpt.helpers.isGpsEnabled
import net.voussoir.trkpt.helpers.isNetworkEnabled
import net.voussoir.trkpt.helpers.isRecentEnough
import net.voussoir.trkpt.helpers.iso8601_local
import net.voussoir.trkpt.helpers.random_device_id
import net.voussoir.trkpt.helpers.*
import org.osmdroid.util.GeoPoint
import java.lang.ref.WeakReference
import java.util.*
@ -70,7 +65,11 @@ class TrackerService: Service()
var device_id: String = random_device_id()
var currentBestLocation: Location = getDefaultLocation()
var lastCommit: Long = 0
var location_min_time_ms: Long = 0
var last_significant_motion: Long = 0
var arrived_at_home: Long = 0
var location_interval: Long = 0
val LOCATION_INTERVAL_FULLPOWER: Long = 0
val LOCATION_INTERVAL_SLEEP: Long = Keys.ONE_MINUTE_IN_MILLISECONDS
private val RECENT_TRKPT_COUNT = 3600
private val DISPLACEMENT_LOCATION_COUNT = 5
lateinit var recent_displacement_locations: Deque<Location>
@ -94,7 +93,10 @@ class TrackerService: Service()
var mapfragment: MapFragment? = null
private fun addGpsLocationListener()
private lateinit var sensor_manager: SensorManager
private var significant_motion_sensor: Sensor? = null
private fun addGpsLocationListener(interval: Long)
{
if (! use_gps_location)
{
@ -124,7 +126,7 @@ class TrackerService: Service()
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
location_min_time_ms,
interval,
0f,
gpsLocationListener,
)
@ -132,7 +134,7 @@ class TrackerService: Service()
Log.i("VOUSSOIR", "Added GPS location listener.")
}
private fun addNetworkLocationListener()
private fun addNetworkLocationListener(interval: Long)
{
if (! use_network_location)
{
@ -162,7 +164,7 @@ class TrackerService: Service()
locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
0,
interval,
0f,
networkLocationListener,
)
@ -198,6 +200,27 @@ class TrackerService: Service()
}
}
fun reset_location_listeners(interval: Long)
{
location_interval = interval
if (gpsLocationListenerRegistered)
{
removeGpsLocationListener()
}
if (networkLocationListenerRegistered)
{
removeNetworkLocationListener()
}
if (use_gps_location)
{
addGpsLocationListener(interval)
}
if (use_network_location)
{
addNetworkLocationListener(interval)
}
}
private fun createLocationListener(): LocationListener
{
return object : LocationListener
@ -208,17 +231,11 @@ class TrackerService: Service()
// beeper.startTone(ToneGenerator.TONE_PROP_ACK, 150)
if (location.time == currentBestLocation.time)
if (location.time <= currentBestLocation.time)
{
return
}
if (! isBetterLocation(location, currentBestLocation))
{
Log.i("VOUSSOIR", "Not better than previous.")
return
}
currentBestLocation = location
val mf = mapfragment
@ -241,16 +258,12 @@ class TrackerService: Service()
Log.i("VOUSSOIR", "Omitting due to 0,0 location.")
return
}
if (! isRecentEnough(location))
{
Log.i("VOUSSOIR", "Omitting due to not recent enough.")
return
}
if (! isAccurateEnough(location, Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY))
{
Log.i("VOUSSOIR", "Omitting due to not accurate enough.")
return
}
// The Homepoint checks need to come before the other checks because if there
// is even the slightest chance that the user has left the homepoint, we want to
// wake back up to full power. We do not want to put this below the isAccurateEnough
// of isRecentEnough checks because we already know that the sleeping GPS produces
// very inaccurate points so if those bail early we'd stay in sleep mode.
for ((index, homepoint) in trackbook.homepoints.withIndex())
{
if (homepoint.location.distanceTo(location) < homepoint.radius)
@ -261,9 +274,42 @@ class TrackerService: Service()
trackbook.homepoints.remove(homepoint)
trackbook.homepoints.addFirst(homepoint)
}
if (arrived_at_home == 0L)
{
Log.i("VOUSSOIR", "Arrived at home.")
arrived_at_home = System.currentTimeMillis()
}
else if (location_interval == LOCATION_INTERVAL_SLEEP || significant_motion_sensor == null)
{
// If we are already asleep, do not reset the listeners again because
// that immediately fetches a new location.
// If we cannot rely on the motion sensor, then don't sleep!
}
else if ((System.currentTimeMillis() - arrived_at_home) > Keys.ONE_MINUTE_IN_MILLISECONDS)
{
Log.i("VOUSSOIR", "Staying at home.")
reset_location_listeners(interval=LOCATION_INTERVAL_SLEEP)
}
return
}
}
if (arrived_at_home > 0)
{
Log.i("VOUSSOIR", "Leaving home.")
arrived_at_home = 0
reset_location_listeners(interval=LOCATION_INTERVAL_FULLPOWER)
}
if (! isRecentEnough(location))
{
Log.i("VOUSSOIR", "Omitting due to not recent enough.")
return
}
if (! isAccurateEnough(location, Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY))
{
Log.i("VOUSSOIR", "Omitting due to not accurate enough.")
return
}
if (recent_displacement_locations.isEmpty())
{
// pass
@ -321,13 +367,20 @@ class TrackerService: Service()
private fun displayNotification(): Notification
{
val timestamp = iso8601_local(currentBestLocation.time)
val timestamp = iso8601_local_noms(currentBestLocation.time)
if (shouldCreateNotificationChannel())
{
createNotificationChannel()
}
if (location_interval == LOCATION_INTERVAL_SLEEP)
{
notification_builder.setContentText("${timestamp} (sleeping)")
}
else
{
notification_builder.setContentText(timestamp)
}
notification_builder.setWhen(currentBestLocation.time)
if (trackingState == Keys.STATE_TRACKING_ACTIVE)
@ -400,8 +453,7 @@ class TrackerService: Service()
{
Log.i("VOUSSOIR", "TrackerService.onBind")
bound = true
addGpsLocationListener()
addNetworkLocationListener()
reset_location_listeners(interval=LOCATION_INTERVAL_FULLPOWER)
return binder
}
@ -410,8 +462,7 @@ class TrackerService: Service()
{
Log.i("VOUSSOIR", "TrackerService.onRebind")
bound = true
addGpsLocationListener()
addNetworkLocationListener()
reset_location_listeners(interval=LOCATION_INTERVAL_FULLPOWER)
}
/* Overrides onUnbind from Service */
@ -466,6 +517,31 @@ class TrackerService: Service()
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int
{
Log.i("VOUSSOIR", "TrackerService.onStartCommand")
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
sensor_manager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
significant_motion_sensor = sensor_manager.getDefaultSensor(Sensor.TYPE_SIGNIFICANT_MOTION)
if (significant_motion_sensor != null)
{
val triggerEventListener = object : TriggerEventListener() {
override fun onTrigger(event: TriggerEvent?) {
Log.i("VOUSSOIR", "Significant motion")
// beeper.startTone(ToneGenerator.TONE_PROP_ACK, 150)
vibrator.vibrate(50)
last_significant_motion = System.currentTimeMillis()
arrived_at_home = 0L
if (location_interval == LOCATION_INTERVAL_SLEEP)
{
reset_location_listeners(LOCATION_INTERVAL_FULLPOWER)
val mf = mapfragment
mf?.handler?.postDelayed(mf.location_update_redraw, 0)
}
sensor_manager.requestTriggerSensor(this, significant_motion_sensor)
}
}
sensor_manager.requestTriggerSensor(triggerEventListener, significant_motion_sensor)
}
// SERVICE RESTART (via START_STICKY)
if (intent == null)
{
@ -507,8 +583,8 @@ class TrackerService: Service()
fun startTracking()
{
Log.i("VOUSSOIR", "TrackerService.startTracking")
addGpsLocationListener()
addNetworkLocationListener()
arrived_at_home = 0
reset_location_listeners(interval=LOCATION_INTERVAL_FULLPOWER)
trackingState = Keys.STATE_TRACKING_ACTIVE
PreferencesHelper.saveTrackingState(trackingState)
recent_displacement_locations.clear()
@ -519,7 +595,8 @@ class TrackerService: Service()
{
Log.i("VOUSSOIR", "TrackerService.stopTracking")
trackbook.database.commit()
arrived_at_home = 0
reset_location_listeners(interval=LOCATION_INTERVAL_FULLPOWER)
trackingState = Keys.STATE_TRACKING_STOPPED
PreferencesHelper.saveTrackingState(trackingState)
recent_displacement_locations.clear()
@ -533,26 +610,12 @@ class TrackerService: Service()
Keys.PREF_LOCATION_GPS ->
{
use_gps_location = PreferencesHelper.load_location_gps()
if (use_gps_location)
{
addGpsLocationListener()
}
else
{
removeGpsLocationListener()
}
reset_location_listeners(interval=LOCATION_INTERVAL_FULLPOWER)
}
Keys.PREF_LOCATION_NETWORK ->
{
use_network_location = PreferencesHelper.load_location_network()
if (use_network_location)
{
addNetworkLocationListener()
}
else
{
removeNetworkLocationListener()
}
reset_location_listeners(interval=LOCATION_INTERVAL_FULLPOWER)
}
Keys.PREF_USE_IMPERIAL_UNITS ->
{

View File

@ -20,16 +20,10 @@ fun iso8601_local(timestamp: Long): String
return iso8601_format.format(timestamp)
}
fun iso8601(datetime: Date): String
fun iso8601_local_noms(timestamp: Long): String
{
return iso8601(datetime.time)
}
fun iso8601_parse(datetime: String): Date
{
val iso8601_format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
iso8601_format.timeZone = TimeZone.getTimeZone("UTC")
return iso8601_format.parse(datetime)
val iso8601_format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
return iso8601_format.format(timestamp)
}
fun random_int(): Int

View File

@ -0,0 +1,13 @@
<!--
Thank you
https://pictogrammers.com/library/mdi/icon/satellite-variant/
-->
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="@color/icon_default"
android:pathData="M11.62,1L17.28,6.67L15.16,8.79L13.04,6.67L11.62,8.09L13.95,10.41L12.79,11.58L13.24,12.04C14.17,11.61 15.31,11.77 16.07,12.54L12.54,16.07C11.77,15.31 11.61,14.17 12.04,13.24L11.58,12.79L10.41,13.95L8.09,11.62L6.67,13.04L8.79,15.16L6.67,17.28L1,11.62L3.14,9.5L5.26,11.62L6.67,10.21L3.84,7.38C3.06,6.6 3.06,5.33 3.84,4.55L4.55,3.84C5.33,3.06 6.6,3.06 7.38,3.84L10.21,6.67L11.62,5.26L9.5,3.14L11.62,1M18,14A4,4 0 0,1 14,18V16A2,2 0 0,0 16,14H18M22,14A8,8 0 0,1 14,22V20A6,6 0 0,0 20,14H22Z" />
</vector>

View File

@ -0,0 +1,14 @@
<!--
Thank you
https://pictogrammers.com/library/mdi/icon/sleep/
-->
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/icon_default"
android:pathData="M23,12H17V10L20.39,6H17V4H23V6L19.62,10H23V12M15,16H9V14L12.39,10H9V8H15V10L11.62,14H15V16M7,20H1V18L4.39,14H1V12H7V14L3.62,18H7V20Z" />
</vector>

View File

@ -87,6 +87,29 @@
app:layout_constraintEnd_toEndOf="parent"
app:tint="@color/location_button_icon" />
<TextView
android:id="@+id/map_current_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
<ImageButton
android:id="@+id/power_level_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:src="@drawable/ic_satellite_24dp"
app:backgroundTint="@color/default_transparent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_satellite_24dp"
app:tint="@color/location_button_icon" />
<!-- GROUPS -->
</androidx.constraintlayout.widget.ConstraintLayout>