• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 static android.content.res.Resources.ID_NULL;
20 import static android.net.wifi.WifiInfo.sanitizeSsid;
21 
22 import android.app.Activity;
23 import android.app.settings.SettingsEnums;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.graphics.Matrix;
27 import android.graphics.Rect;
28 import android.graphics.SurfaceTexture;
29 import android.net.wifi.EasyConnectStatusCallback;
30 import android.net.wifi.UriParserResults;
31 import android.net.wifi.WifiConfiguration;
32 import android.net.wifi.WifiManager;
33 import android.os.Bundle;
34 import android.os.Handler;
35 import android.os.HandlerThread;
36 import android.os.Looper;
37 import android.os.Message;
38 import android.os.Process;
39 import android.os.SimpleClock;
40 import android.os.SystemClock;
41 import android.text.TextUtils;
42 import android.util.EventLog;
43 import android.util.Log;
44 import android.util.Size;
45 import android.view.LayoutInflater;
46 import android.view.Menu;
47 import android.view.MenuInflater;
48 import android.view.TextureView;
49 import android.view.TextureView.SurfaceTextureListener;
50 import android.view.View;
51 import android.view.ViewGroup;
52 import android.view.accessibility.AccessibilityEvent;
53 import android.widget.TextView;
54 
55 import androidx.annotation.StringRes;
56 import androidx.annotation.UiThread;
57 import androidx.annotation.VisibleForTesting;
58 import androidx.lifecycle.ViewModelProvider;
59 
60 import com.android.settings.R;
61 import com.android.settings.overlay.FeatureFactory;
62 import com.android.settingslib.qrcode.QrCamera;
63 import com.android.settingslib.qrcode.QrDecorateView;
64 import com.android.settingslib.wifi.WifiPermissionChecker;
65 import com.android.wifitrackerlib.WifiEntry;
66 import com.android.wifitrackerlib.WifiPickerTracker;
67 
68 import java.time.Clock;
69 import java.time.ZoneOffset;
70 import java.util.List;
71 
72 public class WifiDppQrCodeScannerFragment extends WifiDppQrCodeBaseFragment implements
73         SurfaceTextureListener,
74         QrCamera.ScannerCallback,
75         WifiManager.ActionListener {
76     private static final String TAG = "WifiDppQrCodeScanner";
77 
78     /** Message sent to hide error message */
79     private static final int MESSAGE_HIDE_ERROR_MESSAGE = 1;
80 
81     /** Message sent to show error message */
82     private static final int MESSAGE_SHOW_ERROR_MESSAGE = 2;
83 
84     /** Message sent to manipulate Wi-Fi DPP QR code */
85     private static final int MESSAGE_SCAN_WIFI_DPP_SUCCESS = 3;
86 
87     /** Message sent to manipulate ZXing Wi-Fi QR code */
88     private static final int MESSAGE_SCAN_ZXING_WIFI_FORMAT_SUCCESS = 4;
89 
90     private static final long SHOW_ERROR_MESSAGE_INTERVAL = 10000;
91     private static final long SHOW_SUCCESS_SQUARE_INTERVAL = 1000;
92 
93     // Key for Bundle usage
94     private static final String KEY_IS_CONFIGURATOR_MODE = "key_is_configurator_mode";
95     private static final String KEY_LATEST_ERROR_CODE = "key_latest_error_code";
96     public static final String KEY_WIFI_CONFIGURATION = "key_wifi_configuration";
97 
98     private static final int ARG_RESTART_CAMERA = 1;
99 
100     // Max age of tracked WifiEntries.
101     private static final long MAX_SCAN_AGE_MILLIS = 15_000;
102     // Interval between initiating WifiPickerTracker scans.
103     private static final long SCAN_INTERVAL_MILLIS = 10_000;
104 
105     private static final @StringRes int REACHABLE_WIFI_NETWORK = ID_NULL;
106 
107     private QrCamera mCamera;
108     private TextureView mTextureView;
109     private QrDecorateView mDecorateView;
110     private TextView mErrorMessage;
111 
112     /** true if the fragment working for configurator, false enrollee*/
113     private boolean mIsConfiguratorMode;
114 
115     /** The SSID of the Wi-Fi network which the user specify to enroll */
116     private String mSsid;
117 
118     /** QR code data scanned by camera */
119     private WifiQrCode mWifiQrCode;
120 
121     /** The WifiConfiguration connecting for enrollee usage */
122     private WifiConfiguration mEnrolleeWifiConfiguration;
123 
124     private int mLatestStatusCode = WifiDppUtils.EASY_CONNECT_EVENT_FAILURE_NONE;
125 
126     private WifiPickerTracker mWifiPickerTracker;
127     private HandlerThread mWorkerThread;
128     private WifiPermissionChecker mWifiPermissionChecker;
129 
130     private final Handler mHandler = new Handler() {
131         @Override
132         public void handleMessage(Message msg) {
133             switch (msg.what) {
134                 case MESSAGE_HIDE_ERROR_MESSAGE:
135                     mErrorMessage.setVisibility(View.INVISIBLE);
136                     break;
137 
138                 case MESSAGE_SHOW_ERROR_MESSAGE:
139                     final String errorMessage = (String) msg.obj;
140 
141                     mErrorMessage.setVisibility(View.VISIBLE);
142                     mErrorMessage.setText(errorMessage);
143                     mErrorMessage.sendAccessibilityEvent(
144                             AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
145 
146                     // Cancel any pending messages to hide error view and requeue the message so
147                     // user has time to see error
148                     removeMessages(MESSAGE_HIDE_ERROR_MESSAGE);
149                     sendEmptyMessageDelayed(MESSAGE_HIDE_ERROR_MESSAGE,
150                             SHOW_ERROR_MESSAGE_INTERVAL);
151 
152                     if (msg.arg1 == ARG_RESTART_CAMERA) {
153                         setProgressBarShown(false);
154                         mDecorateView.setFocused(false);
155                         restartCamera();
156                     }
157                     break;
158 
159                 case MESSAGE_SCAN_WIFI_DPP_SUCCESS:
160                     if (mScanWifiDppSuccessListener == null) {
161                         // mScanWifiDppSuccessListener may be null after onDetach(), do nothing here
162                         return;
163                     }
164                     mScanWifiDppSuccessListener.onScanWifiDppSuccess((WifiQrCode)msg.obj);
165 
166                     if (!mIsConfiguratorMode) {
167                         setProgressBarShown(true);
168                         startWifiDppEnrolleeInitiator((WifiQrCode)msg.obj);
169                         updateEnrolleeSummary();
170                         mSummary.sendAccessibilityEvent(
171                                 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
172                     }
173 
174                     notifyUserForQrCodeRecognition();
175                     break;
176 
177                 case MESSAGE_SCAN_ZXING_WIFI_FORMAT_SUCCESS:
178                     final Context context = getContext();
179                     if (context == null) {
180                         // Context may be null if the message is received after the Activity has
181                         // been destroyed
182                         Log.d(TAG, "Scan success but context is null");
183                         return;
184                     }
185 
186                     // We may get 2 WifiConfiguration if the QR code has no password in it,
187                     // one for open network and one for enhanced open network.
188                     final WifiManager wifiManager =
189                             context.getSystemService(WifiManager.class);
190                     final WifiConfiguration qrCodeWifiConfiguration = (WifiConfiguration) msg.obj;
191 
192                     // Adds all Wi-Fi networks in QR code to the set of configured networks and
193                     // connects to it if it's reachable.
194                     boolean hasHiddenOrReachableWifiNetwork = false;
195                     final int id = wifiManager.addNetwork(qrCodeWifiConfiguration);
196                     if (id == -1) {
197                         return;
198                     }
199 
200                     if (!canConnectWifi(qrCodeWifiConfiguration.SSID)) {
201                         return;
202                     }
203 
204                     wifiManager.enableNetwork(id, /* attemptConnect */ false);
205                     // WifiTracker only contains a hidden SSID Wi-Fi network if it's saved.
206                     // We can't check if a hidden SSID Wi-Fi network is reachable in advance.
207                     @StringRes int wifiReachabilityStringId =
208                             getWifiReachabilityStringId(qrCodeWifiConfiguration);
209                     if (wifiReachabilityStringId == REACHABLE_WIFI_NETWORK) {
210                         hasHiddenOrReachableWifiNetwork = true;
211                         mEnrolleeWifiConfiguration = qrCodeWifiConfiguration;
212                         wifiManager.connect(id,
213                                 /* listener */ WifiDppQrCodeScannerFragment.this);
214                     }
215 
216                     if (!hasHiddenOrReachableWifiNetwork) {
217                         showErrorMessageAndRestartCamera(wifiReachabilityStringId);
218                         return;
219                     }
220 
221                     mMetricsFeatureProvider.action(
222                             mMetricsFeatureProvider.getAttribution(getActivity()),
223                             SettingsEnums.ACTION_SETTINGS_ENROLL_WIFI_QR_CODE,
224                             SettingsEnums.SETTINGS_WIFI_DPP_ENROLLEE,
225                             /* key */ null,
226                             /* value */ Integer.MIN_VALUE);
227 
228                     notifyUserForQrCodeRecognition();
229                     break;
230 
231                 default:
232             }
233         }
234     };
235 
236     @UiThread
notifyUserForQrCodeRecognition()237     private void notifyUserForQrCodeRecognition() {
238         if (mCamera != null) {
239             mCamera.stop();
240         }
241 
242         mDecorateView.setFocused(true);
243         mErrorMessage.setVisibility(View.INVISIBLE);
244 
245         WifiDppUtils.triggerVibrationForQrCodeRecognition(getContext());
246     }
247 
getWifiReachabilityStringId(WifiConfiguration wifiConfiguration)248     private @StringRes int getWifiReachabilityStringId(WifiConfiguration wifiConfiguration) {
249         if (wifiConfiguration.hiddenSSID) {
250             return REACHABLE_WIFI_NETWORK;
251         }
252         final List<WifiEntry> wifiEntries = mWifiPickerTracker.getWifiEntries();
253         final WifiEntry connectedWifiEntry = mWifiPickerTracker.getConnectedWifiEntry();
254         if (connectedWifiEntry != null) {
255             // Add connected WifiEntry to prevent fail toast to users when it's connected.
256             wifiEntries.add(connectedWifiEntry);
257         }
258 
259         boolean canFindNetwork = false;
260         for (WifiEntry wifiEntry : wifiEntries) {
261             if (!TextUtils.equals(wifiEntry.getSsid(), sanitizeSsid(wifiConfiguration.SSID))) {
262                 continue;
263             }
264             canFindNetwork = true;
265             int security = WifiDppUtils.getSecurityTypeFromWifiConfiguration(wifiConfiguration);
266             if (isSecurityMatched(security, wifiEntry.getSecurity())) {
267                 Log.d(TAG, "WiFi DPP detects connection security for a matching WiFi network.");
268                 return REACHABLE_WIFI_NETWORK;
269             }
270         }
271         if (canFindNetwork) {
272             Log.e(TAG, "WiFi DPP check connection no matched security");
273             return R.string.wifi_dpp_check_connection_no_matched_security;
274         }
275         Log.e(TAG, "WiFi DPP check connection no matched SSID");
276         return R.string.wifi_dpp_check_connection_no_matched_ssid;
277     }
278 
279     @VisibleForTesting
isSecurityMatched(int qrSecurity, int entrySecurity)280     boolean isSecurityMatched(int qrSecurity, int entrySecurity) {
281         if (qrSecurity == entrySecurity) {
282             return true;
283         }
284         // Default security type of PSK/SAE transition mode WifiEntry is SECURITY_PSK and
285         // there is no way to know if a WifiEntry is of transition mode. Give it a chance.
286         if (qrSecurity == WifiEntry.SECURITY_SAE && entrySecurity == WifiEntry.SECURITY_PSK) {
287             return true;
288         }
289         // If configured is no password, the Wi-Fi framework will attempt OPEN and OWE security.
290         return isNoPasswordSecurity(qrSecurity) && isNoPasswordSecurity(entrySecurity);
291     }
292 
isNoPasswordSecurity(int security)293     private boolean isNoPasswordSecurity(int security) {
294         return security == WifiEntry.SECURITY_NONE || security == WifiEntry.SECURITY_OWE;
295     }
296 
297     @VisibleForTesting
canConnectWifi(String ssid)298     boolean canConnectWifi(String ssid) {
299         final List<WifiEntry> wifiEntries = mWifiPickerTracker.getWifiEntries();
300         for (WifiEntry wifiEntry : wifiEntries) {
301             if (!TextUtils.equals(wifiEntry.getSsid(), sanitizeSsid(ssid))) continue;
302 
303             if (!wifiEntry.canConnect()) {
304                 Log.w(TAG, "Wi-Fi is not allowed to connect by your organization. SSID:" + ssid);
305                 showErrorMessageAndRestartCamera(R.string.not_allowed_by_ent);
306                 return false;
307             }
308         }
309         return true;
310     }
311 
312     @Override
onCreate(Bundle savedInstanceState)313     public void onCreate(Bundle savedInstanceState) {
314         super.onCreate(savedInstanceState);
315 
316         if (savedInstanceState != null) {
317             mIsConfiguratorMode = savedInstanceState.getBoolean(KEY_IS_CONFIGURATOR_MODE);
318             mLatestStatusCode = savedInstanceState.getInt(KEY_LATEST_ERROR_CODE);
319             mEnrolleeWifiConfiguration = savedInstanceState.getParcelable(KEY_WIFI_CONFIGURATION);
320         }
321 
322         final WifiDppInitiatorViewModel model =
323                 new ViewModelProvider(this).get(WifiDppInitiatorViewModel.class);
324 
325         model.getEnrolleeSuccessNetworkId().observe(this, networkId -> {
326             // After configuration change, observe callback will be triggered,
327             // do nothing for this case if a handshake does not end
328             if (model.isWifiDppHandshaking()) {
329                 return;
330             }
331 
332             new EasyConnectEnrolleeStatusCallback().onEnrolleeSuccess(networkId.intValue());
333         });
334 
335         model.getStatusCode().observe(this, statusCode -> {
336             // After configuration change, observe callback will be triggered,
337             // do nothing for this case if a handshake does not end
338             if (model.isWifiDppHandshaking()) {
339                 return;
340             }
341 
342             int code = statusCode.intValue();
343             Log.d(TAG, "Easy connect enrollee callback onFailure " + code);
344             new EasyConnectEnrolleeStatusCallback().onFailure(code);
345         });
346     }
347 
348     @Override
onPause()349     public void onPause() {
350         if (mCamera != null) {
351             mCamera.stop();
352         }
353 
354         super.onPause();
355     }
356 
357     @Override
onResume()358     public void onResume() {
359         super.onResume();
360 
361         if (!isWifiDppHandshaking()) {
362             restartCamera();
363         }
364     }
365 
366     @Override
getMetricsCategory()367     public int getMetricsCategory() {
368         if (mIsConfiguratorMode) {
369             return SettingsEnums.SETTINGS_WIFI_DPP_CONFIGURATOR;
370         } else {
371             return SettingsEnums.SETTINGS_WIFI_DPP_ENROLLEE;
372         }
373     }
374 
375     // Container Activity must implement this interface
376     public interface OnScanWifiDppSuccessListener {
onScanWifiDppSuccess(WifiQrCode wifiQrCode)377         void onScanWifiDppSuccess(WifiQrCode wifiQrCode);
378     }
379     private OnScanWifiDppSuccessListener mScanWifiDppSuccessListener;
380 
381     /**
382      * Configurator container activity of the fragment should create instance with this constructor.
383      */
WifiDppQrCodeScannerFragment()384     public WifiDppQrCodeScannerFragment() {
385         super();
386 
387         mIsConfiguratorMode = true;
388     }
389 
WifiDppQrCodeScannerFragment(WifiPickerTracker wifiPickerTracker, WifiPermissionChecker wifiPermissionChecker)390     public WifiDppQrCodeScannerFragment(WifiPickerTracker wifiPickerTracker,
391             WifiPermissionChecker wifiPermissionChecker) {
392         super();
393 
394         mIsConfiguratorMode = true;
395         mWifiPickerTracker = wifiPickerTracker;
396         mWifiPermissionChecker = wifiPermissionChecker;
397     }
398 
399     /**
400      * Enrollee container activity of the fragment should create instance with this constructor and
401      * specify the SSID string of the WI-Fi network to be provisioned.
402      */
WifiDppQrCodeScannerFragment(String ssid)403     WifiDppQrCodeScannerFragment(String ssid) {
404         super();
405 
406         mIsConfiguratorMode = false;
407         mSsid = ssid;
408     }
409 
410     @Override
onActivityCreated(Bundle savedInstanceState)411     public void onActivityCreated(Bundle savedInstanceState) {
412         super.onActivityCreated(savedInstanceState);
413 
414         mWorkerThread = new HandlerThread(
415                 TAG + "{" + Integer.toHexString(System.identityHashCode(this)) + "}",
416                 Process.THREAD_PRIORITY_BACKGROUND);
417         mWorkerThread.start();
418         final Clock elapsedRealtimeClock = new SimpleClock(ZoneOffset.UTC) {
419             @Override
420             public long millis() {
421                 return SystemClock.elapsedRealtime();
422             }
423         };
424         final Context context = getContext();
425         mWifiPickerTracker = FeatureFactory.getFeatureFactory()
426                 .getWifiTrackerLibProvider()
427                 .createWifiPickerTracker(getSettingsLifecycle(), context,
428                         new Handler(Looper.getMainLooper()),
429                         mWorkerThread.getThreadHandler(),
430                         elapsedRealtimeClock,
431                         MAX_SCAN_AGE_MILLIS,
432                         SCAN_INTERVAL_MILLIS,
433                         null /* listener */);
434 
435         // setTitle for TalkBack
436         if (mIsConfiguratorMode) {
437             getActivity().setTitle(R.string.wifi_dpp_add_device_to_network);
438         } else {
439             getActivity().setTitle(R.string.wifi_dpp_scan_qr_code);
440         }
441     }
442 
443     @Override
onAttach(Context context)444     public void onAttach(Context context) {
445         super.onAttach(context);
446 
447         mScanWifiDppSuccessListener = (OnScanWifiDppSuccessListener) context;
448     }
449 
450     @Override
onDetach()451     public void onDetach() {
452         mScanWifiDppSuccessListener = null;
453 
454         super.onDetach();
455     }
456 
457     @Override
onDestroyView()458     public void onDestroyView() {
459         mWorkerThread.quit();
460 
461         super.onDestroyView();
462     }
463 
464     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)465     public final View onCreateView(LayoutInflater inflater, ViewGroup container,
466             Bundle savedInstanceState) {
467         return inflater.inflate(R.layout.wifi_dpp_qrcode_scanner_fragment, container,
468                 /* attachToRoot */ false);
469     }
470 
471     @Override
onViewCreated(View view, Bundle savedInstanceState)472     public void onViewCreated(View view, Bundle savedInstanceState) {
473         super.onViewCreated(view, savedInstanceState);
474         mSummary = view.findViewById(R.id.sud_layout_subtitle);
475 
476         mTextureView = view.findViewById(R.id.preview_view);
477         mTextureView.setSurfaceTextureListener(this);
478 
479         mDecorateView = view.findViewById(R.id.decorate_view);
480 
481         setProgressBarShown(isWifiDppHandshaking());
482 
483         if (mIsConfiguratorMode) {
484             setHeaderTitle(R.string.wifi_dpp_add_device_to_network);
485 
486             WifiNetworkConfig wifiNetworkConfig = ((WifiNetworkConfig.Retriever) getActivity())
487                 .getWifiNetworkConfig();
488             if (!WifiNetworkConfig.isValidConfig(wifiNetworkConfig)) {
489                 throw new IllegalStateException("Invalid Wi-Fi network for configuring");
490             }
491             mSummary.setText(getString(R.string.wifi_dpp_center_qr_code,
492                     wifiNetworkConfig.getSsid()));
493         } else {
494             setHeaderTitle(R.string.wifi_dpp_scan_qr_code);
495 
496             updateEnrolleeSummary();
497         }
498 
499         mErrorMessage = view.findViewById(R.id.error_message);
500     }
501 
502     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)503     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
504         menu.removeItem(Menu.FIRST);
505 
506         super.onCreateOptionsMenu(menu, inflater);
507     }
508 
509     @Override
onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height)510     public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
511         initCamera(surface);
512     }
513 
514     @Override
onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height)515     public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
516         // Do nothing
517     }
518 
519     @Override
onSurfaceTextureDestroyed(SurfaceTexture surface)520     public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
521         destroyCamera();
522         return true;
523     }
524 
525     @Override
onSurfaceTextureUpdated(SurfaceTexture surface)526     public void onSurfaceTextureUpdated(SurfaceTexture surface) {
527         // Do nothing
528     }
529 
530     @Override
getViewSize()531     public Size getViewSize() {
532         return new Size(mTextureView.getWidth(), mTextureView.getHeight());
533     }
534 
535     @Override
getFramePosition(Size previewSize, int cameraOrientation)536     public Rect getFramePosition(Size previewSize, int cameraOrientation) {
537         return new Rect(0, 0, previewSize.getHeight(), previewSize.getHeight());
538     }
539 
540     @Override
setTransform(Matrix transform)541     public void setTransform(Matrix transform) {
542         mTextureView.setTransform(transform);
543     }
544 
545     @Override
isValid(String qrCode)546     public boolean isValid(String qrCode) {
547         try {
548             mWifiQrCode = new WifiQrCode(qrCode);
549         } catch (IllegalArgumentException e) {
550             showErrorMessage(R.string.wifi_dpp_qr_code_is_not_valid_format);
551             return false;
552         }
553 
554         // It's impossible to provision other device with ZXing Wi-Fi Network config format
555         if (mIsConfiguratorMode
556                 && mWifiQrCode.getScheme()
557                         == UriParserResults.URI_SCHEME_ZXING_WIFI_NETWORK_CONFIG) {
558             showErrorMessage(R.string.wifi_dpp_qr_code_is_not_valid_format);
559             return false;
560         }
561 
562         return true;
563     }
564 
565     /**
566      * This method is only called when QrCamera.ScannerCallback.isValid returns true;
567      */
568     @Override
handleSuccessfulResult(String qrCode)569     public void handleSuccessfulResult(String qrCode) {
570         switch (mWifiQrCode.getScheme()) {
571             case UriParserResults.URI_SCHEME_DPP:
572                 handleWifiDpp();
573                 break;
574 
575             case UriParserResults.URI_SCHEME_ZXING_WIFI_NETWORK_CONFIG:
576                 handleZxingWifiFormat();
577                 break;
578 
579             default:
580                 // continue below
581         }
582     }
583 
handleWifiDpp()584     private void handleWifiDpp() {
585         Message message = mHandler.obtainMessage(MESSAGE_SCAN_WIFI_DPP_SUCCESS);
586         message.obj = new WifiQrCode(mWifiQrCode.getQrCode());
587 
588         mHandler.sendMessageDelayed(message, SHOW_SUCCESS_SQUARE_INTERVAL);
589     }
590 
handleZxingWifiFormat()591     private void handleZxingWifiFormat() {
592         Message message = mHandler.obtainMessage(MESSAGE_SCAN_ZXING_WIFI_FORMAT_SUCCESS);
593         message.obj = new WifiQrCode(mWifiQrCode.getQrCode()).getWifiConfiguration();
594 
595         mHandler.sendMessageDelayed(message, SHOW_SUCCESS_SQUARE_INTERVAL);
596     }
597 
598     @Override
handleCameraFailure()599     public void handleCameraFailure() {
600         destroyCamera();
601     }
602 
initCamera(SurfaceTexture surface)603     private void initCamera(SurfaceTexture surface) {
604         // Check if the camera has already created.
605         if (mCamera == null) {
606             mCamera = new QrCamera(getContext(), this);
607 
608             if (isWifiDppHandshaking()) {
609                 if (mDecorateView != null) {
610                     mDecorateView.setFocused(true);
611                 }
612             } else {
613                 mCamera.start(surface);
614             }
615         }
616     }
617 
destroyCamera()618     private void destroyCamera() {
619         if (mCamera != null) {
620             mCamera.stop();
621             mCamera = null;
622         }
623     }
624 
showErrorMessage(@tringRes int messageResId)625     private void showErrorMessage(@StringRes int messageResId) {
626         final Message message = mHandler.obtainMessage(MESSAGE_SHOW_ERROR_MESSAGE,
627                 getString(messageResId));
628         message.sendToTarget();
629     }
630 
631     @VisibleForTesting
showErrorMessageAndRestartCamera(@tringRes int messageResId)632     void showErrorMessageAndRestartCamera(@StringRes int messageResId) {
633         final Message message = mHandler.obtainMessage(MESSAGE_SHOW_ERROR_MESSAGE,
634                 getString(messageResId));
635         message.arg1 = ARG_RESTART_CAMERA;
636         message.sendToTarget();
637     }
638 
639     @Override
onSaveInstanceState(Bundle outState)640     public void onSaveInstanceState(Bundle outState) {
641         outState.putBoolean(KEY_IS_CONFIGURATOR_MODE, mIsConfiguratorMode);
642         outState.putInt(KEY_LATEST_ERROR_CODE, mLatestStatusCode);
643         outState.putParcelable(KEY_WIFI_CONFIGURATION, mEnrolleeWifiConfiguration);
644 
645         super.onSaveInstanceState(outState);
646     }
647 
648     private class EasyConnectEnrolleeStatusCallback extends EasyConnectStatusCallback {
649         @Override
onEnrolleeSuccess(int newNetworkId)650         public void onEnrolleeSuccess(int newNetworkId) {
651 
652             // Connect to the new network.
653             final WifiManager wifiManager = getContext().getSystemService(WifiManager.class);
654             final List<WifiConfiguration> wifiConfigs =
655                     wifiManager.getPrivilegedConfiguredNetworks();
656             for (WifiConfiguration wifiConfig : wifiConfigs) {
657                 if (wifiConfig.networkId == newNetworkId) {
658                     mLatestStatusCode = WifiDppUtils.EASY_CONNECT_EVENT_SUCCESS;
659                     mEnrolleeWifiConfiguration = wifiConfig;
660                     if (!canConnectWifi(wifiConfig.SSID)) return;
661                     wifiManager.connect(wifiConfig, WifiDppQrCodeScannerFragment.this);
662                     return;
663                 }
664             }
665 
666             Log.e(TAG, "Invalid networkId " + newNetworkId);
667             mLatestStatusCode = EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_GENERIC;
668             updateEnrolleeSummary();
669             showErrorMessageAndRestartCamera(R.string.wifi_dpp_check_connection_try_again);
670         }
671 
672         @Override
onConfiguratorSuccess(int code)673         public void onConfiguratorSuccess(int code) {
674             // Do nothing
675         }
676 
677         @Override
onFailure(int code)678         public void onFailure(int code) {
679             Log.d(TAG, "EasyConnectEnrolleeStatusCallback.onFailure " + code);
680 
681             int errorMessageResId;
682             switch (code) {
683                 case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_INVALID_URI:
684                     errorMessageResId = R.string.wifi_dpp_qr_code_is_not_valid_format;
685                     break;
686 
687                 case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_AUTHENTICATION:
688                     errorMessageResId = R.string.wifi_dpp_failure_authentication_or_configuration;
689                     break;
690 
691                 case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_NOT_COMPATIBLE:
692                     errorMessageResId = R.string.wifi_dpp_failure_not_compatible;
693                     break;
694 
695                 case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_CONFIGURATION:
696                     errorMessageResId = R.string.wifi_dpp_failure_authentication_or_configuration;
697                     break;
698 
699                 case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_BUSY:
700                     if (code == mLatestStatusCode) {
701                         throw(new IllegalStateException("stopEasyConnectSession and try again for"
702                                 + "EASY_CONNECT_EVENT_FAILURE_BUSY but still failed"));
703                     }
704 
705                     mLatestStatusCode = code;
706                     final WifiManager wifiManager =
707                         getContext().getSystemService(WifiManager.class);
708                     wifiManager.stopEasyConnectSession();
709                     startWifiDppEnrolleeInitiator(mWifiQrCode);
710                     return;
711 
712                 case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_TIMEOUT:
713                     errorMessageResId = R.string.wifi_dpp_failure_timeout;
714                     break;
715 
716                 case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_GENERIC:
717                     errorMessageResId = R.string.wifi_dpp_failure_generic;
718                     break;
719 
720                 case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_NOT_SUPPORTED:
721                     throw(new IllegalStateException("EASY_CONNECT_EVENT_FAILURE_NOT_SUPPORTED" +
722                             " should be a configurator only error"));
723 
724                 case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_INVALID_NETWORK:
725                     throw(new IllegalStateException("EASY_CONNECT_EVENT_FAILURE_INVALID_NETWORK" +
726                             " should be a configurator only error"));
727 
728                 default:
729                     throw(new IllegalStateException("Unexpected Wi-Fi DPP error"));
730             }
731 
732             mLatestStatusCode = code;
733             updateEnrolleeSummary();
734             showErrorMessageAndRestartCamera(errorMessageResId);
735         }
736 
737         @Override
onProgress(int code)738         public void onProgress(int code) {
739             // Do nothing
740         }
741     }
742 
startWifiDppEnrolleeInitiator(WifiQrCode wifiQrCode)743     private void startWifiDppEnrolleeInitiator(WifiQrCode wifiQrCode) {
744         final WifiDppInitiatorViewModel model =
745                 new ViewModelProvider(this).get(WifiDppInitiatorViewModel.class);
746 
747         model.startEasyConnectAsEnrolleeInitiator(wifiQrCode.getQrCode());
748     }
749 
750     @Override
onSuccess()751     public void onSuccess() {
752         final Intent resultIntent = new Intent();
753         resultIntent.putExtra(KEY_WIFI_CONFIGURATION, mEnrolleeWifiConfiguration);
754 
755         final Activity hostActivity = getActivity();
756         if (hostActivity == null) return;
757         if (mWifiPermissionChecker == null) {
758             mWifiPermissionChecker = new WifiPermissionChecker(hostActivity);
759         }
760 
761         if (!mWifiPermissionChecker.canAccessWifiState()) {
762             Log.w(TAG, "Calling package does not have ACCESS_WIFI_STATE permission for result.");
763             EventLog.writeEvent(0x534e4554, "187176859",
764                     mWifiPermissionChecker.getLaunchedPackage(), "no ACCESS_WIFI_STATE permission");
765             hostActivity.finish();
766             return;
767         }
768 
769         if (!mWifiPermissionChecker.canAccessFineLocation()) {
770             Log.w(TAG, "Calling package does not have ACCESS_FINE_LOCATION permission for result.");
771             EventLog.writeEvent(0x534e4554, "187176859",
772                     mWifiPermissionChecker.getLaunchedPackage(),
773                     "no ACCESS_FINE_LOCATION permission");
774             hostActivity.finish();
775             return;
776         }
777 
778         hostActivity.setResult(Activity.RESULT_OK, resultIntent);
779         hostActivity.finish();
780     }
781 
782     @Override
onFailure(int reason)783     public void onFailure(int reason) {
784         Log.d(TAG, "Wi-Fi connect onFailure reason - " + reason);
785         showErrorMessageAndRestartCamera(R.string.wifi_dpp_check_connection_try_again);
786     }
787 
788     // Check is Easy Connect handshaking or not
isWifiDppHandshaking()789     private boolean isWifiDppHandshaking() {
790         final WifiDppInitiatorViewModel model =
791                 new ViewModelProvider(this).get(WifiDppInitiatorViewModel.class);
792 
793         return model.isWifiDppHandshaking();
794     }
795 
796     /**
797      * To resume camera decoding task after handshake fail or Wi-Fi connection fail.
798      */
restartCamera()799     private void restartCamera() {
800         if (mCamera == null) {
801             Log.d(TAG, "mCamera is not available for restarting camera");
802             return;
803         }
804 
805         if (mCamera.isDecodeTaskAlive()) {
806             mCamera.stop();
807         }
808 
809         final SurfaceTexture surfaceTexture = mTextureView.getSurfaceTexture();
810         if (surfaceTexture == null) {
811             throw new IllegalStateException("SurfaceTexture is not ready for restarting camera");
812         }
813 
814         mCamera.start(surfaceTexture);
815     }
816 
updateEnrolleeSummary()817     private void updateEnrolleeSummary() {
818         if (isWifiDppHandshaking()) {
819             mSummary.setText(R.string.wifi_dpp_connecting);
820         } else {
821             String description;
822             if (TextUtils.isEmpty(mSsid)) {
823                 description = getString(R.string.wifi_dpp_scan_qr_code_join_unknown_network, mSsid);
824             } else {
825                 description = getString(R.string.wifi_dpp_scan_qr_code_join_network, mSsid);
826             }
827             mSummary.setText(description);
828         }
829     }
830 
831     @VisibleForTesting
isDecodeTaskAlive()832     protected boolean isDecodeTaskAlive() {
833         return mCamera != null && mCamera.isDecodeTaskAlive();
834     }
835 
836     @Override
isFooterAvailable()837     protected boolean isFooterAvailable() {
838         return false;
839     }
840 }
841