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