Skip to content

Commit b5e96d2

Browse files
committed
Added Lux Meter tile
1 parent 17dda33 commit b5e96d2

7 files changed

Lines changed: 220 additions & 0 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
android:name="android.hardware.sensor.gyroscope"
2020
android:required="false" />
2121

22+
<uses-feature
23+
android:name="android.hardware.sensor.light"
24+
android:required="true" />
25+
2226
<uses-feature
2327
android:name="android.hardware.camera.flash"
2428
android:required="false" />
@@ -89,6 +93,26 @@
8993
android:value="Required for accessing accelerometer sensor data while tile is active" />
9094
</service>
9195

96+
<!--lux meter quick settings tile service-->
97+
98+
<service
99+
android:name=".tiles.LuxMeterTileService"
100+
android:exported="true"
101+
android:foregroundServiceType="specialUse"
102+
android:icon="@drawable/ic_lux_meter_off"
103+
android:label="@string/lux_meter_tile_label"
104+
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
105+
<intent-filter>
106+
<action android:name="android.service.quicksettings.action.QS_TILE" />
107+
</intent-filter>
108+
<meta-data
109+
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
110+
android:value="true" />
111+
<property
112+
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
113+
android:value="Required for accessing light sensor data while tile is active" />
114+
</service>
115+
92116
<!--lock screen quick settings tile service-->
93117

