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