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 (!setFeatureEnabledForUserUnchecked(context, status)) {
140 throw IllegalStateException("AutoOnFeature database failure for user: ${context.getUser()}")
141 }
142 Counter.logIncrement(
143 if (status) "bluetooth.value_auto_on_enabled" else "bluetooth.value_auto_on_disabled"
144 )
145 Timer.resetStorage(context.contentResolver)
146 resetAutoOnTimerForUser(looper, context, state, callback_on)
147 }
148
149 ////////////////////////////////////////////////////////////////////////////////////////////////////
150 ////////////////////////////////////////// PRIVATE METHODS /////////////////////////////////////////
151 ////////////////////////////////////////////////////////////////////////////////////////////////////
152
153 @VisibleForTesting internal var timer: Timer? = null
154
155 @VisibleForTesting
156 internal class Timer
157 private constructor(
158 looper: Looper,
159 private val context: Context,
160 private val receiver: BroadcastReceiver,
161 private val callback_on: () -> Unit,
162 private val now: LocalDateTime,
163 private val target: LocalDateTime,
164 private val timeToSleep: Duration
165 ) : AlarmManager.OnAlarmListener {
166 private val alarmManager: AlarmManager = context.getSystemService(AlarmManager::class.java)!!
167
168 private val handler = Handler(looper)
169
170 init {
171 writeDateToStorage(target, context.contentResolver)
172 alarmManager.set(
173 AlarmManager.ELAPSED_REALTIME,
174 SystemClock.elapsedRealtime() + timeToSleep.inWholeMilliseconds,
175 "Bluetooth AutoOnFeature",
176 this,
177 handler
178 )
179 Log.i(TAG, "[${this}]: Scheduling next Bluetooth restart")
180
181 context.registerReceiver(
182 receiver,
<lambda>null183 IntentFilter().apply {
184 addAction(Intent.ACTION_DATE_CHANGED)
185 addAction(Intent.ACTION_TIMEZONE_CHANGED)
186 addAction(Intent.ACTION_TIME_CHANGED)
187 },
188 null,
189 handler
190 )
191 }
192
onAlarmnull193 override fun onAlarm() {
194 Log.i(TAG, "[${this}]: Bluetooth restarting now")
195 callback_on()
196 cancel()
197 timer = null
198 }
199
200 companion object {
201 @VisibleForTesting internal val STORAGE_KEY = "bluetooth_internal_automatic_turn_on_timer"
202
writeDateToStoragenull203 private fun writeDateToStorage(date: LocalDateTime, resolver: ContentResolver): Boolean {
204 return Settings.Secure.putString(resolver, STORAGE_KEY, date.toString())
205 }
206
getDateFromStoragenull207 private fun getDateFromStorage(resolver: ContentResolver): LocalDateTime? {
208 val date = Settings.Secure.getString(resolver, STORAGE_KEY)
209 return date?.let { LocalDateTime.parse(it) }
210 }
211
resetStoragenull212 fun resetStorage(resolver: ContentResolver) {
213 Settings.Secure.putString(resolver, STORAGE_KEY, null)
214 }
215
startnull216 fun start(
217 looper: Looper,
218 context: Context,
219 receiver: BroadcastReceiver,
220 callback_on: () -> Unit
221 ): Timer? {
222 val now = LocalDateTime.now()
223 val target = getDateFromStorage(context.contentResolver) ?: nextTimeout(now)
224 val timeToSleep =
225 now.until(target, ChronoUnit.NANOS).toDuration(DurationUnit.NANOSECONDS)
226
227 if (timeToSleep.isNegative()) {
228 Log.i(TAG, "Starting now (${now}) as it was scheduled for ${target}")
229 callback_on()
230 resetStorage(context.contentResolver)
231 return null
232 }
233
234 return Timer(looper, context, receiver, callback_on, now, target, timeToSleep)
235 }
236
237 /** Return a LocalDateTime for tomorrow 5 am */
nextTimeoutnull238 private fun nextTimeout(now: LocalDateTime) =
239 LocalDateTime.of(now.toLocalDate(), LocalTime.of(5, 0)).plusDays(1)
240 }
241
242 /** Save timer to storage and stop it */
243 internal fun pause() {
244 Log.i(TAG, "[${this}]: Pausing timer")
245 context.unregisterReceiver(receiver)
246 alarmManager.cancel(this)
247 handler.removeCallbacksAndMessages(null)
248 }
249
250 /** Stop timer and reset storage */
251 @VisibleForTesting
cancelnull252 internal fun cancel() {
253 Log.i(TAG, "[${this}]: Cancelling timer")
254 context.unregisterReceiver(receiver)
255 alarmManager.cancel(this)
256 handler.removeCallbacksAndMessages(null)
257 resetStorage(context.contentResolver)
258 }
259
toStringnull260 override fun toString() =
261 "Timer was scheduled at ${now} and should expire at ${target}. (sleep for ${timeToSleep})."
262 }
263
264 @VisibleForTesting internal val USER_SETTINGS_KEY = "bluetooth_automatic_turn_on"
265
266 /**
267 * *Do not use outside of this file to avoid async issues*
268 *
269 * @return whether the auto on feature is enabled for this user
270 */
271 private fun isFeatureEnabledForUser(resolver: ContentResolver): Boolean {
272 return Settings.Secure.getInt(resolver, USER_SETTINGS_KEY, 0) == 1
273 }
274
275 /**
276 * *Do not use outside of this file to avoid async issues*
277 *
278 * @return whether the auto on feature is supported for the user
279 */
isFeatureSupportedForUsernull280 private fun isFeatureSupportedForUser(resolver: ContentResolver): Boolean {
281 return Settings.Secure.getInt(resolver, USER_SETTINGS_KEY, -1) != -1
282 }
283
284 /**
285 * *Do not use outside of this file to avoid async issues*
286 *
287 * @return whether the auto on feature is enabled for this user
288 */
289 @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
setFeatureEnabledForUserUncheckednull290 private fun setFeatureEnabledForUserUnchecked(context: Context, status: Boolean): Boolean {
291 val ret =
292 Settings.Secure.putInt(context.contentResolver, USER_SETTINGS_KEY, if (status) 1 else 0)
293 if (ret) {
294 context.sendBroadcast(
295 Intent(ACTION_AUTO_ON_STATE_CHANGED)
296 .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
297 .putExtra(
298 EXTRA_AUTO_ON_STATE,
299 if (status) AUTO_ON_STATE_ENABLED else AUTO_ON_STATE_DISABLED
300 ),
301 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
302 BroadcastOptions.makeBasic()
303 .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE)
304 .toBundle(),
305 )
306 }
307 return ret
308 }
309