1 /* 2 * Copyright 2018 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_BLUETOOTH; 20 import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; 21 22 import static com.android.car.settings.enterprise.ActionDisabledByAdminDialogFragment.DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG; 23 import static com.android.car.settings.enterprise.EnterpriseUtils.hasUserRestrictionByDpm; 24 25 import android.bluetooth.BluetoothAdapter; 26 import android.car.drivingstate.CarUxRestrictions; 27 import android.content.BroadcastReceiver; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.IntentFilter; 31 import android.content.pm.ApplicationInfo; 32 import android.content.pm.PackageManager; 33 import android.content.pm.ResolveInfo; 34 import android.os.UserManager; 35 36 import androidx.lifecycle.LifecycleObserver; 37 import androidx.preference.Preference; 38 39 import com.android.car.settings.R; 40 import com.android.car.settings.common.FragmentController; 41 import com.android.car.settings.common.Logger; 42 import com.android.car.settings.common.PreferenceController; 43 import com.android.car.settings.common.Settings; 44 import com.android.car.settings.enterprise.EnterpriseUtils; 45 46 import java.util.List; 47 import java.util.stream.Collectors; 48 49 /** 50 * Controls a preference that, when clicked, launches the page for pairing new Bluetooth devices. 51 * The associated preference for this controller should define the fragment attribute or an intent 52 * to launch for the Bluetooth device pairing page. If the adapter is not enabled, a click will 53 * enable Bluetooth. The summary message is updated to indicate this effect to the user. 54 */ 55 public class PairNewDevicePreferenceController extends PreferenceController<Preference> implements 56 LifecycleObserver { 57 58 private static final Logger LOG = new Logger(PairNewDevicePreferenceController.class); 59 60 private final IntentFilter mIntentFilter = new IntentFilter( 61 BluetoothAdapter.ACTION_STATE_CHANGED); 62 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 63 @Override 64 public void onReceive(Context context, Intent intent) { 65 refreshUi(); 66 } 67 }; 68 private final UserManager mUserManager; 69 PairNewDevicePreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)70 public PairNewDevicePreferenceController(Context context, String preferenceKey, 71 FragmentController fragmentController, CarUxRestrictions uxRestrictions) { 72 super(context, preferenceKey, fragmentController, uxRestrictions); 73 mUserManager = UserManager.get(context); 74 } 75 76 @Override getPreferenceType()77 protected Class<Preference> getPreferenceType() { 78 return Preference.class; 79 } 80 81 @Override onCreateInternal()82 protected void onCreateInternal() { 83 super.onCreateInternal(); 84 setClickableWhileDisabled(getPreference(), /* clickable= */ true, p -> { 85 if (getAvailabilityStatus() == AVAILABLE_FOR_VIEWING) { 86 showActionDisabledByAdminDialog(); 87 } 88 }); 89 } 90 showActionDisabledByAdminDialog()91 private void showActionDisabledByAdminDialog() { 92 getFragmentController().showDialog( 93 EnterpriseUtils.getActionDisabledByAdminDialog(getContext(), 94 DISALLOW_CONFIG_BLUETOOTH), 95 DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG); 96 } 97 98 @Override checkInitialized()99 protected void checkInitialized() { 100 if (getPreference().getIntent() == null && getPreference().getFragment() == null) { 101 throw new IllegalStateException( 102 "Preference should declare fragment or intent for page to pair new devices"); 103 } 104 } 105 106 @Override getDefaultAvailabilityStatus()107 protected int getDefaultAvailabilityStatus() { 108 if (!getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)) { 109 return UNSUPPORTED_ON_DEVICE; 110 } 111 if (hasUserRestrictionByDpm(getContext(), DISALLOW_CONFIG_BLUETOOTH)) { 112 return AVAILABLE_FOR_VIEWING; 113 } 114 return isUserRestricted() ? DISABLED_FOR_PROFILE : AVAILABLE; 115 } 116 isUserRestricted()117 private boolean isUserRestricted() { 118 return mUserManager.hasUserRestriction(DISALLOW_BLUETOOTH) 119 || mUserManager.hasUserRestriction(DISALLOW_CONFIG_BLUETOOTH); 120 } 121 122 @Override onStartInternal()123 protected void onStartInternal() { 124 getContext().registerReceiver(mReceiver, mIntentFilter); 125 } 126 127 @Override onStopInternal()128 protected void onStopInternal() { 129 getContext().unregisterReceiver(mReceiver); 130 } 131 132 @Override updateState(Preference preference)133 protected void updateState(Preference preference) { 134 preference.setSummary( 135 BluetoothAdapter.getDefaultAdapter().isEnabled() ? "" : getContext().getString( 136 R.string.bluetooth_pair_new_device_summary)); 137 } 138 139 /** 140 * Checks whether provided application object represents system application. 141 */ isSystemApp(ResolveInfo info)142 private boolean isSystemApp(ResolveInfo info) { 143 return (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; 144 } 145 146 @Override handlePreferenceClicked(Preference preference)147 protected boolean handlePreferenceClicked(Preference preference) { 148 // Enable the adapter if it is not on (user is notified via summary message). 149 BluetoothAdapter.getDefaultAdapter().enable(); 150 151 Context context = getContext(); 152 153 if (!context.getResources().getBoolean( 154 R.bool.config_use_custom_pair_device_flow)) { 155 LOG.i("Custom pairing device flow is deactivated"); 156 return false; 157 } 158 159 Intent intent = getPreference().getIntent(); 160 if (intent == null) { 161 LOG.e("Preference doesn't contain " + Settings.ACTION_PAIR_DEVICE_SETTINGS + " intent"); 162 return false; 163 } 164 165 List<ResolveInfo> infos = context.getPackageManager().queryIntentActivities(intent, 166 PackageManager.MATCH_DEFAULT_ONLY); 167 168 List<String> packages = infos.stream().filter(this::isSystemApp).map( 169 ri -> ri.activityInfo.packageName).collect( 170 Collectors.toUnmodifiableList()); 171 172 LOG.i("Found " + packages.size() + " system packages matching " 173 + intent.getAction() + " intent action. Found packages are: " + packages); 174 175 if (packages.size() > 1) { 176 LOG.w("More than one package found satisfying " + intent.getAction() 177 + " intent action. Picking the first one."); 178 } 179 180 for (String packageName : packages) { 181 LOG.i("Starting custom pair device activity identified by " + packageName + " package"); 182 183 intent.setPackage(packageName); 184 context.startActivity(intent); 185 return true; // new activity will handle pairing workflow 186 } 187 188 return false; 189 } 190 } 191