1 /*
2  * Copyright (C) 2019 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 package com.android.car.settings.bluetooth;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.app.Activity;
22 import android.app.AlertDialog;
23 import android.app.admin.DevicePolicyManager;
24 import android.bluetooth.BluetoothAdapter;
25 import android.bluetooth.BluetoothDevice;
26 import android.content.BroadcastReceiver;
27 import android.content.Context;
28 import android.content.DialogInterface;
29 import android.content.Intent;
30 import android.content.IntentFilter;
31 import android.content.pm.ApplicationInfo;
32 import android.content.pm.PackageManager;
33 import android.content.pm.ResolveInfo;
34 import android.os.Bundle;
35 import android.os.Process;
36 import android.os.UserHandle;
37 import android.os.UserManager;
38 import android.text.TextUtils;
39 
40 import androidx.activity.ComponentActivity;
41 import androidx.annotation.VisibleForTesting;
42 
43 import com.android.car.settings.R;
44 import com.android.car.settings.common.Logger;
45 import com.android.car.ui.AlertDialogBuilder;
46 import com.android.settingslib.bluetooth.BluetoothDiscoverableTimeoutReceiver;
47 import com.android.settingslib.bluetooth.LocalBluetoothAdapter;
48 import com.android.settingslib.bluetooth.LocalBluetoothManager;
49 import com.android.settingslib.core.lifecycle.HideNonSystemOverlayMixin;
50 
51 import java.util.List;
52 
53 /**
54  * This {@link Activity} handles requests to toggle Bluetooth by collecting user
55  * consent and waiting until the state change is completed. It can also be used to make the device
56  * explicitly discoverable for a given amount of time.
57  */
58 public class BluetoothRequestPermissionActivity extends ComponentActivity {
59     private static final Logger LOG = new Logger(BluetoothRequestPermissionActivity.class);
60 
61     @VisibleForTesting
62     static final int REQUEST_UNKNOWN = 0;
63     @VisibleForTesting
64     static final int REQUEST_ENABLE = 1;
65     @VisibleForTesting
66     static final int REQUEST_DISABLE = 2;
67     @VisibleForTesting
68     static final int REQUEST_ENABLE_DISCOVERABLE = 3;
69 
70     private static final int DISCOVERABLE_TIMEOUT_TWO_MINUTES = 120;
71     private static final int DISCOVERABLE_TIMEOUT_ONE_HOUR = 3600;
72 
73     @VisibleForTesting
74     static final String EXTRA_BYPASS_CONFIRM_DIALOG = "bypassConfirmDialog";
75 
76     @VisibleForTesting
77     static final int DEFAULT_DISCOVERABLE_TIMEOUT = DISCOVERABLE_TIMEOUT_TWO_MINUTES;
78     @VisibleForTesting
79     static final int MAX_DISCOVERABLE_TIMEOUT = DISCOVERABLE_TIMEOUT_ONE_HOUR;
80 
81     private AlertDialog mDialog;
82     private boolean mBypassConfirmDialog = false;
83     private int mRequest;
84     private int mTimeout = DEFAULT_DISCOVERABLE_TIMEOUT;
85 
86     @NonNull
87     private CharSequence mAppLabel;
88     private LocalBluetoothAdapter mLocalBluetoothAdapter;
89     private LocalBluetoothManager mLocalBluetoothManager;
90     private StateChangeReceiver mReceiver;
91 
92     @Override
onCreate(Bundle savedInstanceState)93     protected void onCreate(Bundle savedInstanceState) {
94         super.onCreate(savedInstanceState);
95 
96         getLifecycle().addObserver(new HideNonSystemOverlayMixin(this));
97 
98         mRequest = parseIntent();
99         if (mRequest == REQUEST_UNKNOWN) {
100             finishWithResult(RESULT_CANCELED);
101             return;
102         }
103 
104         mLocalBluetoothManager = LocalBluetoothManager.getInstance(
105                 getApplicationContext(), /* onInitCallback= */ null);
106         if (mLocalBluetoothManager == null) {
107             LOG.e("Bluetooth is not supported on this device");
108             finishWithResult(RESULT_CANCELED);
109         }
110 
111         mLocalBluetoothAdapter = mLocalBluetoothManager.getBluetoothAdapter();
112 
113         int btState = mLocalBluetoothAdapter.getState();
114         switch (mRequest) {
115             case REQUEST_DISABLE:
116                 switch (btState) {
117                     case BluetoothAdapter.STATE_OFF:
118                     case BluetoothAdapter.STATE_TURNING_OFF:
119                         proceedAndFinish();
120                         break;
121 
122                     case BluetoothAdapter.STATE_ON:
123                     case BluetoothAdapter.STATE_TURNING_ON:
124                         mDialog = createRequestDisableBluetoothDialog();
125                         mDialog.show();
126                         break;
127 
128                     default:
129                         LOG.e("Unknown adapter state: " + btState);
130                         finishWithResult(RESULT_CANCELED);
131                         break;
132                 }
133                 break;
134             case REQUEST_ENABLE:
135                 switch (btState) {
136                     case BluetoothAdapter.STATE_OFF:
137                     case BluetoothAdapter.STATE_TURNING_OFF:
138                         mDialog = createRequestEnableBluetoothDialog();
139                         mDialog.show();
140                         break;
141                     case BluetoothAdapter.STATE_ON:
142                     case BluetoothAdapter.STATE_TURNING_ON:
143                         proceedAndFinish();
144                         break;
145                     default:
146                         LOG.e("Unknown adapter state: " + btState);
147                         finishWithResult(RESULT_CANCELED);
148                         break;
149                 }
150                 break;
151             case REQUEST_ENABLE_DISCOVERABLE:
152                 switch (btState) {
153                     case BluetoothAdapter.STATE_OFF:
154                     case BluetoothAdapter.STATE_TURNING_OFF:
155                     case BluetoothAdapter.STATE_TURNING_ON:
156                         /*
157                          * Strictly speaking STATE_TURNING_ON belong with STATE_ON; however, BT
158                          * may not be ready when the user clicks yes and we would fail to turn on
159                          * discovery mode. We still show the dialog and handle this case via the
160                          * broadcast receiver.
161                          */
162                         if (isSetupWizardDialogBypass()) {
163                             /*
164                              * In some cases, users may get to the setup wizard's bluetooth fragment
165                              * while in this state. We still need to wait until we reach STATE_ON
166                              * before enabling discovery mode but without showing a dialog.
167                              */
168                             enableBluetoothWithWaitingDialog(/* dialogToShowOnWait= */ null);
169                         } else {
170                             mDialog = createRequestEnableBluetoothDialogWithTimeout(mTimeout);
171                             mDialog.show();
172                         }
173                         break;
174                     case BluetoothAdapter.STATE_ON:
175                         // Allow SetupWizard specifically to skip the discoverability dialog.
176                         if (isSetupWizardDialogBypass()) {
177                             proceedAndFinish();
178                         } else {
179                             mDialog = createDiscoverableConfirmDialog(mTimeout);
180                             mDialog.show();
181                         }
182                         break;
183                     default:
184                         LOG.e("Unknown adapter state: " + btState);
185                         finishWithResult(RESULT_CANCELED);
186                         break;
187                 }
188                 break;
189         }
190     }
191 
192     @Override
onDestroy()193     protected void onDestroy() {
194         super.onDestroy();
195         if (mReceiver != null) {
196             unregisterReceiver(mReceiver);
197         }
198     }
199 
isSetupWizardDialogBypass()200     private boolean isSetupWizardDialogBypass() {
201         String callerName = getCallingPackage();
202         return mBypassConfirmDialog && callerName != null
203             && callerName.equals(getSetupWizardPackageName());
204     }
205 
206     @Nullable
getSetupWizardPackageName()207     private String getSetupWizardPackageName() {
208         Intent intent = new Intent(Intent.ACTION_MAIN);
209         intent.addCategory(Intent.CATEGORY_SETUP_WIZARD);
210 
211         List<ResolveInfo> matches = getPackageManager().queryIntentActivities(intent,
212                 PackageManager.MATCH_SYSTEM_ONLY | PackageManager.MATCH_DIRECT_BOOT_AWARE
213                         | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
214                         | PackageManager.MATCH_DISABLED_COMPONENTS);
215         if (matches.size() == 1) {
216             return matches.get(0).activityInfo.packageName;
217         } else {
218             LOG.e("There should probably be exactly one setup wizard; found " + matches.size()
219                     + ": matches=" + matches);
220             return null;
221         }
222     }
223 
proceedAndFinish()224     private void proceedAndFinish() {
225         if (mRequest == REQUEST_ENABLE_DISCOVERABLE) {
226             finishWithResult(setDiscoverable(mTimeout));
227         } else {
228             finishWithResult(RESULT_OK);
229         }
230     }
231 
232     // Returns the code that should be used to finish the activity.
setDiscoverable(int timeoutSeconds)233     private int setDiscoverable(int timeoutSeconds) {
234         if (!mLocalBluetoothAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE,
235                 timeoutSeconds)) {
236             return RESULT_CANCELED;
237         }
238 
239         // If already in discoverable mode, this will extend the timeout.
240         long endTime = System.currentTimeMillis() + (long) timeoutSeconds * 1000;
241         BluetoothUtils.persistDiscoverableEndTimestamp(/* context= */ this, endTime);
242         if (timeoutSeconds > 0) {
243             BluetoothDiscoverableTimeoutReceiver.setDiscoverableAlarm(/* context= */ this, endTime);
244         }
245 
246         int returnCode = timeoutSeconds;
247         return returnCode < RESULT_FIRST_USER ? RESULT_FIRST_USER : returnCode;
248     }
249 
finishWithResult(int result)250     private void finishWithResult(int result) {
251         if (mDialog != null) {
252             mDialog.dismiss();
253         }
254         setResult(result);
255         finish();
256     }
257 
parseIntent()258     private int parseIntent() {
259         int request;
260         Intent intent = getIntent();
261         if (intent == null) {
262             return REQUEST_UNKNOWN;
263         }
264 
265         switch (intent.getAction()) {
266             case BluetoothAdapter.ACTION_REQUEST_ENABLE:
267                 request = REQUEST_ENABLE;
268                 break;
269             case BluetoothAdapter.ACTION_REQUEST_DISABLE:
270                 request = REQUEST_DISABLE;
271                 break;
272             case BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE:
273                 request = REQUEST_ENABLE_DISCOVERABLE;
274                 mTimeout = intent.getIntExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION,
275                         DEFAULT_DISCOVERABLE_TIMEOUT);
276                 mBypassConfirmDialog = intent.getBooleanExtra(EXTRA_BYPASS_CONFIRM_DIALOG, false);
277 
278                 if (mTimeout < 1 || mTimeout > MAX_DISCOVERABLE_TIMEOUT) {
279                     mTimeout = DEFAULT_DISCOVERABLE_TIMEOUT;
280                 }
281                 break;
282             default:
283                 LOG.e("Error: this activity may be started only with intent "
284                         + BluetoothAdapter.ACTION_REQUEST_ENABLE);
285                 return REQUEST_UNKNOWN;
286         }
287 
288         String packageName = getLaunchedFromPackage();
289         int mCallingUid = getLaunchedFromUid();
290 
291         if (UserHandle.isSameApp(mCallingUid, Process.SYSTEM_UID)
292                 && getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME) != null) {
293             packageName = getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME);
294         }
295 
296         if (!UserHandle.isSameApp(mCallingUid, Process.SYSTEM_UID)
297                 && getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME) != null) {
298             LOG.w("Non-system Uid: " + mCallingUid + " tried to override packageName");
299         }
300 
301         if (!mBypassConfirmDialog && !TextUtils.isEmpty(packageName)) {
302             try {
303                 ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(
304                         packageName, 0);
305                 mAppLabel = applicationInfo.loadLabel(getPackageManager());
306             } catch (PackageManager.NameNotFoundException e) {
307                 LOG.e("Couldn't find app with package name " + packageName);
308                 return REQUEST_UNKNOWN;
309             }
310         }
311 
312         return request;
313     }
314 
createWaitingDialog()315     private AlertDialog createWaitingDialog() {
316         int message = mRequest == REQUEST_DISABLE ? R.string.bluetooth_turning_off
317                 : R.string.bluetooth_turning_on;
318 
319         return new AlertDialogBuilder(/* context= */ this)
320                 .setMessage(message)
321                 .setCancelable(false).setOnCancelListener(
322                         dialog -> finishWithResult(RESULT_CANCELED))
323                 .create();
324     }
325 
326     // Assumes {@code timeoutSeconds} > 0.
createDiscoverableConfirmDialog(int timeoutSeconds)327     private AlertDialog createDiscoverableConfirmDialog(int timeoutSeconds) {
328         String message = mAppLabel != null
329                 ? getString(R.string.bluetooth_ask_discovery, mAppLabel, timeoutSeconds)
330                 : getString(R.string.bluetooth_ask_discovery_no_name, timeoutSeconds);
331 
332         return new AlertDialogBuilder(/* context= */ this)
333                 .setMessage(message)
334                 .setPositiveButton(R.string.allow, (dialog, which) -> proceedAndFinish())
335                 .setNegativeButton(R.string.deny,
336                         (dialog, which) -> finishWithResult(RESULT_CANCELED))
337                 .setOnCancelListener(dialog -> finishWithResult(RESULT_CANCELED))
338                 .create();
339     }
340 
createRequestEnableBluetoothDialog()341     private AlertDialog createRequestEnableBluetoothDialog() {
342         String message = mAppLabel != null
343                 ? getString(R.string.bluetooth_ask_enablement, mAppLabel)
344                 : getString(R.string.bluetooth_ask_enablement_no_name);
345 
346         return new AlertDialogBuilder(/* context= */ this)
347                 .setMessage(message)
348                 .setPositiveButton(R.string.allow, this::onConfirmEnableBluetooth)
349                 .setNegativeButton(R.string.deny,
350                         (dialog, which) -> finishWithResult(RESULT_CANCELED))
351                 .setOnCancelListener(dialog -> finishWithResult(RESULT_CANCELED))
352                 .create();
353     }
354 
355     // Assumes {@code timeoutSeconds} > 0.
createRequestEnableBluetoothDialogWithTimeout(int timeoutSeconds)356     private AlertDialog createRequestEnableBluetoothDialogWithTimeout(int timeoutSeconds) {
357         String message = mAppLabel != null
358                 ? getString(R.string.bluetooth_ask_enablement_and_discovery, mAppLabel,
359                         timeoutSeconds)
360                 : getString(R.string.bluetooth_ask_enablement_and_discovery_no_name,
361                         timeoutSeconds);
362 
363         return new AlertDialogBuilder(/* context= */ this)
364                 .setMessage(message)
365                 .setPositiveButton(R.string.allow, this::onConfirmEnableBluetooth)
366                 .setNegativeButton(R.string.deny,
367                         (dialog, which) -> finishWithResult(RESULT_CANCELED))
368                 .setOnCancelListener(dialog -> finishWithResult(RESULT_CANCELED))
369                 .create();
370     }
371 
onConfirmEnableBluetooth(DialogInterface dialog, int which)372     private void onConfirmEnableBluetooth(DialogInterface dialog, int which) {
373         UserManager userManager = getSystemService(UserManager.class);
374         if (userManager.hasUserRestriction(UserManager.DISALLOW_BLUETOOTH)) {
375             // If Bluetooth is disallowed, don't try to enable it, show policy
376             // transparency message instead.
377             DevicePolicyManager dpm = getSystemService(DevicePolicyManager.class);
378             Intent intent = dpm.createAdminSupportIntent(
379                     UserManager.DISALLOW_BLUETOOTH);
380             if (intent != null) {
381                 startActivity(intent);
382             }
383             return;
384         }
385 
386         if (mRequest == REQUEST_ENABLE) {
387             enableBluetoothWithWaitingDialog(createWaitingDialog());
388         } else {
389             enableBluetoothWithWaitingDialog(createDiscoverableConfirmDialog(mTimeout));
390         }
391     }
392 
393     /*
394      * Ensure bluetooth is enabled and then check if it is in STATE_ON. If it isn't, register
395      * the broadcast receiver to wait for the state to change and show a waiting dialog if provided.
396      */
enableBluetoothWithWaitingDialog(@ullable AlertDialog dialogToShowOnWait)397     private void enableBluetoothWithWaitingDialog(@Nullable AlertDialog dialogToShowOnWait) {
398         mLocalBluetoothAdapter.enable();
399 
400         int desiredState = BluetoothAdapter.STATE_ON;
401         if (mLocalBluetoothAdapter.getState() == desiredState) {
402             proceedAndFinish();
403         } else {
404             // Register this receiver to listen for state change after the enabling has started.
405             mReceiver = new StateChangeReceiver(desiredState);
406             registerReceiver(mReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
407 
408             if (dialogToShowOnWait != null) {
409                 mDialog = dialogToShowOnWait;
410                 mDialog.show();
411             }
412         }
413     }
414 
createRequestDisableBluetoothDialog()415     private AlertDialog createRequestDisableBluetoothDialog() {
416         String message = mAppLabel != null
417                 ? getString(R.string.bluetooth_ask_disablement, mAppLabel)
418                 : getString(R.string.bluetooth_ask_disablement_no_name);
419 
420         return new AlertDialogBuilder(/* context= */ this)
421                 .setMessage(message)
422                 .setPositiveButton(R.string.allow, this::onConfirmDisableBluetooth)
423                 .setNegativeButton(R.string.deny,
424                         (dialog, which) -> finishWithResult(RESULT_CANCELED))
425                 .setOnCancelListener(dialog -> finishWithResult(RESULT_CANCELED))
426                 .create();
427     }
428 
onConfirmDisableBluetooth(DialogInterface dialog, int which)429     private void onConfirmDisableBluetooth(DialogInterface dialog, int which) {
430         mLocalBluetoothAdapter.disable();
431 
432         int desiredState = BluetoothAdapter.STATE_OFF;
433         if (mLocalBluetoothAdapter.getState() == desiredState) {
434             proceedAndFinish();
435         } else {
436             // Register this receiver to listen for state change after the disabling has started.
437             mReceiver = new StateChangeReceiver(desiredState);
438             registerReceiver(mReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
439 
440             // Show dialog while waiting for disabling to complete.
441             mDialog = createWaitingDialog();
442             mDialog.show();
443         }
444     }
445 
446     @VisibleForTesting
getRequestType()447     int getRequestType() {
448         return mRequest;
449     }
450 
451     @VisibleForTesting
getTimeout()452     int getTimeout() {
453         return mTimeout;
454     }
455 
456     @VisibleForTesting
getCurrentDialog()457     AlertDialog getCurrentDialog() {
458         return mDialog;
459     }
460 
461     @VisibleForTesting
getCurrentReceiver()462     StateChangeReceiver getCurrentReceiver() {
463         return mReceiver;
464     }
465 
466     /**
467      * Listens for bluetooth state changes and finishes the activity if changed to the desired
468      * state. If the desired bluetooth state is not received in time, the activity is finished with
469      * {@link Activity#RESULT_CANCELED}.
470      */
471     @VisibleForTesting
472     final class StateChangeReceiver extends BroadcastReceiver {
473         private static final long TOGGLE_TIMEOUT_MILLIS = 10000; // 10 sec
474         private final int mDesiredState;
475 
StateChangeReceiver(int desiredState)476         StateChangeReceiver(int desiredState) {
477             mDesiredState = desiredState;
478 
479             getWindow().getDecorView().postDelayed(() -> {
480                 if (!isFinishing() && !isDestroyed()) {
481                     finishWithResult(RESULT_CANCELED);
482                 }
483             }, TOGGLE_TIMEOUT_MILLIS);
484         }
485 
486         @Override
onReceive(Context context, Intent intent)487         public void onReceive(Context context, Intent intent) {
488             if (intent == null) {
489                 return;
490             }
491 
492             int currentState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
493                     BluetoothDevice.ERROR);
494             if (mDesiredState == currentState) {
495                 proceedAndFinish();
496             }
497         }
498     }
499 }
500