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