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