1 /*
2 * Copyright 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 @file:JvmName("AutoOnFeature")
18
19 package com.android.server.bluetooth
20
21 import android.app.AlarmManager
22 import android.app.BroadcastOptions
23 import android.bluetooth.BluetoothAdapter.ACTION_AUTO_ON_STATE_CHANGED
24 import android.bluetooth.BluetoothAdapter.AUTO_ON_STATE_DISABLED
25 import android.bluetooth.BluetoothAdapter.AUTO_ON_STATE_ENABLED
26 import android.bluetooth.BluetoothAdapter.EXTRA_AUTO_ON_STATE
27 import android.bluetooth.BluetoothAdapter.STATE_ON
28 import android.content.BroadcastReceiver
29 import android.content.ContentResolver
30 import android.content.Context
31 import android.content.Intent
32 import android.content.IntentFilter
33 import android.os.Build
34 import android.os.Handler
35 import android.os.Looper
36 import android.os.SystemClock
37 import android.provider.Settings
38 import androidx.annotation.RequiresApi
39 import androidx.annotation.VisibleForTesting
40 import com.android.modules.expresslog.Counter
41 import com.android.server.bluetooth.airplane.hasUserToggledApm as hasUserToggledApm
42 import com.android.server.bluetooth.airplane.isOnOverrode as isAirplaneModeOn
43 import com.android.server.bluetooth.satellite.isOn as isSatelliteModeOn
44 import java.time.LocalDateTime
45 import java.time.LocalTime
46 import java.time.temporal.ChronoUnit
47 import kotlin.time.Duration
48 import kotlin.time.DurationUnit
49 import kotlin.time.toDuration
50
51 private const val TAG = "AutoOnFeature"
52
resetAutoOnTimerForUsernull53 public fun resetAutoOnTimerForUser(
54 looper: Looper,
55 context: Context,
56 state: BluetoothAdapterState,
57 callback_on: () -> Unit
58 ) {
59 // Remove any previous timer
60 timer?.cancel()
61 timer = null
62
63 if (!isFeatureEnabledForUser(context.contentResolver)) {
64 Log.d(TAG, "Not Enabled for current user: ${context.getUser()}")
65 return
66 }
67 if (state.oneOf(STATE_ON)) {
68 Log.d(TAG, "Bluetooth already in ${state}, no need for timer")
69 return
70 }
71 if (isSatelliteModeOn) {
72 Log.d(TAG, "Satellite prevent feature activation")
73 return
74 }
75 if (isAirplaneModeOn) {
76 if (!hasUserToggledApm(context)) {
77 Log.d(TAG, "Airplane prevent feature activation")
78 return
79 }
80 Log.d(TAG, "Airplane bypassed as airplane enhanced mode has been activated previously")
81 }
82
83 val receiver =
84 object : BroadcastReceiver() {
85 override fun onReceive(ctx: Context, intent: Intent) {
86 Log.i(TAG, "Received ${intent.action} that trigger a new alarm scheduling")
87 pause()
88 resetAutoOnTimerForUser(looper, context, state, callback_on)
89 }
90 }
91
92 timer = Timer.start(looper, context, receiver, callback_on)
93 }
94
pausenull95 public fun pause() {
96 timer?.pause()
97 timer = null
98 }
99
100 @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
notifyBluetoothOnnull101 public fun notifyBluetoothOn(context: Context) {
102 timer?.cancel()
103 timer = null
104
105 if (!isFeatureSupportedForUser(context.contentResolver)) {
106 val defaultFeatureValue = true
107 if (!setFeatureEnabledForUserUnchecked(context, defaultFeatureValue)) {
108 Log.e(TAG, "Failed to set feature to its default value ${defaultFeatureValue}")
109 } else {
110 Log.i(TAG, "Feature was set to its default value ${defaultFeatureValue}")
111 }
112 } else {
113 // When Bluetooth turned on state, any saved time will be obsolete.
114 // This happen only when the phone reboot while Bluetooth is ON
115 Timer.resetStorage(context.contentResolver)
116 }
117 }
118
isUserSupportednull119 public fun isUserSupported(resolver: ContentResolver) = isFeatureSupportedForUser(resolver)
120
121 public fun isUserEnabled(context: Context): Boolean {
122 if (!isUserSupported(context.contentResolver)) {
123 throw IllegalStateException("AutoOnFeature not supported for user: ${context.getUser()}")
124 }
125 return isFeatureEnabledForUser(context.contentResolver)
126 }
127
128 @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
setUserEnablednull129 public fun setUserEnabled(
130 looper: Looper,
131 context: Context,
132 state: BluetoothAdapterState,
133 status: Boolean,
134 callback_on: () -> Unit,
135 ) {
136 if (!isUserSupported(context.contentResolver)) {
137 throw IllegalStateException("AutoOnFeature not supported for user: ${context.getUser()}")
138 }
139 if (isFeatureEnabledForUser(context.contentResolver) && status == true) {
140 Log.i(TAG, "setUserEnabled: Nothing to do, feature is already enabled")
141 return
142 }
143 if (!setFeatureEnabledForUserUnchecked(context, status)) {
144 throw IllegalStateException("AutoOnFeature database failure for user: ${context.getUser()}")
145 }
146 Counter.logIncrement(
147 if (status) "bluetooth.value_auto_on_enabled" else "bluetooth.value_auto_on_disabled"
148 )
149 Timer.resetStorage(context.contentResolver)
150 resetAutoOnTimerForUser(looper, context, state, callback_on)
151 }
152
153 ////////////////////////////////////////////////////////////////////////////////////////////////////
154 ////////////////////////////////////////// PRIVATE METHODS /////////////////////////////////////////
155 ////////////////////////////////////////////////////////////////////////////////////////////////////
156
157 @VisibleForTesting internal var timer: Timer? = null
158
159 @VisibleForTesting
160 internal class Timer
161 private constructor(
162 looper: Looper,
163 private val context: Context,
164 private val receiver: BroadcastReceiver,
165 private val callback_on: () -> Unit,
166 private val now: LocalDateTime,
167 private val target: LocalDateTime,
168 private val timeToSleep: Duration
169 ) : AlarmManager.OnAlarmListener {
170 private val alarmManager: AlarmManager = context.getSystemService(AlarmManager::class.java)!!
171
172 private val handler = Handler(looper)
173
174 init {
175 writeDateToStorage(target, context.contentResolver)
176 alarmManager.set(
177 AlarmManager.ELAPSED_REALTIME,
178 SystemClock.elapsedRealtime() + timeToSleep.inWholeMilliseconds,
179 "Bluetooth AutoOnFeature",
180 this,
181 handler
182 )
183 Log.i(TAG, "[${this}]: Scheduling next Bluetooth restart")
184
185 context.registerReceiver(
186 receiver,
<lambda>null187 IntentFilter().apply {
188 addAction(Intent.ACTION_DATE_CHANGED)
189 addAction(Intent.ACTION_TIMEZONE_CHANGED)
190 addAction(Intent.ACTION_TIME_CHANGED)
191 },
192 null,
193 handler
194 )
195 }
196
onAlarmnull197 override fun onAlarm() {
198 Log.i(TAG, "[${this}]: Bluetooth restarting now")
199 callback_on()
200 cancel()
201 timer = null
202 }
203
204 companion object {
205 @VisibleForTesting internal val STORAGE_KEY = "bluetooth_internal_automatic_turn_on_timer"
206
writeDateToStoragenull207 private fun writeDateToStorage(date: LocalDateTime, resolver: ContentResolver): Boolean {
208 return Settings.Secure.putString(resolver, STORAGE_KEY, date.toString())
209 }
210
getDateFromStoragenull211 private fun getDateFromStorage(resolver: ContentResolver): LocalDateTime? {
212 val date = Settings.Secure.getString(resolver, STORAGE_KEY)
213 return date?.let { LocalDateTime.parse(it) }
214 }
215
resetStoragenull216 fun resetStorage(resolver: ContentResolver) {
217 Settings.Secure.putString(resolver, STORAGE_KEY, null)
218 }
219
startnull220 fun start(
221 looper: Looper,
222 context: Context,
223 receiver: BroadcastReceiver,
224 callback_on: () -> Unit
225 ): Timer? {
226 val now = LocalDateTime.now()
227 val target = getDateFromStorage(context.contentResolver) ?: nextTimeout(now)
228 val timeToSleep =
229 now.until(target, ChronoUnit.NANOS).toDuration(DurationUnit.NANOSECONDS)
230
231 if (timeToSleep.isNegative()) {
232 Log.i(TAG, "Starting now (${now}) as it was scheduled for ${target}")
233 callback_on()
234 resetStorage(context.contentResolver)
235 return null
236 }
237
238 return Timer(looper, context, receiver, callback_on, now, target, timeToSleep)
239 }
240
241 /** Return a LocalDateTime for tomorrow 5 am */
nextTimeoutnull242 private fun nextTimeout(now: LocalDateTime) =
243 LocalDateTime.of(now.toLocalDate(), LocalTime.of(5, 0)).plusDays(1)
244 }
245
246 /** Save timer to storage and stop it */
247 internal fun pause() {
248 Log.i(TAG, "[${this}]: Pausing timer")
249 context.unregisterReceiver(receiver)
250 alarmManager.cancel(this)
251 handler.removeCallbacksAndMessages(null)
252 }
253
254 /** Stop timer and reset storage */
255 @VisibleForTesting
cancelnull256 internal fun cancel() {
257 Log.i(TAG, "[${this}]: Cancelling timer")
258 context.unregisterReceiver(receiver)
259 alarmManager.cancel(this)
260 handler.removeCallbacksAndMessages(null)
261 resetStorage(context.contentResolver)
262 }
263
toStringnull264 override fun toString() =
265 "Timer was scheduled at ${now} and should expire at ${target}. (sleep for ${timeToSleep})."
266 }
267
268 @VisibleForTesting internal val USER_SETTINGS_KEY = "bluetooth_automatic_turn_on"
269
270 /**
271 * *Do not use outside of this file to avoid async issues*
272 *
273 * @return whether the auto on feature is enabled for this user
274 */
275 private fun isFeatureEnabledForUser(resolver: ContentResolver): Boolean {
276 return Settings.Secure.getInt(resolver, USER_SETTINGS_KEY, 0) == 1
277 }
278
279 /**
280 * *Do not use outside of this file to avoid async issues*
281 *
282 * @return whether the auto on feature is supported for the user
283 */
isFeatureSupportedForUsernull284 private fun isFeatureSupportedForUser(resolver: ContentResolver): Boolean {
285 return Settings.Secure.getInt(resolver, USER_SETTINGS_KEY, -1) != -1
286 }
287
288 /**
289 * *Do not use outside of this file to avoid async issues*
290 *
291 * @return whether the auto on feature is enabled for this user
292 */
293 @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
setFeatureEnabledForUserUncheckednull294 private fun setFeatureEnabledForUserUnchecked(context: Context, status: Boolean): Boolean {
295 val ret =
296 Settings.Secure.putInt(context.contentResolver, USER_SETTINGS_KEY, if (status) 1 else 0)
297 if (ret) {
298 context.sendBroadcast(
299 Intent(ACTION_AUTO_ON_STATE_CHANGED)
300 .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
301 .putExtra(
302 EXTRA_AUTO_ON_STATE,
303 if (status) AUTO_ON_STATE_ENABLED else AUTO_ON_STATE_DISABLED
304 ),
305 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
306 BroadcastOptions.makeBasic()
307 .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE)
308 .toBundle(),
309 )
310 }
311 return ret
312 }
313