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