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