1 /*
2  * Copyright (C) 2018 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.wifi.dpp;
18 
19 import android.app.ActionBar;
20 import android.app.Activity;
21 import android.app.settings.SettingsEnums;
22 import android.content.Context;
23 import android.content.pm.ActivityInfo;
24 import android.net.wifi.EasyConnectStatusCallback;
25 import android.net.wifi.WifiManager;
26 import android.os.Bundle;
27 import android.text.TextUtils;
28 import android.util.Log;
29 import android.view.LayoutInflater;
30 import android.view.View;
31 import android.view.ViewGroup;
32 import android.view.accessibility.AccessibilityEvent;
33 import android.widget.Button;
34 import android.widget.ImageView;
35 import android.widget.ProgressBar;
36 
37 import androidx.lifecycle.ViewModelProviders;
38 
39 import com.android.settings.R;
40 
41 import java.util.concurrent.Executor;
42 
43 /**
44  * After getting Wi-Fi network information and(or) QR code, this fragment config a device to connect
45  * to the Wi-Fi network.
46  */
47 public class WifiDppAddDeviceFragment extends WifiDppQrCodeBaseFragment {
48     private static final String TAG = "WifiDppAddDeviceFragment";
49 
50     private ProgressBar mProgressBar;
51     private ImageView mWifiApPictureView;
52     private Button mChooseDifferentNetwork;
53     private Button mButtonLeft;
54     private Button mButtonRight;
55 
56     private int mLatestStatusCode = WifiDppUtils.EASY_CONNECT_EVENT_FAILURE_NONE;
57 
58     // Key for Bundle usage
59     private static final String KEY_LATEST_ERROR_CODE = "key_latest_error_code";
60 
61     private class EasyConnectConfiguratorStatusCallback extends EasyConnectStatusCallback {
62         @Override
onEnrolleeSuccess(int newNetworkId)63         public void onEnrolleeSuccess(int newNetworkId) {
64             // Do nothing
65         }
66 
67         @Override
onConfiguratorSuccess(int code)68         public void onConfiguratorSuccess(int code) {
69             showSuccessUi(/* isConfigurationChange */ false);
70         }
71 
72         @Override
onFailure(int code)73         public void onFailure(int code) {
74             Log.d(TAG, "EasyConnectConfiguratorStatusCallback.onFailure " + code);
75 
76             showErrorUi(code, /* isConfigurationChange */ false);
77         }
78 
79         @Override
onProgress(int code)80         public void onProgress(int code) {
81             // Do nothing
82         }
83     }
84 
showSuccessUi(boolean isConfigurationChange)85     private void showSuccessUi(boolean isConfigurationChange) {
86         setHeaderIconImageResource(R.drawable.ic_devices_check_circle_green);
87         mTitle.setText(R.string.wifi_dpp_wifi_shared_with_device);
88         mProgressBar.setVisibility(isGoingInitiator() ? View.VISIBLE : View.INVISIBLE);
89         mSummary.setVisibility(View.INVISIBLE);
90         mWifiApPictureView.setImageResource(R.drawable.wifi_dpp_success);
91         mChooseDifferentNetwork.setVisibility(View.INVISIBLE);
92         mButtonLeft.setText(R.string.wifi_dpp_add_another_device);
93         mButtonLeft.setOnClickListener(v -> getFragmentManager().popBackStack());
94         mButtonRight.setText(R.string.done);
95         mButtonRight.setOnClickListener(v -> {
96             final Activity activity = getActivity();
97             activity.setResult(Activity.RESULT_OK);
98             activity.finish();
99         });
100 
101         if (!isConfigurationChange) {
102             mLatestStatusCode = WifiDppUtils.EASY_CONNECT_EVENT_SUCCESS;
103             changeFocusAndAnnounceChange(mButtonRight, mTitle);
104         }
105     }
106 
showErrorUi(int code, boolean isConfigurationChange)107     private void showErrorUi(int code, boolean isConfigurationChange) {
108         switch (code) {
109             case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_INVALID_URI:
110                 mSummary.setText(R.string.wifi_dpp_could_not_detect_valid_qr_code);
111                 break;
112 
113             case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_AUTHENTICATION:
114                 mSummary.setText(R.string.wifi_dpp_failure_authentication_or_configuration);
115                 break;
116 
117             case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_NOT_COMPATIBLE:
118                 mSummary.setText(R.string.wifi_dpp_failure_not_compatible);
119                 break;
120 
121             case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_CONFIGURATION:
122                 mSummary.setText(R.string.wifi_dpp_failure_authentication_or_configuration);
123                 break;
124 
125             case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_BUSY:
126                 if (isConfigurationChange) {
127                     return;
128                 }
129 
130                 if (code == mLatestStatusCode) {
131                     throw(new IllegalStateException("Tried restarting EasyConnectSession but still"
132                             + "receiving EASY_CONNECT_EVENT_FAILURE_BUSY"));
133                 }
134 
135                 mLatestStatusCode = code;
136                 final WifiManager wifiManager =
137                         getContext().getSystemService(WifiManager.class);
138                 wifiManager.stopEasyConnectSession();
139                 startWifiDppConfiguratorInitiator();
140                 return;
141 
142             case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_TIMEOUT:
143                 mSummary.setText(R.string.wifi_dpp_failure_timeout);
144                 break;
145 
146             case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_GENERIC:
147                 mSummary.setText(R.string.wifi_dpp_failure_generic);
148                 break;
149 
150             case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_NOT_SUPPORTED:
151                 mSummary.setText(getString(R.string.wifi_dpp_failure_not_supported, getSsid()));
152                 break;
153 
154             case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_INVALID_NETWORK:
155                 throw(new IllegalStateException("Wi-Fi DPP configurator used a non-PSK/non-SAE"
156                         + "network to handshake"));
157 
158             default:
159                 throw(new IllegalStateException("Unexpected Wi-Fi DPP error"));
160         }
161 
162         mTitle.setText(R.string.wifi_dpp_could_not_add_device);
163         mWifiApPictureView.setImageResource(R.drawable.wifi_dpp_error);
164         mChooseDifferentNetwork.setVisibility(View.INVISIBLE);
165         if (hasRetryButton(code)) {
166             mButtonRight.setText(R.string.retry);
167         } else {
168             mButtonRight.setText(R.string.done);
169             mButtonRight.setOnClickListener(v -> getActivity().finish());
170             mButtonLeft.setVisibility(View.INVISIBLE);
171         }
172 
173         if (isGoingInitiator()) {
174             mSummary.setText(R.string.wifi_dpp_sharing_wifi_with_this_device);
175         }
176 
177         mProgressBar.setVisibility(isGoingInitiator() ? View.VISIBLE : View.INVISIBLE);
178         mButtonRight.setVisibility(isGoingInitiator() ? View.INVISIBLE : View.VISIBLE);
179 
180         if (!isConfigurationChange) {
181             mLatestStatusCode = code;
182             changeFocusAndAnnounceChange(mButtonRight, mSummary);
183         }
184     }
185 
hasRetryButton(int code)186     private boolean hasRetryButton(int code) {
187         switch (code) {
188             case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_INVALID_URI:
189             case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_NOT_COMPATIBLE:
190                 return false;
191 
192             default:
193                 break;
194         }
195 
196         return true;
197     }
198 
199     @Override
getMetricsCategory()200     public int getMetricsCategory() {
201         return SettingsEnums.SETTINGS_WIFI_DPP_CONFIGURATOR;
202     }
203 
204     @Override
onCreate(Bundle savedInstanceState)205     public void onCreate(Bundle savedInstanceState) {
206         super.onCreate(savedInstanceState);
207 
208         if (savedInstanceState != null) {
209             mLatestStatusCode = savedInstanceState.getInt(KEY_LATEST_ERROR_CODE);
210         }
211 
212         final WifiDppInitiatorViewModel model =
213                 ViewModelProviders.of(this).get(WifiDppInitiatorViewModel.class);
214 
215         model.getStatusCode().observe(this, statusCode -> {
216             // After configuration change, observe callback will be triggered,
217             // do nothing for this case if a handshake does not end
218             if (model.isGoingInitiator()) {
219                 return;
220             }
221 
222             int code = statusCode.intValue();
223             if (code == WifiDppUtils.EASY_CONNECT_EVENT_SUCCESS) {
224                 new EasyConnectConfiguratorStatusCallback().onConfiguratorSuccess(code);
225             } else {
226                 new EasyConnectConfiguratorStatusCallback().onFailure(code);
227             }
228         });
229     }
230 
231     @Override
onActivityCreated(Bundle savedInstanceState)232     public void onActivityCreated(Bundle savedInstanceState) {
233         super.onActivityCreated(savedInstanceState);
234 
235         final ActionBar actionBar = getActivity().getActionBar();
236         if (actionBar != null) {
237             actionBar.hide();
238         }
239     }
240 
241     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)242     public final View onCreateView(LayoutInflater inflater, ViewGroup container,
243             Bundle savedInstanceState) {
244         return inflater.inflate(R.layout.wifi_dpp_add_device_fragment, container,
245                 /* attachToRoot */ false);
246     }
247 
248     @Override
onViewCreated(View view, Bundle savedInstanceState)249     public void onViewCreated(View view, Bundle savedInstanceState) {
250         super.onViewCreated(view, savedInstanceState);
251 
252         setHeaderIconImageResource(R.drawable.ic_devices_other_opaque_black);
253 
254         mProgressBar = view.findViewById(R.id.indeterminate_bar);
255 
256         final WifiQrCode wifiQrCode = ((WifiDppConfiguratorActivity) getActivity())
257                 .getWifiDppQrCode();
258         final String information = wifiQrCode.getInformation();
259         if (TextUtils.isEmpty(information)) {
260             mTitle.setText(R.string.wifi_dpp_device_found);
261         } else {
262             mTitle.setText(information);
263         }
264 
265         updateSummary();
266         mWifiApPictureView = view.findViewById(R.id.wifi_ap_picture_view);
267 
268         mChooseDifferentNetwork = view.findViewById(R.id.choose_different_network);
269         mChooseDifferentNetwork.setOnClickListener(v ->
270             mClickChooseDifferentNetworkListener.onClickChooseDifferentNetwork()
271         );
272 
273         mButtonLeft = view.findViewById(R.id.button_left);
274         mButtonLeft.setText(R.string.cancel);
275         mButtonLeft.setOnClickListener(v -> getActivity().finish());
276 
277         mButtonRight = view.findViewById(R.id.button_right);
278         mButtonRight.setText(R.string.wifi_dpp_share_wifi);
279         mButtonRight.setOnClickListener(v -> {
280             mProgressBar.setVisibility(View.VISIBLE);
281             mButtonRight.setVisibility(View.INVISIBLE);
282             startWifiDppConfiguratorInitiator();
283             updateSummary();
284             mTitleSummaryContainer.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
285         });
286 
287         if (savedInstanceState != null) {
288             if (mLatestStatusCode == WifiDppUtils.EASY_CONNECT_EVENT_SUCCESS) {
289                 showSuccessUi(/* isConfigurationChange */ true);
290             } else if (mLatestStatusCode == WifiDppUtils.EASY_CONNECT_EVENT_FAILURE_NONE) {
291                 mProgressBar.setVisibility(isGoingInitiator() ? View.VISIBLE : View.INVISIBLE);
292                 mButtonRight.setVisibility(isGoingInitiator() ? View.INVISIBLE : View.VISIBLE);
293             } else {
294                 showErrorUi(mLatestStatusCode, /* isConfigurationChange */ true);
295             }
296         } else {
297             changeFocusAndAnnounceChange(mButtonRight, mTitleSummaryContainer);
298         }
299     }
300 
301     @Override
onSaveInstanceState(Bundle outState)302     public void onSaveInstanceState(Bundle outState) {
303         outState.putInt(KEY_LATEST_ERROR_CODE, mLatestStatusCode);
304 
305         super.onSaveInstanceState(outState);
306     }
307 
getSsid()308     private String getSsid() {
309         final WifiNetworkConfig wifiNetworkConfig = ((WifiDppConfiguratorActivity) getActivity())
310                 .getWifiNetworkConfig();
311         if (!WifiNetworkConfig.isValidConfig(wifiNetworkConfig)) {
312             throw new IllegalStateException("Invalid Wi-Fi network for configuring");
313         }
314         return wifiNetworkConfig.getSsid();
315     }
316 
startWifiDppConfiguratorInitiator()317     private void startWifiDppConfiguratorInitiator() {
318         final WifiQrCode wifiQrCode = ((WifiDppConfiguratorActivity) getActivity())
319                 .getWifiDppQrCode();
320         final String qrCode = wifiQrCode.getQrCode();
321         final int networkId =
322                 ((WifiDppConfiguratorActivity) getActivity()).getWifiNetworkConfig().getNetworkId();
323         final WifiDppInitiatorViewModel model =
324                 ViewModelProviders.of(this).get(WifiDppInitiatorViewModel.class);
325 
326         model.startEasyConnectAsConfiguratorInitiator(qrCode, networkId);
327     }
328 
329     // Container Activity must implement this interface
330     public interface OnClickChooseDifferentNetworkListener {
onClickChooseDifferentNetwork()331         public void onClickChooseDifferentNetwork();
332     }
333     OnClickChooseDifferentNetworkListener mClickChooseDifferentNetworkListener;
334 
335     @Override
onAttach(Context context)336     public void onAttach(Context context) {
337         super.onAttach(context);
338 
339         mClickChooseDifferentNetworkListener = (OnClickChooseDifferentNetworkListener) context;
340     }
341 
342     @Override
onDetach()343     public void onDetach() {
344         mClickChooseDifferentNetworkListener = null;
345 
346         super.onDetach();
347     }
348 
349     // Check is Easy Connect handshaking or not
isGoingInitiator()350     private boolean isGoingInitiator() {
351         final WifiDppInitiatorViewModel model =
352                 ViewModelProviders.of(this).get(WifiDppInitiatorViewModel.class);
353 
354         return model.isGoingInitiator();
355     }
356 
updateSummary()357     private void updateSummary() {
358         if (isGoingInitiator()) {
359             mSummary.setText(R.string.wifi_dpp_sharing_wifi_with_this_device);
360         } else {
361             mSummary.setText(getString(R.string.wifi_dpp_add_device_to_wifi, getSsid()));
362         }
363     }
364 
365     /**
366      * This fragment will change UI display and text messages for events. To improve Talkback user
367      * experienience, using this method to focus on a right component and announce a changed text
368      * after an UI changing event.
369      *
370      * @param focusView The UI component which will be focused
371      * @param announceView The UI component's text will be talked
372      */
changeFocusAndAnnounceChange(View focusView, View announceView)373     private void changeFocusAndAnnounceChange(View focusView, View announceView) {
374         focusView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
375         announceView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
376     }
377 }
378