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