1 /* 2 * Copyright (C) 2021 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 static com.android.internal.util.CollectionUtils.filter; 20 21 import android.companion.Association; 22 import android.companion.CompanionDeviceManager; 23 import android.companion.ICompanionDeviceManager; 24 import android.content.Context; 25 import android.content.DialogInterface; 26 import android.content.Intent; 27 import android.content.pm.PackageManager; 28 import android.graphics.drawable.Drawable; 29 import android.net.Uri; 30 import android.os.RemoteException; 31 import android.os.ServiceManager; 32 import android.provider.DeviceConfig; 33 import android.text.TextUtils; 34 import android.util.Log; 35 36 import androidx.annotation.VisibleForTesting; 37 import androidx.appcompat.app.AlertDialog; 38 import androidx.preference.Preference; 39 import androidx.preference.PreferenceCategory; 40 import androidx.preference.PreferenceFragmentCompat; 41 import androidx.preference.PreferenceScreen; 42 43 import com.android.settings.R; 44 import com.android.settings.core.SettingsUIDeviceConfig; 45 import com.android.settings.overlay.FeatureFactory; 46 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 47 import com.android.settingslib.core.lifecycle.Lifecycle; 48 49 import com.google.common.base.Objects; 50 51 import java.util.ArrayList; 52 import java.util.HashSet; 53 import java.util.List; 54 import java.util.Set; 55 import java.util.stream.Collectors; 56 57 58 /** 59 * This class adds Companion Device app rows to launch the app or remove the associations 60 */ 61 public class BluetoothDetailsCompanionAppsController extends BluetoothDetailsController { 62 public static final String KEY_DEVICE_COMPANION_APPS = "device_companion_apps"; 63 private static final String LOG_TAG = "BTCompanionController"; 64 65 private CachedBluetoothDevice mCachedDevice; 66 67 @VisibleForTesting 68 PreferenceCategory mProfilesContainer; 69 70 @VisibleForTesting 71 CompanionDeviceManager mCompanionDeviceManager; 72 73 @VisibleForTesting 74 PackageManager mPackageManager; 75 BluetoothDetailsCompanionAppsController(Context context, PreferenceFragmentCompat fragment, CachedBluetoothDevice device, Lifecycle lifecycle)76 public BluetoothDetailsCompanionAppsController(Context context, 77 PreferenceFragmentCompat fragment, CachedBluetoothDevice device, Lifecycle lifecycle) { 78 super(context, fragment, device, lifecycle); 79 mCachedDevice = device; 80 mCompanionDeviceManager = context.getSystemService(CompanionDeviceManager.class); 81 mPackageManager = context.getPackageManager(); 82 lifecycle.addObserver(this); 83 } 84 85 @Override init(PreferenceScreen screen)86 protected void init(PreferenceScreen screen) { 87 mProfilesContainer = screen.findPreference(getPreferenceKey()); 88 mProfilesContainer.setLayoutResource(R.layout.preference_companion_app); 89 } 90 getAssociations(String address)91 private List<Association> getAssociations(String address) { 92 return filter( 93 mCompanionDeviceManager.getAllAssociations(), 94 a -> Objects.equal(address, a.getDeviceMacAddress())); 95 } 96 removePreference(PreferenceCategory container, String packageName)97 private static void removePreference(PreferenceCategory container, String packageName) { 98 Preference preference = container.findPreference(packageName); 99 if (preference != null) { 100 container.removePreference(preference); 101 } 102 } 103 removeAssociationDialog(String packageName, String address, PreferenceCategory container, CharSequence appName, Context context)104 private void removeAssociationDialog(String packageName, String address, 105 PreferenceCategory container, CharSequence appName, Context context) { 106 DialogInterface.OnClickListener dialogClickListener = (dialog, which) -> { 107 if (which == DialogInterface.BUTTON_POSITIVE) { 108 removeAssociation(packageName, address, container); 109 } 110 }; 111 112 AlertDialog.Builder builder = new AlertDialog.Builder(context); 113 114 builder.setPositiveButton( 115 R.string.bluetooth_companion_app_remove_association_confirm_button, 116 dialogClickListener) 117 .setNegativeButton(android.R.string.cancel, dialogClickListener) 118 .setTitle(R.string.bluetooth_companion_app_remove_association_dialog_title) 119 .setMessage(mContext.getString( 120 R.string.bluetooth_companion_app_body, appName, mCachedDevice.getName())) 121 .show(); 122 } 123 removeAssociation(String packageName, String address, PreferenceCategory container)124 private static void removeAssociation(String packageName, String address, 125 PreferenceCategory container) { 126 try { 127 java.util.Objects.requireNonNull(ICompanionDeviceManager.Stub.asInterface( 128 ServiceManager.getService( 129 Context.COMPANION_DEVICE_SERVICE))).disassociate( 130 address, packageName); 131 } catch (RemoteException e) { 132 throw new RuntimeException(e); 133 } 134 135 removePreference(container, packageName); 136 } 137 getAppName(String packageName)138 private CharSequence getAppName(String packageName) { 139 CharSequence appName = null; 140 try { 141 appName = mPackageManager.getApplicationLabel( 142 mPackageManager.getApplicationInfo(packageName, 0)); 143 } catch (PackageManager.NameNotFoundException e) { 144 Log.e(LOG_TAG, "Package Not Found", e); 145 } 146 147 return appName; 148 } 149 getPreferencesNeedToShow(String address, PreferenceCategory container)150 private List<String> getPreferencesNeedToShow(String address, PreferenceCategory container) { 151 List<String> preferencesToRemove = new ArrayList<>(); 152 Set<String> packages = getAssociations(address) 153 .stream().map(Association::getPackageName) 154 .collect(Collectors.toSet()); 155 156 for (int i = 0; i < container.getPreferenceCount(); i++) { 157 String preferenceKey = container.getPreference(i).getKey(); 158 if (packages.isEmpty() || !packages.contains(preferenceKey)) { 159 preferencesToRemove.add(preferenceKey); 160 } 161 } 162 163 for (String preferenceName : preferencesToRemove) { 164 removePreference(container, preferenceName); 165 } 166 167 return packages.stream() 168 .filter(p -> container.findPreference(p) == null) 169 .collect(Collectors.toList()); 170 } 171 172 /** 173 * Refreshes the state of the preferences for all the associations, possibly adding or 174 * removing preferences as needed. 175 */ 176 @Override refresh()177 protected void refresh() { 178 // Do nothing. More details in b/191992001 179 } 180 181 /** 182 * Add preferences for each association for the bluetooth device 183 */ updatePreferences(Context context, String address, PreferenceCategory container)184 public void updatePreferences(Context context, 185 String address, PreferenceCategory container) { 186 // If the device is FastPair, remove CDM companion apps. 187 final BluetoothFeatureProvider bluetoothFeatureProvider = FeatureFactory.getFactory(context) 188 .getBluetoothFeatureProvider(context); 189 final boolean sliceEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, 190 SettingsUIDeviceConfig.BT_SLICE_SETTINGS_ENABLED, true); 191 final Uri settingsUri = bluetoothFeatureProvider.getBluetoothDeviceSettingsUri( 192 mCachedDevice.getDevice()); 193 if (sliceEnabled && settingsUri != null) { 194 container.removeAll(); 195 return; 196 } 197 198 Set<String> addedPackages = new HashSet<>(); 199 200 for (String packageName : getPreferencesNeedToShow(address, container)) { 201 CharSequence appName = getAppName(packageName); 202 203 if (TextUtils.isEmpty(appName) || !addedPackages.add(packageName)) { 204 continue; 205 } 206 207 Drawable removeIcon = context.getResources().getDrawable(R.drawable.ic_clear); 208 CompanionAppWidgetPreference preference = new CompanionAppWidgetPreference( 209 removeIcon, 210 v -> removeAssociationDialog(packageName, address, container, appName, context), 211 context 212 ); 213 214 Drawable appIcon; 215 216 try { 217 appIcon = mPackageManager.getApplicationIcon(packageName); 218 } catch (PackageManager.NameNotFoundException e) { 219 Log.e(LOG_TAG, "Icon Not Found", e); 220 continue; 221 } 222 Intent intent = context.getPackageManager().getLaunchIntentForPackage(packageName); 223 preference.setIcon(appIcon); 224 preference.setTitle(appName.toString()); 225 preference.setOnPreferenceClickListener(v -> { 226 context.startActivity(intent); 227 return true; 228 }); 229 230 preference.setKey(packageName); 231 preference.setVisible(true); 232 container.addPreference(preference); 233 } 234 } 235 236 @Override getPreferenceKey()237 public String getPreferenceKey() { 238 return KEY_DEVICE_COMPANION_APPS; 239 } 240 } 241