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