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.app.AlertDialog;
22 import android.bluetooth.BluetoothAdapter;
23 import android.bluetooth.BluetoothDevice;
24 import android.content.BroadcastReceiver;
25 import android.content.Context;
26 import android.content.DialogInterface;
27 import android.content.Intent;
28 import android.content.IntentFilter;
29 import android.content.pm.ApplicationInfo;
30 import android.content.pm.PackageManager;
31 import android.os.Bundle;
32 import android.text.TextUtils;
33 import android.util.Log;
34 
35 import com.android.settings.R;
36 import com.android.settingslib.bluetooth.BluetoothDiscoverableTimeoutReceiver;
37 import com.android.settingslib.bluetooth.LocalBluetoothAdapter;
38 import com.android.settingslib.bluetooth.LocalBluetoothManager;
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 {
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 = "RequestPermissionActivity";
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 LocalBluetoothAdapter mLocalAdapter;
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 mLocalAdapter and returns true on error
78         if (parseIntent()) {
79             finish();
80             return;
81         }
82 
83         int btState = mLocalAdapter.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         mDialog = builder.create();
192         mDialog.show();
193     }
194 
195     @Override
onActivityResult(int requestCode, int resultCode, Intent data)196     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
197         if (resultCode != Activity.RESULT_OK) {
198             cancelAndFinish();
199             return;
200         }
201 
202         switch (mRequest) {
203             case REQUEST_ENABLE:
204             case REQUEST_ENABLE_DISCOVERABLE: {
205                 if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_ON) {
206                     proceedAndFinish();
207                 } else {
208                     // If BT is not up yet, show "Turning on Bluetooth..."
209                     mReceiver = new StateChangeReceiver();
210                     registerReceiver(mReceiver, new IntentFilter(
211                             BluetoothAdapter.ACTION_STATE_CHANGED));
212                     createDialog();
213                 }
214             } break;
215 
216             case REQUEST_DISABLE: {
217                 if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_OFF) {
218                     proceedAndFinish();
219                 } else {
220                     // If BT is not up yet, show "Turning off Bluetooth..."
221                     mReceiver = new StateChangeReceiver();
222                     registerReceiver(mReceiver, new IntentFilter(
223                             BluetoothAdapter.ACTION_STATE_CHANGED));
224                     createDialog();
225                 }
226             } break;
227 
228             default: {
229                 cancelAndFinish();
230             } break;
231         }
232     }
233 
onClick(DialogInterface dialog, int which)234     public void onClick(DialogInterface dialog, int which) {
235         switch (which) {
236             case DialogInterface.BUTTON_POSITIVE:
237                 proceedAndFinish();
238                 break;
239 
240             case DialogInterface.BUTTON_NEGATIVE:
241                 setResult(RESULT_CANCELED);
242                 finish();
243                 break;
244         }
245     }
246 
proceedAndFinish()247     private void proceedAndFinish() {
248         int returnCode;
249 
250         if (mRequest == REQUEST_ENABLE || mRequest == REQUEST_DISABLE) {
251             // BT toggled. Done
252             returnCode = RESULT_OK;
253         } else if (mLocalAdapter.setScanMode(
254                 BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, mTimeout)) {
255             // If already in discoverable mode, this will extend the timeout.
256             long endTime = System.currentTimeMillis() + (long) mTimeout * 1000;
257             LocalBluetoothPreferences.persistDiscoverableEndTimestamp(
258                     this, endTime);
259             if (0 < mTimeout) {
260                BluetoothDiscoverableTimeoutReceiver.setDiscoverableAlarm(this, endTime);
261             }
262             returnCode = mTimeout;
263             // Activity.RESULT_FIRST_USER should be 1
264             if (returnCode < RESULT_FIRST_USER) {
265                 returnCode = RESULT_FIRST_USER;
266             }
267         } else {
268             returnCode = RESULT_CANCELED;
269         }
270 
271         if (mDialog != null) {
272             mDialog.dismiss();
273         }
274 
275         setResult(returnCode);
276         finish();
277     }
278 
cancelAndFinish()279     private void cancelAndFinish() {
280         setResult(Activity.RESULT_CANCELED);
281         finish();
282     }
283 
284     /**
285      * Parse the received Intent and initialize mLocalBluetoothAdapter.
286      * @return true if an error occurred; false otherwise
287      */
parseIntent()288     private boolean parseIntent() {
289         Intent intent = getIntent();
290         if (intent == null) {
291             return true;
292         }
293         if (intent.getAction().equals(BluetoothAdapter.ACTION_REQUEST_ENABLE)) {
294             mRequest = REQUEST_ENABLE;
295         } else if (intent.getAction().equals(BluetoothAdapter.ACTION_REQUEST_DISABLE)) {
296             mRequest = REQUEST_DISABLE;
297         } else if (intent.getAction().equals(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)) {
298             mRequest = REQUEST_ENABLE_DISCOVERABLE;
299             mTimeout = intent.getIntExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION,
300                     BluetoothDiscoverableEnabler.DEFAULT_DISCOVERABLE_TIMEOUT);
301 
302             Log.d(TAG, "Setting Bluetooth Discoverable Timeout = " + mTimeout);
303 
304             if (mTimeout < 1 || mTimeout > MAX_DISCOVERABLE_TIMEOUT) {
305                 mTimeout = BluetoothDiscoverableEnabler.DEFAULT_DISCOVERABLE_TIMEOUT;
306             }
307         } else {
308             Log.e(TAG, "Error: this activity may be started only with intent "
309                     + BluetoothAdapter.ACTION_REQUEST_ENABLE + " or "
310                     + BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
311             setResult(RESULT_CANCELED);
312             return true;
313         }
314 
315         LocalBluetoothManager manager = Utils.getLocalBtManager(this);
316         if (manager == null) {
317             Log.e(TAG, "Error: there's a problem starting Bluetooth");
318             setResult(RESULT_CANCELED);
319             return true;
320         }
321 
322         String packageName = getCallingPackage();
323         if (TextUtils.isEmpty(packageName)) {
324             packageName = getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME);
325         }
326         if (!TextUtils.isEmpty(packageName)) {
327             try {
328                 ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(
329                         packageName, 0);
330                 mAppLabel = applicationInfo.loadSafeLabel(getPackageManager());
331             } catch (PackageManager.NameNotFoundException e) {
332                 Log.e(TAG, "Couldn't find app with package name " + packageName);
333                 setResult(RESULT_CANCELED);
334                 return true;
335             }
336         }
337 
338         mLocalAdapter = manager.getBluetoothAdapter();
339 
340         return false;
341     }
342 
343     @Override
onDestroy()344     protected void onDestroy() {
345         super.onDestroy();
346         if (mReceiver != null) {
347             unregisterReceiver(mReceiver);
348             mReceiver = null;
349         }
350     }
351 
352     @Override
onBackPressed()353     public void onBackPressed() {
354         setResult(RESULT_CANCELED);
355         super.onBackPressed();
356     }
357 
358     private final class StateChangeReceiver extends BroadcastReceiver {
359         private static final long TOGGLE_TIMEOUT_MILLIS = 10000; // 10 sec
360 
StateChangeReceiver()361         public StateChangeReceiver() {
362             getWindow().getDecorView().postDelayed(() -> {
363                 if (!isFinishing() && !isDestroyed()) {
364                     cancelAndFinish();
365                 }
366             }, TOGGLE_TIMEOUT_MILLIS);
367         }
368 
onReceive(Context context, Intent intent)369         public void onReceive(Context context, Intent intent) {
370             if (intent == null) {
371                 return;
372             }
373             final int currentState = intent.getIntExtra(
374                     BluetoothAdapter.EXTRA_STATE, BluetoothDevice.ERROR);
375             switch (mRequest) {
376                 case REQUEST_ENABLE:
377                 case REQUEST_ENABLE_DISCOVERABLE: {
378                     if (currentState == BluetoothAdapter.STATE_ON) {
379                         proceedAndFinish();
380                     }
381                 } break;
382 
383                 case REQUEST_DISABLE: {
384                     if (currentState == BluetoothAdapter.STATE_OFF) {
385                         proceedAndFinish();
386                     }
387                 } break;
388             }
389         }
390     }
391 }
392