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