1 /*
2  * Copyright (C) 2009 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.settings.bluetooth;
18 
19 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
20 
21 import android.app.Activity;
22 import android.bluetooth.BluetoothAdapter;
23 import android.bluetooth.BluetoothDevice;
24 import android.bluetooth.BluetoothStatusCodes;
25 import android.content.BroadcastReceiver;
26 import android.content.Context;
27 import android.content.DialogInterface;
28 import android.content.Intent;
29 import android.content.IntentFilter;
30 import android.content.pm.ApplicationInfo;
31 import android.content.pm.PackageItemInfo;
32 import android.content.pm.PackageManager;
33 import android.os.Bundle;
34 import android.os.Process;
35 import android.os.UserHandle;
36 import android.text.TextUtils;
37 import android.util.Log;
38 
39 import androidx.annotation.NonNull;
40 import androidx.appcompat.app.AlertDialog;
41 
42 import com.android.settings.R;
43 import com.android.settingslib.bluetooth.BluetoothDiscoverableTimeoutReceiver;
44 
45 import kotlin.Unit;
46 
47 import java.time.Duration;
48 
49 /**
50  * RequestPermissionActivity asks the user whether to enable discovery. This is
51  * usually started by an application wanted to start bluetooth and or discovery
52  */
53 public class RequestPermissionActivity extends Activity implements
54         DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
55     // Command line to test this
56     // adb shell am start -a android.bluetooth.adapter.action.REQUEST_ENABLE
57     // adb shell am start -a android.bluetooth.adapter.action.REQUEST_DISCOVERABLE
58     // adb shell am start -a android.bluetooth.adapter.action.REQUEST_DISABLE
59 
60     private static final String TAG = "BtRequestPermission";
61 
62     private static final int MAX_DISCOVERABLE_TIMEOUT = 3600; // 1 hr
63 
64     static final int REQUEST_ENABLE = 1;
65     static final int REQUEST_ENABLE_DISCOVERABLE = 2;
66     static final int REQUEST_DISABLE = 3;
67 
68     private BluetoothAdapter mBluetoothAdapter;
69 
70     private int mTimeout = BluetoothDiscoverableEnabler.DEFAULT_DISCOVERABLE_TIMEOUT;
71 
72     private int mRequest;
73 
74     private AlertDialog mDialog;
75     private AlertDialog mRequestDialog;
76 
77     private BroadcastReceiver mReceiver;
78 
79     private @NonNull CharSequence mAppLabel;
80 
81     @Override
onCreate(Bundle savedInstanceState)82     protected void onCreate(Bundle savedInstanceState) {
83         super.onCreate(savedInstanceState);
84 
85         getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
86 
87         setResult(Activity.RESULT_CANCELED);
88 
89         // Note: initializes mBluetoothAdapter and returns true on error
90         if (parseIntent()) {
91             finish();
92             return;
93         }
94 
95         int btState = mBluetoothAdapter.getState();
96 
97         if (mRequest == REQUEST_DISABLE) {
98             switch (btState) {
99                 case BluetoothAdapter.STATE_OFF:
100                 case BluetoothAdapter.STATE_TURNING_OFF:
101                     proceedAndFinish();
102                     break;
103                 case BluetoothAdapter.STATE_ON:
104                 case BluetoothAdapter.STATE_TURNING_ON:
105                     mRequestDialog =
106                             RequestPermissionHelper.INSTANCE.requestDisable(this, mAppLabel,
107                                     () -> {
108                                         onDisableConfirmed();
109                                         return Unit.INSTANCE;
110                                     },
111                                     () -> {
112                                         cancelAndFinish();
113                                         return Unit.INSTANCE;
114                                     });
115                     if (mRequestDialog != null) {
116                         mRequestDialog.show();
117                     }
118                     break;
119                 default:
120                     Log.e(TAG, "Unknown adapter state: " + btState);
121                     cancelAndFinish();
122                     break;
123             }
124         } else {
125             switch (btState) {
126                 case BluetoothAdapter.STATE_OFF:
127                 case BluetoothAdapter.STATE_TURNING_OFF:
128                 case BluetoothAdapter.STATE_TURNING_ON:
129                     /*
130                      * Strictly speaking STATE_TURNING_ON belong with STATE_ON;
131                      * however, BT may not be ready when the user clicks yes and we
132                      * would fail to turn on discovery mode. By kicking this to the
133                      * RequestPermissionHelperActivity, this class will handle that
134                      * case via the broadcast receiver.
135                      */
136 
137                     // Show the helper dialog to ask the user about enabling bt AND discovery
138                     mRequestDialog =
139                             RequestPermissionHelper.INSTANCE.requestEnable(this, mAppLabel,
140                                     mRequest == REQUEST_ENABLE_DISCOVERABLE ? mTimeout : -1,
141                                     () -> {
142                                         onEnableConfirmed();
143                                         return Unit.INSTANCE;
144                                     },
145                                     () -> {
146                                         cancelAndFinish();
147                                         return Unit.INSTANCE;
148                                     });
149                     if (mRequestDialog != null) {
150                         mRequestDialog.show();
151                     }
152                     break;
153                 case BluetoothAdapter.STATE_ON:
154                     if (mRequest == REQUEST_ENABLE) {
155                         // Nothing to do. Already enabled.
156                         proceedAndFinish();
157                     } else {
158                         // Ask the user about enabling discovery mode
159                         createDialog();
160                     }
161                     break;
162                 default:
163                     Log.e(TAG, "Unknown adapter state: " + btState);
164                     cancelAndFinish();
165                     break;
166             }
167         }
168     }
169 
createDialog()170     private void createDialog() {
171         if (getResources().getBoolean(R.bool.auto_confirm_bluetooth_activation_dialog)) {
172             onClick(null, DialogInterface.BUTTON_POSITIVE);
173             return;
174         }
175 
176         AlertDialog.Builder builder = new AlertDialog.Builder(this);
177 
178         // Non-null receiver means we are toggling
179         if (mReceiver != null) {
180             switch (mRequest) {
181                 case REQUEST_ENABLE:
182                 case REQUEST_ENABLE_DISCOVERABLE: {
183                     builder.setMessage(getString(R.string.bluetooth_turning_on));
184                 } break;
185 
186                 default: {
187                     builder.setMessage(getString(R.string.bluetooth_turning_off));
188                 } break;
189             }
190             builder.setCancelable(false);
191         } else {
192             // Ask the user whether to turn on discovery mode or not
193             // For lasting discoverable mode there is a different message
194             if (mTimeout == BluetoothDiscoverableEnabler.DISCOVERABLE_TIMEOUT_NEVER) {
195                 CharSequence message = mAppLabel != null
196                         ? getString(R.string.bluetooth_ask_lasting_discovery, mAppLabel)
197                         : getString(R.string.bluetooth_ask_lasting_discovery_no_name);
198                 builder.setMessage(message);
199             } else {
200                 CharSequence message = mAppLabel != null
201                         ? getString(R.string.bluetooth_ask_discovery, mAppLabel, mTimeout)
202                         : getString(R.string.bluetooth_ask_discovery_no_name, mTimeout);
203                 builder.setMessage(message);
204             }
205             builder.setPositiveButton(getString(R.string.allow), this);
206             builder.setNegativeButton(getString(R.string.deny), this);
207         }
208 
209         builder.setOnDismissListener(this);
210         mDialog = builder.create();
211         mDialog.show();
212     }
213 
onEnableConfirmed()214     private void onEnableConfirmed() {
215         mBluetoothAdapter.enable();
216         if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) {
217             proceedAndFinish();
218         } else {
219             // If BT is not up yet, show "Turning on Bluetooth..."
220             mReceiver = new StateChangeReceiver();
221             registerReceiver(mReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
222             createDialog();
223         }
224     }
225 
onDisableConfirmed()226     private void onDisableConfirmed() {
227         mBluetoothAdapter.disable();
228         if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF) {
229             proceedAndFinish();
230         } else {
231             // If BT is not up yet, show "Turning off Bluetooth..."
232             mReceiver = new StateChangeReceiver();
233             registerReceiver(mReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
234             createDialog();
235         }
236     }
237 
onClick(DialogInterface dialog, int which)238     public void onClick(DialogInterface dialog, int which) {
239         switch (which) {
240             case DialogInterface.BUTTON_POSITIVE:
241                 proceedAndFinish();
242                 break;
243 
244             case DialogInterface.BUTTON_NEGATIVE:
245                 cancelAndFinish();
246                 break;
247         }
248     }
249 
250     @Override
onDismiss(final DialogInterface dialog)251     public void onDismiss(final DialogInterface dialog) {
252         cancelAndFinish();
253     }
254 
proceedAndFinish()255     private void proceedAndFinish() {
256         int returnCode;
257 
258         if (mRequest == REQUEST_ENABLE || mRequest == REQUEST_DISABLE) {
259             // BT toggled. Done
260             returnCode = RESULT_OK;
261         } else {
262             mBluetoothAdapter.setDiscoverableTimeout(Duration.ofSeconds(mTimeout));
263             if (mBluetoothAdapter.setScanMode(
264                     BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE)
265                     == BluetoothStatusCodes.SUCCESS) {
266                 // If already in discoverable mode, this will extend the timeout.
267                 long endTime = System.currentTimeMillis() + (long) mTimeout * 1000;
268                 LocalBluetoothPreferences.persistDiscoverableEndTimestamp(
269                         this, endTime);
270                 if (0 < mTimeout) {
271                     BluetoothDiscoverableTimeoutReceiver.setDiscoverableAlarm(this, endTime);
272                 }
273                 returnCode = mTimeout;
274                 // Activity.RESULT_FIRST_USER should be 1
275                 if (returnCode < RESULT_FIRST_USER) {
276                     returnCode = RESULT_FIRST_USER;
277                 }
278             } else {
279                 returnCode = RESULT_CANCELED;
280             }
281         }
282 
283         setResult(returnCode);
284         finish();
285     }
286 
cancelAndFinish()287     private void cancelAndFinish() {
288         setResult(Activity.RESULT_CANCELED);
289         finish();
290     }
291 
292     /**
293      * Parse the received Intent and initialize mBluetoothAdapter.
294      * @return true if an error occurred; false otherwise
295      */
parseIntent()296     private boolean parseIntent() {
297         Intent intent = getIntent();
298         if (intent == null) {
299             return true;
300         }
301         if (intent.getAction().equals(BluetoothAdapter.ACTION_REQUEST_ENABLE)) {
302             mRequest = REQUEST_ENABLE;
303         } else if (intent.getAction().equals(BluetoothAdapter.ACTION_REQUEST_DISABLE)) {
304             mRequest = REQUEST_DISABLE;
305         } else if (intent.getAction().equals(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)) {
306             mRequest = REQUEST_ENABLE_DISCOVERABLE;
307             mTimeout = intent.getIntExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION,
308                     BluetoothDiscoverableEnabler.DEFAULT_DISCOVERABLE_TIMEOUT);
309 
310             Log.d(TAG, "Setting Bluetooth Discoverable Timeout = " + mTimeout);
311 
312             if (mTimeout < 1 || mTimeout > MAX_DISCOVERABLE_TIMEOUT) {
313                 mTimeout = BluetoothDiscoverableEnabler.DEFAULT_DISCOVERABLE_TIMEOUT;
314             }
315         } else {
316             Log.e(TAG, "Error: this activity may be started only with intent "
317                     + BluetoothAdapter.ACTION_REQUEST_ENABLE + ", "
318                     + BluetoothAdapter.ACTION_REQUEST_DISABLE + " or "
319                     + BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
320             setResult(RESULT_CANCELED);
321             return true;
322         }
323 
324         String packageName = getLaunchedFromPackage();
325         int mCallingUid = getLaunchedFromUid();
326 
327         if (UserHandle.isSameApp(mCallingUid, Process.SYSTEM_UID)
328                 && getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME) != null) {
329             packageName = getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME);
330         }
331 
332         if (!UserHandle.isSameApp(mCallingUid, Process.SYSTEM_UID)
333                 && getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME) != null) {
334             Log.w(TAG, "Non-system Uid: " + mCallingUid + " tried to override packageName \n");
335         }
336 
337         if (!TextUtils.isEmpty(packageName)) {
338             try {
339                 ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(
340                         packageName, 0);
341                 mAppLabel = applicationInfo.loadSafeLabel(getPackageManager(),
342                         PackageItemInfo.DEFAULT_MAX_LABEL_SIZE_PX,
343                         PackageItemInfo.SAFE_LABEL_FLAG_TRIM
344                                 | PackageItemInfo.SAFE_LABEL_FLAG_FIRST_LINE);
345             } catch (PackageManager.NameNotFoundException e) {
346                 Log.e(TAG, "Couldn't find app with package name " + packageName);
347                 setResult(RESULT_CANCELED);
348                 return true;
349             }
350         }
351 
352         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
353         if (mBluetoothAdapter == null) {
354             Log.e(TAG, "Error: there's a problem starting Bluetooth");
355             setResult(RESULT_CANCELED);
356             return true;
357         }
358 
359         return false;
360     }
361 
362     @Override
onDestroy()363     protected void onDestroy() {
364         super.onDestroy();
365         if (mReceiver != null) {
366             unregisterReceiver(mReceiver);
367             mReceiver = null;
368         }
369         if (mDialog != null && mDialog.isShowing()) {
370             mDialog.dismiss();
371             mDialog = null;
372         }
373         if (mRequestDialog != null && mRequestDialog.isShowing()) {
374             mRequestDialog.dismiss();
375             mRequestDialog = null;
376         }
377     }
378 
379     @Override
onBackPressed()380     public void onBackPressed() {
381         setResult(RESULT_CANCELED);
382         super.onBackPressed();
383     }
384 
385     private final class StateChangeReceiver extends BroadcastReceiver {
386         private static final long TOGGLE_TIMEOUT_MILLIS = 10000; // 10 sec
387 
StateChangeReceiver()388         public StateChangeReceiver() {
389             getWindow().getDecorView().postDelayed(() -> {
390                 if (!isFinishing() && !isDestroyed()) {
391                     cancelAndFinish();
392                 }
393             }, TOGGLE_TIMEOUT_MILLIS);
394         }
395 
onReceive(Context context, Intent intent)396         public void onReceive(Context context, Intent intent) {
397             if (intent == null) {
398                 return;
399             }
400             final int currentState = intent.getIntExtra(
401                     BluetoothAdapter.EXTRA_STATE, BluetoothDevice.ERROR);
402             switch (mRequest) {
403                 case REQUEST_ENABLE:
404                 case REQUEST_ENABLE_DISCOVERABLE: {
405                     if (currentState == BluetoothAdapter.STATE_ON) {
406                         proceedAndFinish();
407                     }
408                 } break;
409 
410                 case REQUEST_DISABLE: {
411                     if (currentState == BluetoothAdapter.STATE_OFF) {
412                         proceedAndFinish();
413                     }
414                 } break;
415             }
416         }
417     }
418 }
419