1 /*
2  * Copyright (C) 2020 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.development;
18 
19 import android.app.Activity;
20 import android.app.Dialog;
21 import android.app.settings.SettingsEnums;
22 import android.content.BroadcastReceiver;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.debug.AdbManager;
27 import android.debug.FingerprintAndPairDevice;
28 import android.debug.IAdbManager;
29 import android.debug.PairDevice;
30 import android.os.Build;
31 import android.os.Bundle;
32 import android.os.RemoteException;
33 import android.os.ServiceManager;
34 import android.provider.Settings;
35 import android.util.Log;
36 
37 import androidx.preference.Preference;
38 import androidx.preference.PreferenceCategory;
39 
40 import com.android.settings.R;
41 import com.android.settings.SettingsActivity;
42 import com.android.settings.core.SubSettingLauncher;
43 import com.android.settings.dashboard.DashboardFragment;
44 import com.android.settings.search.BaseSearchIndexProvider;
45 import com.android.settings.widget.MainSwitchBarController;
46 import com.android.settings.widget.SettingsMainSwitchBar;
47 import com.android.settingslib.core.AbstractPreferenceController;
48 import com.android.settingslib.core.lifecycle.Lifecycle;
49 import com.android.settingslib.development.DevelopmentSettingsEnabler;
50 import com.android.settingslib.search.SearchIndexable;
51 import com.android.settingslib.widget.FooterPreference;
52 
53 import java.util.ArrayList;
54 import java.util.HashMap;
55 import java.util.List;
56 import java.util.Map;
57 
58 /**
59  * Fragment shown when clicking in the "Wireless Debugging" preference in
60  * the developer options.
61  */
62 @SearchIndexable
63 public class WirelessDebuggingFragment extends DashboardFragment
64         implements WirelessDebuggingEnabler.OnEnabledListener, DeveloperOptionAwareMixin {
65 
66     private static final String TAG = "WirelessDebuggingFrag";
67 
68     // Activity result from clicking on a paired device.
69     static final int PAIRED_DEVICE_REQUEST = 0;
70     static final String PAIRED_DEVICE_REQUEST_TYPE = "request_type";
71     static final int FORGET_ACTION = 0;
72     // Activity result from pairing a device.
73     static final int PAIRING_DEVICE_REQUEST = 1;
74     static final String PAIRING_DEVICE_REQUEST_TYPE = "request_type_pairing";
75     static final int SUCCESS_ACTION = 0;
76     static final int FAIL_ACTION = 1;
77     static final String PAIRED_DEVICE_EXTRA = "paired_device";
78     static final String DEVICE_NAME_EXTRA = "device_name";
79     static final String IP_ADDR_EXTRA = "ip_addr";
80 
81     // UI components
82     private static final String PREF_KEY_ADB_DEVICE_NAME = "adb_device_name_pref";
83     private static final String PREF_KEY_ADB_IP_ADDR = "adb_ip_addr_pref";
84     private static final String PREF_KEY_PAIRING_METHODS_CATEGORY = "adb_pairing_methods_category";
85     private static final String PREF_KEY_ADB_CODE_PAIRING = "adb_pair_method_code_pref";
86     private static final String PREF_KEY_PAIRED_DEVICES_CATEGORY = "adb_paired_devices_category";
87     private static final String PREF_KEY_FOOTER_CATEGORY = "adb_wireless_footer_category";
88     private static AdbIpAddressPreferenceController sAdbIpAddressPreferenceController;
89 
90     private final PairingCodeDialogListener mPairingCodeDialogListener =
91             new PairingCodeDialogListener();
92     private WirelessDebuggingEnabler mWifiDebuggingEnabler;
93     private Preference mDeviceNamePreference;
94     private Preference mIpAddrPreference;
95     private PreferenceCategory mPairingMethodsCategory;
96     private Preference mCodePairingPreference;
97     private PreferenceCategory mPairedDevicesCategory;
98     private PreferenceCategory mFooterCategory;
99     private FooterPreference mOffMessagePreference;
100     // Map of paired devices, with the device GUID is the key
101     private Map<String, AdbPairedDevicePreference> mPairedDevicePreferences;
102     private IAdbManager mAdbManager;
103     private int mConnectionPort;
104     private IntentFilter mIntentFilter;
105     private AdbWirelessDialog mPairingCodeDialog;
106 
107     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
108         @Override
109         public void onReceive(Context context, Intent intent) {
110             String action = intent.getAction();
111             if (AdbManager.WIRELESS_DEBUG_PAIRED_DEVICES_ACTION.equals(action)) {
112                 Map<String, PairDevice> newPairedDevicesList =
113                         (HashMap<String, PairDevice>) intent.getSerializableExtra(
114                             AdbManager.WIRELESS_DEVICES_EXTRA);
115                 updatePairedDevicePreferences(newPairedDevicesList);
116             } else if (AdbManager.WIRELESS_DEBUG_STATE_CHANGED_ACTION.equals(action)) {
117                 int status = intent.getIntExtra(AdbManager.WIRELESS_STATUS_EXTRA,
118                         AdbManager.WIRELESS_STATUS_DISCONNECTED);
119                 if (status == AdbManager.WIRELESS_STATUS_CONNECTED
120                         || status == AdbManager.WIRELESS_STATUS_DISCONNECTED) {
121                     sAdbIpAddressPreferenceController.updateState(mIpAddrPreference);
122                 }
123             } else if (AdbManager.WIRELESS_DEBUG_PAIRING_RESULT_ACTION.equals(action)) {
124                 Integer res = intent.getIntExtra(
125                         AdbManager.WIRELESS_STATUS_EXTRA,
126                         AdbManager.WIRELESS_STATUS_FAIL);
127 
128                 if (res.equals(AdbManager.WIRELESS_STATUS_PAIRING_CODE)) {
129                     String pairingCode = intent.getStringExtra(
130                                 AdbManager.WIRELESS_PAIRING_CODE_EXTRA);
131                     if (mPairingCodeDialog != null) {
132                         mPairingCodeDialog.getController().setPairingCode(pairingCode);
133                     }
134                 } else if (res.equals(AdbManager.WIRELESS_STATUS_SUCCESS)) {
135                     removeDialog(AdbWirelessDialogUiBase.MODE_PAIRING);
136                     mPairingCodeDialog = null;
137                 } else if (res.equals(AdbManager.WIRELESS_STATUS_FAIL)) {
138                     removeDialog(AdbWirelessDialogUiBase.MODE_PAIRING);
139                     mPairingCodeDialog = null;
140                     showDialog(AdbWirelessDialogUiBase.MODE_PAIRING_FAILED);
141                 } else if (res.equals(AdbManager.WIRELESS_STATUS_CONNECTED)) {
142                     int port = intent.getIntExtra(AdbManager.WIRELESS_DEBUG_PORT_EXTRA, 0);
143                     Log.i(TAG, "Got pairing code port=" + port);
144                     String ipAddr = sAdbIpAddressPreferenceController.getIpv4Address() + ":" + port;
145                     if (mPairingCodeDialog != null) {
146                         mPairingCodeDialog.getController().setIpAddr(ipAddr);
147                     }
148                 }
149             }
150         }
151     };
152 
153     class PairingCodeDialogListener implements AdbWirelessDialog.AdbWirelessDialogListener {
154         @Override
onDismiss()155         public void onDismiss() {
156             Log.i(TAG, "onDismiss");
157             mPairingCodeDialog = null;
158             try {
159                 mAdbManager.disablePairing();
160             } catch (RemoteException e) {
161                 Log.e(TAG, "Unable to cancel pairing");
162             }
163         }
164     }
165 
166     @Override
onAttach(Context context)167     public void onAttach(Context context) {
168         super.onAttach(context);
169         use(AdbQrCodePreferenceController.class).setParentFragment(this);
170     }
171 
172     @Override
onActivityCreated(Bundle savedInstanceState)173     public void onActivityCreated(Bundle savedInstanceState) {
174         super.onActivityCreated(savedInstanceState);
175         final SettingsActivity activity = (SettingsActivity) getActivity();
176         final SettingsMainSwitchBar switchBar = activity.getSwitchBar();
177         switchBar.setTitle(getContext().getString(R.string.wireless_debugging_main_switch_title));
178 
179         mWifiDebuggingEnabler =  new WirelessDebuggingEnabler(activity,
180                 new MainSwitchBarController(switchBar), this, getSettingsLifecycle());
181     }
182 
183     @Override
onCreate(Bundle icicle)184     public void onCreate(Bundle icicle) {
185         super.onCreate(icicle);
186 
187         addPreferences();
188         mIntentFilter = new IntentFilter(AdbManager.WIRELESS_DEBUG_PAIRED_DEVICES_ACTION);
189         mIntentFilter.addAction(AdbManager.WIRELESS_DEBUG_STATE_CHANGED_ACTION);
190         mIntentFilter.addAction(AdbManager.WIRELESS_DEBUG_PAIRING_RESULT_ACTION);
191     }
192 
addPreferences()193     private void addPreferences() {
194         mDeviceNamePreference =
195             (Preference) findPreference(PREF_KEY_ADB_DEVICE_NAME);
196         mIpAddrPreference =
197             (Preference) findPreference(PREF_KEY_ADB_IP_ADDR);
198         mPairingMethodsCategory =
199                 (PreferenceCategory) findPreference(PREF_KEY_PAIRING_METHODS_CATEGORY);
200         mCodePairingPreference =
201                 (Preference) findPreference(PREF_KEY_ADB_CODE_PAIRING);
202         mCodePairingPreference.setOnPreferenceClickListener(preference -> {
203             showDialog(AdbWirelessDialogUiBase.MODE_PAIRING);
204             return true;
205         });
206 
207         mPairedDevicesCategory =
208                 (PreferenceCategory) findPreference(PREF_KEY_PAIRED_DEVICES_CATEGORY);
209         mFooterCategory =
210                 (PreferenceCategory) findPreference(PREF_KEY_FOOTER_CATEGORY);
211 
212         mOffMessagePreference =
213                 new FooterPreference(mFooterCategory.getContext());
214         final CharSequence title =
215                 getText(com.android.settingslib.R.string.adb_wireless_list_empty_off);
216         mOffMessagePreference.setTitle(title);
217         mFooterCategory.addPreference(mOffMessagePreference);
218     }
219 
220     @Override
onDestroyView()221     public void onDestroyView() {
222         super.onDestroyView();
223 
224         mWifiDebuggingEnabler.teardownSwitchController();
225     }
226 
227     @Override
onResume()228     public void onResume() {
229         super.onResume();
230 
231         getActivity().registerReceiver(mReceiver, mIntentFilter,
232                 Context.RECEIVER_EXPORTED_UNAUDITED);
233     }
234 
235     @Override
onPause()236     public void onPause() {
237         super.onPause();
238 
239         removeDialog(AdbWirelessDialogUiBase.MODE_PAIRING);
240         getActivity().unregisterReceiver(mReceiver);
241     }
242 
243     @Override
onActivityResult(int requestCode, int resultCode, Intent data)244     public void onActivityResult(int requestCode, int resultCode, Intent data) {
245         super.onActivityResult(requestCode, resultCode, data);
246 
247         if (requestCode == PAIRED_DEVICE_REQUEST) {
248             handlePairedDeviceRequest(resultCode, data);
249         } else if (requestCode == PAIRING_DEVICE_REQUEST) {
250             handlePairingDeviceRequest(resultCode, data);
251         }
252     }
253 
254     @Override
getMetricsCategory()255     public int getMetricsCategory() {
256         return SettingsEnums.SETTINGS_ADB_WIRELESS;
257     }
258 
259     @Override
getDialogMetricsCategory(int dialogId)260     public int getDialogMetricsCategory(int dialogId) {
261         return SettingsEnums.ADB_WIRELESS_DEVICE_PAIRING_DIALOG;
262     }
263 
264     @Override
onCreateDialog(int dialogId)265     public Dialog onCreateDialog(int dialogId) {
266         Dialog d = AdbWirelessDialog.createModal(getActivity(),
267                 dialogId == AdbWirelessDialogUiBase.MODE_PAIRING
268                     ? mPairingCodeDialogListener : null, dialogId);
269         if (dialogId == AdbWirelessDialogUiBase.MODE_PAIRING) {
270             mPairingCodeDialog = (AdbWirelessDialog) d;
271             try {
272                 mAdbManager.enablePairingByPairingCode();
273             } catch (RemoteException e) {
274                 Log.e(TAG, "Unable to enable pairing");
275                 mPairingCodeDialog = null;
276                 d = AdbWirelessDialog.createModal(getActivity(), null,
277                         AdbWirelessDialogUiBase.MODE_PAIRING_FAILED);
278             }
279         }
280         if (d != null) {
281             return d;
282         }
283         return super.onCreateDialog(dialogId);
284     }
285 
286     @Override
getPreferenceScreenResId()287     protected int getPreferenceScreenResId() {
288         return R.xml.adb_wireless_settings;
289     }
290 
291     @Override
createPreferenceControllers(Context context)292     protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
293         return buildPreferenceControllers(context, getActivity(), this /* fragment */,
294                 getSettingsLifecycle());
295     }
296 
buildPreferenceControllers( Context context, Activity activity, WirelessDebuggingFragment fragment, Lifecycle lifecycle)297     private static List<AbstractPreferenceController> buildPreferenceControllers(
298             Context context, Activity activity, WirelessDebuggingFragment fragment,
299             Lifecycle lifecycle) {
300         final List<AbstractPreferenceController> controllers = new ArrayList<>();
301         sAdbIpAddressPreferenceController =
302                 new AdbIpAddressPreferenceController(context, lifecycle);
303         controllers.add(sAdbIpAddressPreferenceController);
304 
305         return controllers;
306     }
307 
308     @Override
getLogTag()309     protected String getLogTag() {
310         return TAG;
311     }
312 
313     @Override
onEnabled(boolean enabled)314     public void onEnabled(boolean enabled) {
315         if (enabled) {
316             showDebuggingPreferences();
317             mAdbManager = IAdbManager.Stub.asInterface(ServiceManager.getService(
318                     Context.ADB_SERVICE));
319             try {
320                 FingerprintAndPairDevice[] newList = mAdbManager.getPairedDevices();
321                 Map<String, PairDevice> newMap = new HashMap<>();
322                 for (FingerprintAndPairDevice pair : newList) {
323                     newMap.put(pair.keyFingerprint, pair.device);
324                 }
325                 updatePairedDevicePreferences(newMap);
326                 mConnectionPort = mAdbManager.getAdbWirelessPort();
327                 if (mConnectionPort > 0) {
328                     Log.i(TAG, "onEnabled(): connect_port=" + mConnectionPort);
329                 }
330             } catch (RemoteException e) {
331                 Log.e(TAG, "Unable to request the paired list for Adb wireless");
332             }
333             sAdbIpAddressPreferenceController.updateState(mIpAddrPreference);
334         } else {
335             showOffMessage();
336         }
337     }
338 
showOffMessage()339     private void showOffMessage() {
340         mDeviceNamePreference.setVisible(false);
341         mIpAddrPreference.setVisible(false);
342         mPairingMethodsCategory.setVisible(false);
343         mPairedDevicesCategory.setVisible(false);
344         mFooterCategory.setVisible(true);
345     }
346 
showDebuggingPreferences()347     private void showDebuggingPreferences() {
348         mDeviceNamePreference.setVisible(true);
349         mIpAddrPreference.setVisible(true);
350         mPairingMethodsCategory.setVisible(true);
351         mPairedDevicesCategory.setVisible(true);
352         mFooterCategory.setVisible(false);
353     }
354 
updatePairedDevicePreferences(Map<String, PairDevice> newList)355     private void updatePairedDevicePreferences(Map<String, PairDevice> newList) {
356         // TODO(joshuaduong): Move the non-UI stuff into another thread
357         // as the processing could take some time.
358         if (newList == null) {
359             mPairedDevicesCategory.removeAll();
360             return;
361         }
362         if (mPairedDevicePreferences == null) {
363             mPairedDevicePreferences = new HashMap<String, AdbPairedDevicePreference>();
364         }
365         if (mPairedDevicePreferences.isEmpty()) {
366             for (Map.Entry<String, PairDevice> entry : newList.entrySet()) {
367                 AdbPairedDevicePreference p =
368                         new AdbPairedDevicePreference(entry.getValue(),
369                             mPairedDevicesCategory.getContext());
370                 mPairedDevicePreferences.put(
371                         entry.getKey(),
372                         p);
373                 p.setOnPreferenceClickListener(preference -> {
374                     AdbPairedDevicePreference pref =
375                             (AdbPairedDevicePreference) preference;
376                     launchPairedDeviceDetailsFragment(pref);
377                     return true;
378                 });
379                 mPairedDevicesCategory.addPreference(p);
380             }
381         } else {
382             // Remove any devices no longer on the newList
383             mPairedDevicePreferences.entrySet().removeIf(entry -> {
384                 if (newList.get(entry.getKey()) == null) {
385                     mPairedDevicesCategory.removePreference(entry.getValue());
386                     return true;
387                 } else {
388                     // It is in the newList. Just update the PairDevice value
389                     AdbPairedDevicePreference p =
390                             entry.getValue();
391                     p.setPairedDevice(newList.get(entry.getKey()));
392                     p.refresh();
393                     return false;
394                 }
395             });
396             // Add new devices if any.
397             for (Map.Entry<String, PairDevice> entry :
398                     newList.entrySet()) {
399                 if (mPairedDevicePreferences.get(entry.getKey()) == null) {
400                     AdbPairedDevicePreference p =
401                             new AdbPairedDevicePreference(entry.getValue(),
402                                 mPairedDevicesCategory.getContext());
403                     mPairedDevicePreferences.put(
404                             entry.getKey(),
405                             p);
406                     p.setOnPreferenceClickListener(preference -> {
407                         AdbPairedDevicePreference pref =
408                                 (AdbPairedDevicePreference) preference;
409                         launchPairedDeviceDetailsFragment(pref);
410                         return true;
411                     });
412                     mPairedDevicesCategory.addPreference(p);
413                 }
414             }
415         }
416     }
417 
launchPairedDeviceDetailsFragment(AdbPairedDevicePreference p)418     private void launchPairedDeviceDetailsFragment(AdbPairedDevicePreference p) {
419         // For sending to the device details fragment.
420         p.savePairedDeviceToExtras(p.getExtras());
421         new SubSettingLauncher(getContext())
422                 .setTitleRes(com.android.settingslib.R.string.adb_wireless_device_details_title)
423                 .setDestination(AdbDeviceDetailsFragment.class.getName())
424                 .setArguments(p.getExtras())
425                 .setSourceMetricsCategory(getMetricsCategory())
426                 .setResultListener(this, PAIRED_DEVICE_REQUEST)
427                 .launch();
428     }
429 
handlePairedDeviceRequest(int result, Intent data)430     void handlePairedDeviceRequest(int result, Intent data) {
431         if (result != Activity.RESULT_OK) {
432             return;
433         }
434 
435         Log.i(TAG, "Processing paired device request");
436         int requestType = data.getIntExtra(PAIRED_DEVICE_REQUEST_TYPE, -1);
437 
438         PairDevice p;
439 
440         switch (requestType) {
441             case FORGET_ACTION:
442                 try {
443                     p = (PairDevice) data.getParcelableExtra(PAIRED_DEVICE_EXTRA);
444                     mAdbManager.unpairDevice(p.guid);
445                 } catch (RemoteException e) {
446                     Log.e(TAG, "Unable to forget the device");
447                 }
448                 break;
449             default:
450                 break;
451         }
452     }
453 
handlePairingDeviceRequest(int result, Intent data)454     void handlePairingDeviceRequest(int result, Intent data) {
455         if (result != Activity.RESULT_OK) {
456             return;
457         }
458 
459         int requestType = data.getIntExtra(PAIRING_DEVICE_REQUEST_TYPE, -1);
460         switch (requestType) {
461             case FAIL_ACTION:
462                 showDialog(AdbWirelessDialogUiBase.MODE_PAIRING_FAILED);
463                 break;
464             default:
465                 Log.d(TAG, "Successfully paired device");
466                 break;
467         }
468     }
469 
getDeviceName()470     private String getDeviceName() {
471         // Keep device name in sync with Settings > About phone > Device name
472         String deviceName = Settings.Global.getString(getContext().getContentResolver(),
473                 Settings.Global.DEVICE_NAME);
474         if (deviceName == null) {
475             deviceName = Build.MODEL;
476         }
477         return deviceName;
478     }
479 
480     public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
481             new BaseSearchIndexProvider(R.xml.adb_wireless_settings) {
482 
483                 @Override
484                 protected boolean isPageSearchEnabled(Context context) {
485                     return DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(context);
486                 }
487             };
488 }
489