| /* |
| * Copyright (C) 2009 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.settings.bluetooth; |
| |
| import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; |
| |
| import android.annotation.NonNull; |
| import android.app.Activity; |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothStatusCodes; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.DialogInterface; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.PackageItemInfo; |
| import android.content.pm.PackageManager; |
| import android.os.Bundle; |
| import android.os.Process; |
| import android.os.UserHandle; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import androidx.appcompat.app.AlertDialog; |
| |
| import com.android.settings.R; |
| import com.android.settingslib.bluetooth.BluetoothDiscoverableTimeoutReceiver; |
| |
| import kotlin.Unit; |
| |
| import java.time.Duration; |
| |
| /** |
| * RequestPermissionActivity asks the user whether to enable discovery. This is |
| * usually started by an application wanted to start bluetooth and or discovery |
| */ |
| public class RequestPermissionActivity extends Activity implements |
| DialogInterface.OnClickListener, DialogInterface.OnDismissListener { |
| // Command line to test this |
| // adb shell am start -a android.bluetooth.adapter.action.REQUEST_ENABLE |
| // adb shell am start -a android.bluetooth.adapter.action.REQUEST_DISCOVERABLE |
| // adb shell am start -a android.bluetooth.adapter.action.REQUEST_DISABLE |
| |
| private static final String TAG = "BtRequestPermission"; |
| |
| private static final int MAX_DISCOVERABLE_TIMEOUT = 3600; // 1 hr |
| |
| static final int REQUEST_ENABLE = 1; |
| static final int REQUEST_ENABLE_DISCOVERABLE = 2; |
| static final int REQUEST_DISABLE = 3; |
| |
| private BluetoothAdapter mBluetoothAdapter; |
| |
| private int mTimeout = BluetoothDiscoverableEnabler.DEFAULT_DISCOVERABLE_TIMEOUT; |
| |
| private int mRequest; |
| |
| private AlertDialog mDialog; |
| private AlertDialog mRequestDialog; |
| |
| private BroadcastReceiver mReceiver; |
| |
| private @NonNull CharSequence mAppLabel; |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); |
| |
| setResult(Activity.RESULT_CANCELED); |
| |
| // Note: initializes mBluetoothAdapter and returns true on error |
| if (parseIntent()) { |
| finish(); |
| return; |
| } |
| |
| int btState = mBluetoothAdapter.getState(); |
| |
| if (mRequest == REQUEST_DISABLE) { |
| switch (btState) { |
| case BluetoothAdapter.STATE_OFF: |
| case BluetoothAdapter.STATE_TURNING_OFF: |
| proceedAndFinish(); |
| break; |
| case BluetoothAdapter.STATE_ON: |
| case BluetoothAdapter.STATE_TURNING_ON: |
| mRequestDialog = |
| RequestPermissionHelper.INSTANCE.requestDisable(this, mAppLabel, |
| () -> { |
| onDisableConfirmed(); |
| return Unit.INSTANCE; |
| }, |
| () -> { |
| cancelAndFinish(); |
| return Unit.INSTANCE; |
| }); |
| if (mRequestDialog != null) { |
| mRequestDialog.show(); |
| } |
| break; |
| default: |
| Log.e(TAG, "Unknown adapter state: " + btState); |
| cancelAndFinish(); |
| break; |
| } |
| } else { |
| switch (btState) { |
| case BluetoothAdapter.STATE_OFF: |
| case BluetoothAdapter.STATE_TURNING_OFF: |
| case BluetoothAdapter.STATE_TURNING_ON: |
| /* |
| * Strictly speaking STATE_TURNING_ON belong with STATE_ON; |
| * however, BT may not be ready when the user clicks yes and we |
| * would fail to turn on discovery mode. By kicking this to the |
| * RequestPermissionHelperActivity, this class will handle that |
| * case via the broadcast receiver. |
| */ |
| |
| // Show the helper dialog to ask the user about enabling bt AND discovery |
| mRequestDialog = |
| RequestPermissionHelper.INSTANCE.requestEnable(this, mAppLabel, |
| mRequest == REQUEST_ENABLE_DISCOVERABLE ? mTimeout : -1, |
| () -> { |
| onEnableConfirmed(); |
| return Unit.INSTANCE; |
| }, |
| () -> { |
| cancelAndFinish(); |
| return Unit.INSTANCE; |
| }); |
| if (mRequestDialog != null) { |
| mRequestDialog.show(); |
| } |
| break; |
| case BluetoothAdapter.STATE_ON: |
| if (mRequest == REQUEST_ENABLE) { |
| // Nothing to do. Already enabled. |
| proceedAndFinish(); |
| } else { |
| // Ask the user about enabling discovery mode |
| createDialog(); |
| } |
| break; |
| default: |
| Log.e(TAG, "Unknown adapter state: " + btState); |
| cancelAndFinish(); |
| break; |
| } |
| } |
| } |
| |
| private void createDialog() { |
| if (getResources().getBoolean(R.bool.auto_confirm_bluetooth_activation_dialog)) { |
| onClick(null, DialogInterface.BUTTON_POSITIVE); |
| return; |
| } |
| |
| AlertDialog.Builder builder = new AlertDialog.Builder(this); |
| |
| // Non-null receiver means we are toggling |
| if (mReceiver != null) { |
| switch (mRequest) { |
| case REQUEST_ENABLE: |
| case REQUEST_ENABLE_DISCOVERABLE: { |
| builder.setMessage(getString(R.string.bluetooth_turning_on)); |
| } break; |
| |
| default: { |
| builder.setMessage(getString(R.string.bluetooth_turning_off)); |
| } break; |
| } |
| builder.setCancelable(false); |
| } else { |
| // Ask the user whether to turn on discovery mode or not |
| // For lasting discoverable mode there is a different message |
| if (mTimeout == BluetoothDiscoverableEnabler.DISCOVERABLE_TIMEOUT_NEVER) { |
| CharSequence message = mAppLabel != null |
| ? getString(R.string.bluetooth_ask_lasting_discovery, mAppLabel) |
| : getString(R.string.bluetooth_ask_lasting_discovery_no_name); |
| builder.setMessage(message); |
| } else { |
| CharSequence message = mAppLabel != null |
| ? getString(R.string.bluetooth_ask_discovery, mAppLabel, mTimeout) |
| : getString(R.string.bluetooth_ask_discovery_no_name, mTimeout); |
| builder.setMessage(message); |
| } |
| builder.setPositiveButton(getString(R.string.allow), this); |
| builder.setNegativeButton(getString(R.string.deny), this); |
| } |
| |
| builder.setOnDismissListener(this); |
| mDialog = builder.create(); |
| mDialog.show(); |
| } |
| |
| private void onEnableConfirmed() { |
| mBluetoothAdapter.enable(); |
| if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) { |
| proceedAndFinish(); |
| } else { |
| // If BT is not up yet, show "Turning on Bluetooth..." |
| mReceiver = new StateChangeReceiver(); |
| registerReceiver(mReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); |
| createDialog(); |
| } |
| } |
| |
| private void onDisableConfirmed() { |
| mBluetoothAdapter.disable(); |
| if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF) { |
| proceedAndFinish(); |
| } else { |
| // If BT is not up yet, show "Turning off Bluetooth..." |
| mReceiver = new StateChangeReceiver(); |
| registerReceiver(mReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); |
| createDialog(); |
| } |
| } |
| |
| public void onClick(DialogInterface dialog, int which) { |
| switch (which) { |
| case DialogInterface.BUTTON_POSITIVE: |
| proceedAndFinish(); |
| break; |
| |
| case DialogInterface.BUTTON_NEGATIVE: |
| cancelAndFinish(); |
| break; |
| } |
| } |
| |
| @Override |
| public void onDismiss(final DialogInterface dialog) { |
| cancelAndFinish(); |
| } |
| |
| private void proceedAndFinish() { |
| int returnCode; |
| |
| if (mRequest == REQUEST_ENABLE || mRequest == REQUEST_DISABLE) { |
| // BT toggled. Done |
| returnCode = RESULT_OK; |
| } else { |
| mBluetoothAdapter.setDiscoverableTimeout(Duration.ofSeconds(mTimeout)); |
| if (mBluetoothAdapter.setScanMode( |
| BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) |
| == BluetoothStatusCodes.SUCCESS) { |
| // If already in discoverable mode, this will extend the timeout. |
| long endTime = System.currentTimeMillis() + (long) mTimeout * 1000; |
| LocalBluetoothPreferences.persistDiscoverableEndTimestamp( |
| this, endTime); |
| if (0 < mTimeout) { |
| BluetoothDiscoverableTimeoutReceiver.setDiscoverableAlarm(this, endTime); |
| } |
| returnCode = mTimeout; |
| // Activity.RESULT_FIRST_USER should be 1 |
| if (returnCode < RESULT_FIRST_USER) { |
| returnCode = RESULT_FIRST_USER; |
| } |
| } else { |
| returnCode = RESULT_CANCELED; |
| } |
| } |
| |
| setResult(returnCode); |
| finish(); |
| } |
| |
| private void cancelAndFinish() { |
| setResult(Activity.RESULT_CANCELED); |
| finish(); |
| } |
| |
| /** |
| * Parse the received Intent and initialize mBluetoothAdapter. |
| * @return true if an error occurred; false otherwise |
| */ |
| private boolean parseIntent() { |
| Intent intent = getIntent(); |
| if (intent == null) { |
| return true; |
| } |
| if (intent.getAction().equals(BluetoothAdapter.ACTION_REQUEST_ENABLE)) { |
| mRequest = REQUEST_ENABLE; |
| } else if (intent.getAction().equals(BluetoothAdapter.ACTION_REQUEST_DISABLE)) { |
| mRequest = REQUEST_DISABLE; |
| } else if (intent.getAction().equals(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)) { |
| mRequest = REQUEST_ENABLE_DISCOVERABLE; |
| mTimeout = intent.getIntExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, |
| BluetoothDiscoverableEnabler.DEFAULT_DISCOVERABLE_TIMEOUT); |
| |
| Log.d(TAG, "Setting Bluetooth Discoverable Timeout = " + mTimeout); |
| |
| if (mTimeout < 1 || mTimeout > MAX_DISCOVERABLE_TIMEOUT) { |
| mTimeout = BluetoothDiscoverableEnabler.DEFAULT_DISCOVERABLE_TIMEOUT; |
| } |
| } else { |
| Log.e(TAG, "Error: this activity may be started only with intent " |
| + BluetoothAdapter.ACTION_REQUEST_ENABLE + ", " |
| + BluetoothAdapter.ACTION_REQUEST_DISABLE + " or " |
| + BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); |
| setResult(RESULT_CANCELED); |
| return true; |
| } |
| |
| String packageName = getLaunchedFromPackage(); |
| int mCallingUid = getLaunchedFromUid(); |
| |
| if (UserHandle.isSameApp(mCallingUid, Process.SYSTEM_UID) |
| && getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME) != null) { |
| packageName = getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME); |
| } |
| |
| if (!UserHandle.isSameApp(mCallingUid, Process.SYSTEM_UID) |
| && getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME) != null) { |
| Log.w(TAG, "Non-system Uid: " + mCallingUid + " tried to override packageName \n"); |
| } |
| |
| if (!TextUtils.isEmpty(packageName)) { |
| try { |
| ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo( |
| packageName, 0); |
| mAppLabel = applicationInfo.loadSafeLabel(getPackageManager(), |
| PackageItemInfo.DEFAULT_MAX_LABEL_SIZE_PX, |
| PackageItemInfo.SAFE_LABEL_FLAG_TRIM |
| | PackageItemInfo.SAFE_LABEL_FLAG_FIRST_LINE); |
| } catch (PackageManager.NameNotFoundException e) { |
| Log.e(TAG, "Couldn't find app with package name " + packageName); |
| setResult(RESULT_CANCELED); |
| return true; |
| } |
| } |
| |
| mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); |
| if (mBluetoothAdapter == null) { |
| Log.e(TAG, "Error: there's a problem starting Bluetooth"); |
| setResult(RESULT_CANCELED); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| @Override |
| protected void onDestroy() { |
| super.onDestroy(); |
| if (mReceiver != null) { |
| unregisterReceiver(mReceiver); |
| mReceiver = null; |
| } |
| if (mDialog != null && mDialog.isShowing()) { |
| mDialog.dismiss(); |
| mDialog = null; |
| } |
| if (mRequestDialog != null && mRequestDialog.isShowing()) { |
| mRequestDialog.dismiss(); |
| mRequestDialog = null; |
| } |
| } |
| |
| @Override |
| public void onBackPressed() { |
| setResult(RESULT_CANCELED); |
| super.onBackPressed(); |
| } |
| |
| private final class StateChangeReceiver extends BroadcastReceiver { |
| private static final long TOGGLE_TIMEOUT_MILLIS = 10000; // 10 sec |
| |
| public StateChangeReceiver() { |
| getWindow().getDecorView().postDelayed(() -> { |
| if (!isFinishing() && !isDestroyed()) { |
| cancelAndFinish(); |
| } |
| }, TOGGLE_TIMEOUT_MILLIS); |
| } |
| |
| public void onReceive(Context context, Intent intent) { |
| if (intent == null) { |
| return; |
| } |
| final int currentState = intent.getIntExtra( |
| BluetoothAdapter.EXTRA_STATE, BluetoothDevice.ERROR); |
| switch (mRequest) { |
| case REQUEST_ENABLE: |
| case REQUEST_ENABLE_DISCOVERABLE: { |
| if (currentState == BluetoothAdapter.STATE_ON) { |
| proceedAndFinish(); |
| } |
| } break; |
| |
| case REQUEST_DISABLE: { |
| if (currentState == BluetoothAdapter.STATE_OFF) { |
| proceedAndFinish(); |
| } |
| } break; |
| } |
| } |
| } |
| } |