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.getFeatureFactory()
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