94118
<service
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.wstxda.toolkit.resources.label
2+
3+
import android.content.Context
4+
import com.wstxda.toolkit.R
5+
6+
fun Context.luxMeterLabel(lux: Int): String {
7+
return getString(R.string.lux_meter_tile_label_lux, lux)
8+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.wstxda.toolkit.services.sensors
2+
3+
import android.hardware.SensorEvent
4+
5+
fun SensorEvent.getLux(): Int {
6+
return values[0].toInt()
7+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package com.wstxda.toolkit.tiles
2+
3+
import android.app.ForegroundServiceStartNotAllowedException
4+
import android.app.NotificationManager
5+
import android.graphics.drawable.Icon
6+
import android.hardware.Sensor
7+
import android.hardware.SensorEvent
8+
import android.hardware.SensorEventListener
9+
import android.hardware.SensorManager
10+
import android.os.Build.VERSION
11+
import android.os.Build.VERSION_CODES
12+
import android.service.quicksettings.Tile
13+
import android.service.quicksettings.TileService
14+
import android.util.Log
15+
import android.widget.Toast
16+
import androidx.core.content.getSystemService
17+
import com.wstxda.toolkit.R
18+
import com.wstxda.toolkit.resources.label.luxMeterLabel
19+
import com.wstxda.toolkit.services.NOTIFICATION_ID
20+
import com.wstxda.toolkit.services.channel
21+
import com.wstxda.toolkit.services.notification
22+
import com.wstxda.toolkit.services.sensors.getLux
23+
import com.wstxda.toolkit.services.startForegroundCompat
24+
import com.wstxda.toolkit.update
25+
26+
private const val TAG = "LuxMeterTileService"
27+
private const val SENSOR_DELAY = SensorManager.SENSOR_DELAY_UI
28+
29+
// Note: Sensor data is only accessible in foreground, so we need to start this service as a foreground service.
30+
// This is done either on onCreate, onClick or onStartListening, depending on Android version.
31+
32+
// On Android 14 foreground service can not be started in onClick or onStartListening due to a bug:
33+
// https://issuetracker.google.com/issues/299506164
34+
private val START_FOREGROUND_IMMEDIATELY = VERSION.SDK_INT == VERSION_CODES.UPSIDE_DOWN_CAKE
35+
36+
// On Android 15+ foreground service can only be started after user interaction (onClick or sometimes onStartListening):
37+
// https://developer.android.com/about/versions/15/behavior-changes-15#fgs-hardening
38+
private val CAN_ONLY_START_FOREGROUND_ON_CLICK = VERSION.SDK_INT >= VERSION_CODES.VANILLA_ICE_CREAM
39+
40+
class LuxMeterTileService : TileService(), SensorEventListener {
41+
42+
private val sensorManager
43+
get() = getSystemService<SensorManager>()
44+
45+
private val notificationManager
46+
get() = getSystemService<NotificationManager>()
47+
48+
private val sensor by lazy {
49+
sensorManager?.getDefaultSensor(Sensor.TYPE_LIGHT)
50+
}
51+
52+
private val isSupported
53+
get() = sensor != null
54+
55+
override fun onCreate() {
56+
Log.i(TAG, "Create")
57+
notificationManager?.createNotificationChannel(channel())
58+
if (START_FOREGROUND_IMMEDIATELY) {
59+
startForegroundCompat(NOTIFICATION_ID, notification())
60+
}
61+
}
62+
63+
override fun onDestroy() {
64+
Log.i(TAG, "Destroy")
65+
stopForeground(STOP_FOREGROUND_REMOVE)
66+
super.onDestroy()
67+
}
68+
69+
override fun onStartListening() {
70+
Log.i(TAG, "Start listening")
71+
when (qsTile?.state) {
72+
Tile.STATE_ACTIVE -> startLuxMeter()
73+
}
74+
}
75+
76+
override fun onStopListening() {
77+
Log.i(TAG, "Stop listening")
78+
when (qsTile?.state) {
79+
Tile.STATE_ACTIVE -> stopLuxMeter()
80+
}
81+
}
82+
83+
override fun onClick() {
84+
if (!isSupported) showNotSupported() else toggleTile()
85+
}
86+
87+
private fun showNotSupported() {
88+
Toast.makeText(this, R.string.not_supported, Toast.LENGTH_LONG).show()
89+
}
90+
91+
private fun toggleTile() {
92+
when (qsTile?.state) {
93+
Tile.STATE_ACTIVE -> setInactive()
94+
Tile.STATE_INACTIVE -> setActive()
95+
}
96+
}
97+
98+
private fun setActive() {
99+
qsTile?.update {
100+
state = Tile.STATE_ACTIVE
101+
if (VERSION.SDK_INT >= VERSION_CODES.Q) {
102+
subtitle = getString(R.string.lux_meter_tile_label)
103+
}
104+
}
105+
startLuxMeter()
106+
}
107+
108+
private fun setInactive() {
109+
qsTile?.update {
110+
state = Tile.STATE_INACTIVE
111+
icon = Icon.createWithResource(applicationContext, R.drawable.ic_lux_meter_off)
112+
label = getString(R.string.lux_meter_tile_label)
113+
if (VERSION.SDK_INT >= VERSION_CODES.Q) {
114+
subtitle = getString(R.string.tile_label_off)
115+
}
116+
}
117+
stopLuxMeter()
118+
}
119+
120+
private fun startLuxMeter() {
121+
try {
122+
Log.i(TAG, "Start")
123+
if (!START_FOREGROUND_IMMEDIATELY) {
124+
startForegroundCompat(NOTIFICATION_ID, notification())
125+
}
126+
sensorManager?.registerListener(this, sensor, SENSOR_DELAY)
127+
} catch (e: Exception) {
128+
if (CAN_ONLY_START_FOREGROUND_ON_CLICK && e is ForegroundServiceStartNotAllowedException) {
129+
Log.w(TAG, "Foreground service start not allowed", e)
130+
setInactive()
131+
} else {
132+
throw e // Crash on other exceptions
133+
}
134+
}
135+
}
136+
137+
private fun stopLuxMeter() {
138+
Log.i(TAG, "Stop")
139+
if (!START_FOREGROUND_IMMEDIATELY) {
140+
stopForeground(STOP_FOREGROUND_DETACH)
141+
}
142+
sensorManager?.unregisterListener(this)
143+
}
144+
145+
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) = Unit
146+
147+
override fun onSensorChanged(event: SensorEvent) {
148+
val lux = event.getLux()
149+
Log.v(TAG, lux.toString())
150+
qsTile?.update {
151+
label = luxMeterLabel(lux)
152+
icon = Icon.createWithResource(applicationContext, R.drawable.ic_lux_meter_on)
153+
}
154+
}
155+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:tint="?attr/colorControlNormal"
5+
android:viewportWidth="960"
6+
android:viewportHeight="960">
7+
<path
8+
android:fillColor="@android:color/white"
9+
android:pathData="M480,880Q447,880 423.5,856.5Q400,833 400,800L560,800Q560,833 536.5,856.5Q513,880 480,880ZM480,160Q436,160 398.5,175.5Q361,191 332,218L274,162Q315,124 367.5,102Q420,80 480,80Q605,80 692.5,167.5Q780,255 780,380Q780,451 755,501.5Q730,552 698,584L642,528Q663,505 681.5,469Q700,433 700,380Q700,288 636,224Q572,160 480,160ZM848,848L791,905L526,640L330,640Q261,599 220.5,530Q180,461 180,380Q180,360 182.5,341Q185,322 190,304L56,168L112,112L848,848ZM354,560L446,560L260,374Q260,376 260,377Q260,378 260,380Q260,434 284.5,481Q309,528 354,560ZM348,462Q348,462 348,462Q348,462 348,462Q348,462 348,462Q348,462 348,462L348,462ZM482,368Q482,368 482,368Q482,368 482,368Q482,368 482,368Q482,368 482,368L482,368Q482,368 482,368Q482,368 482,368ZM646,680L646,760L320,760L320,680L646,680Z" />
10+
</vector>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:tint="?attr/colorControlNormal"
5+
android:viewportWidth="960"
6+
android:viewportHeight="960">
7+
<path
8+
android:fillColor="@android:color/white"
9+
android:pathData="M480,880Q447,880 423.5,856.5Q400,833 400,800L560,800Q560,833 536.5,856.5Q513,880 480,880ZM320,760L320,680L640,680L640,760L320,760ZM330,640Q261,599 220.5,530Q180,461 180,380Q180,255 267.5,167.5Q355,80 480,80Q605,80 692.5,167.5Q780,255 780,380Q780,461 739.5,530Q699,599 630,640L330,640ZM354,560L606,560Q651,528 675.5,481Q700,434 700,380Q700,288 636,224Q572,160 480,160Q388,160 324,224Q260,288 260,380Q260,434 284.5,481Q309,528 354,560ZM480,560Q480,560 480,560Q480,560 480,560Q480,560 480,560Q480,560 480,560Q480,560 480,560Q480,560 480,560Q480,560 480,560Q480,560 480,560Z" />
10+
</vector>

app/src/main/res/values/strings.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@
4646
<string name="level_tile_zero_degrees" translatable="false">0°</string>
4747
<string name="level_tile_label_degrees" translatable="false">%d°</string>
4848

49+
<!--lux meter tile-->
50+
51+
<string name="lux_meter_tile_label">Lux Meter</string>
52+
<!--tile label-->
53+
<string name="lux_meter_tile_label_lux" translatable="false">%d lx</string>
54+
4955
<!--lock screen tile-->
5056

5157
<string name="lock_tile_label">Lock</string>

0 commit comments

Comments
 (0)