1 /*
2  * Copyright (C) 2017 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.car.settings.bluetooth;
18 
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.SharedPreferences;
22 import android.content.res.Configuration;
23 import android.content.res.Resources;
24 import android.text.TextUtils;
25 import android.widget.Toast;
26 
27 import com.android.car.settings.R;
28 import com.android.car.settings.common.Logger;
29 import com.android.car.ui.AlertDialogBuilder;
30 import com.android.settingslib.bluetooth.LocalBluetoothAdapter;
31 import com.android.settingslib.bluetooth.LocalBluetoothManager;
32 import com.android.settingslib.bluetooth.LocalBluetoothManager.BluetoothManagerCallback;
33 
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.List;
37 
38 /**
39  * BluetoothUtils provides an interface to the preferences
40  * related to Bluetooth.
41  */
42 public final class BluetoothUtils {
43     private static final Logger LOG = new Logger(BluetoothUtils.class);
44     private static final String SHARED_PREFERENCES_NAME = "bluetooth_settings";
45 
46     private static final BluetoothManagerCallback mOnInitCallback = new BluetoothManagerCallback() {
47         @Override
48         public void onBluetoothManagerInitialized(Context appContext,
49                 LocalBluetoothManager bluetoothManager) {
50             com.android.settingslib.bluetooth.BluetoothUtils.setErrorListener(
51                     com.android.car.settings.bluetooth.BluetoothUtils::showError);
52         }
53     };
54 
55     // If a device was picked from the device picker or was in discoverable mode
56     // in the last 60 seconds, show the pairing dialogs in foreground instead
57     // of raising notifications
58     private static final int GRACE_PERIOD_TO_SHOW_DIALOGS_IN_FOREGROUND = 60 * 1000;
59 
60     private static final String KEY_LAST_SELECTED_DEVICE = "last_selected_device";
61 
62     private static final String KEY_LAST_SELECTED_DEVICE_TIME = "last_selected_device_time";
63 
64     private static final String KEY_DISCOVERABLE_END_TIMESTAMP = "discoverable_end_timestamp";
65 
66     public static final String BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY =
67             "persist.bluetooth.showdeviceswithoutnames";
68 
BluetoothUtils()69     private BluetoothUtils() {
70     }
71 
showError(Context context, String name, int messageResId)72     static void showError(Context context, String name, int messageResId) {
73         showError(context, name, messageResId, getLocalBtManager(context));
74     }
75 
showError(Context context, String name, int messageResId, LocalBluetoothManager manager)76     private static void showError(Context context, String name, int messageResId,
77             LocalBluetoothManager manager) {
78         String message = context.getString(messageResId, name);
79         Context activity = manager.getForegroundActivity();
80         if (manager.isForegroundActivity()) {
81             new AlertDialogBuilder(activity)
82                     .setTitle(R.string.bluetooth_error_title)
83                     .setMessage(message)
84                     .setPositiveButton(android.R.string.ok, null)
85                     .create()
86                     .show();
87         } else {
88             Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
89         }
90     }
91 
getSharedPreferences(Context context)92     private static SharedPreferences getSharedPreferences(Context context) {
93         return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
94     }
95 
getDiscoverableEndTimestamp(Context context)96     static long getDiscoverableEndTimestamp(Context context) {
97         return getSharedPreferences(context).getLong(
98                 KEY_DISCOVERABLE_END_TIMESTAMP, 0);
99     }
100 
shouldShowDialogInForeground(Context context, String deviceAddress, String deviceName)101     static boolean shouldShowDialogInForeground(Context context,
102             String deviceAddress, String deviceName) {
103         LocalBluetoothManager manager = getLocalBtManager(context);
104         if (manager == null) {
105             LOG.v("manager == null - do not show dialog.");
106             return false;
107         }
108 
109         // If Bluetooth Settings is visible
110         if (manager.isForegroundActivity()) {
111             return true;
112         }
113 
114         // If in appliance mode, do not show dialog in foreground.
115         if ((context.getResources().getConfiguration().uiMode &
116                 Configuration.UI_MODE_TYPE_APPLIANCE) == Configuration.UI_MODE_TYPE_APPLIANCE) {
117             LOG.v("in appliance mode - do not show dialog.");
118             return false;
119         }
120 
121         long currentTimeMillis = System.currentTimeMillis();
122         SharedPreferences sharedPreferences = getSharedPreferences(context);
123 
124         // If the device was in discoverABLE mode recently
125         long lastDiscoverableEndTime = sharedPreferences.getLong(
126                 KEY_DISCOVERABLE_END_TIMESTAMP, 0);
127         if ((lastDiscoverableEndTime + GRACE_PERIOD_TO_SHOW_DIALOGS_IN_FOREGROUND)
128                 > currentTimeMillis) {
129             return true;
130         }
131 
132         // If the device was discoverING recently
133         LocalBluetoothAdapter adapter = manager.getBluetoothAdapter();
134         if (adapter != null) {
135             if (adapter.isDiscovering()) {
136                 return true;
137             }
138             if ((adapter.getDiscoveryEndMillis() +
139                     GRACE_PERIOD_TO_SHOW_DIALOGS_IN_FOREGROUND) > currentTimeMillis) {
140                 return true;
141             }
142         }
143 
144         // If the device was picked in the device picker recently
145         if (deviceAddress != null) {
146             String lastSelectedDevice = sharedPreferences.getString(
147                     KEY_LAST_SELECTED_DEVICE, null);
148 
149             if (deviceAddress.equals(lastSelectedDevice)) {
150                 long lastDeviceSelectedTime = sharedPreferences.getLong(
151                         KEY_LAST_SELECTED_DEVICE_TIME, 0);
152                 if ((lastDeviceSelectedTime + GRACE_PERIOD_TO_SHOW_DIALOGS_IN_FOREGROUND)
153                         > currentTimeMillis) {
154                     return true;
155                 }
156             }
157         }
158 
159 
160         if (!TextUtils.isEmpty(deviceName)) {
161             // If the device is a custom BT keyboard specifically for this device
162             String packagedKeyboardName = context.getString(
163                     com.android.internal.R.string.config_packagedKeyboardName);
164             if (deviceName.equals(packagedKeyboardName)) {
165                 LOG.v("showing dialog for packaged keyboard");
166                 return true;
167             }
168         }
169 
170         LOG.v("Found no reason to show the dialog - do not show dialog.");
171         return false;
172     }
173 
persistSelectedDeviceInPicker(Context context, String deviceAddress)174     static void persistSelectedDeviceInPicker(Context context, String deviceAddress) {
175         SharedPreferences.Editor editor = getSharedPreferences(context).edit();
176         editor.putString(KEY_LAST_SELECTED_DEVICE, deviceAddress);
177         editor.putLong(KEY_LAST_SELECTED_DEVICE_TIME, System.currentTimeMillis());
178         editor.apply();
179     }
180 
persistDiscoverableEndTimestamp(Context context, long endTimestamp)181     static void persistDiscoverableEndTimestamp(Context context, long endTimestamp) {
182         SharedPreferences.Editor editor = getSharedPreferences(context).edit();
183         editor.putLong(KEY_DISCOVERABLE_END_TIMESTAMP, endTimestamp);
184         editor.apply();
185     }
186 
getLocalBtManager(Context context)187     public static LocalBluetoothManager getLocalBtManager(Context context) {
188         return LocalBluetoothManager.getInstance(context, mOnInitCallback);
189     }
190 
191     /**
192      * Determines whether to enable bluetooth scanning or not depending on the calling package. The
193      * calling package should be Settings or SystemUi.
194      *
195      * @param context The context to call
196      * @param callingPackageName The package name of the calling activity
197      * @return Whether bluetooth scanning should be enabled
198      */
shouldEnableBTScanning(Context context, String callingPackageName)199     public static boolean shouldEnableBTScanning(Context context, String callingPackageName) {
200         // Find Settings package name
201         String settingsPackageName = context.getPackageName();
202 
203         // Find SystemUi package name
204         Resources resources = context.getResources();
205         String systemUiPackageName;
206         String flattenName = resources
207                 .getString(com.android.internal.R.string.config_systemUIServiceComponent);
208         if (TextUtils.isEmpty(flattenName)) {
209             throw new IllegalStateException("No "
210                     + "com.android.internal.R.string.config_systemUIServiceComponent resource");
211         }
212         try {
213             ComponentName componentName = ComponentName.unflattenFromString(flattenName);
214             systemUiPackageName =  componentName.getPackageName();
215         } catch (RuntimeException e) {
216             throw new IllegalStateException("Invalid component name defined by "
217                     + "com.android.internal.R.string.config_systemUIServiceComponent resource: "
218                     + flattenName);
219         }
220 
221         // Find allowed package names
222         List<String> allowedPackages = new ArrayList<>(Arrays.asList(
223                 resources.getStringArray(R.array.config_allowed_bluetooth_scanning_packages)));
224         allowedPackages.add(settingsPackageName);
225         allowedPackages.add(systemUiPackageName);
226 
227         for (String allowedPackage : allowedPackages) {
228             if (TextUtils.equals(callingPackageName, allowedPackage)) {
229                 return true;
230             }
231         }
232 
233         return false;
234     }
235 }
236