• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.server.wifi;
18 
19 import android.app.ActivityManager;
20 import android.app.ActivityOptions;
21 import android.app.AlertDialog;
22 import android.content.BroadcastReceiver;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.net.Uri;
27 import android.net.wifi.WifiContext;
28 import android.net.wifi.WifiManager;
29 import android.os.UserHandle;
30 import android.provider.Browser;
31 import android.text.SpannableString;
32 import android.text.Spanned;
33 import android.text.method.LinkMovementMethod;
34 import android.text.style.URLSpan;
35 import android.util.ArraySet;
36 import android.util.Log;
37 import android.util.SparseArray;
38 import android.view.ContextThemeWrapper;
39 import android.view.Display;
40 import android.view.Gravity;
41 import android.view.View;
42 import android.view.Window;
43 import android.view.WindowInsets;
44 import android.view.WindowManager;
45 import android.widget.TextView;
46 
47 import androidx.annotation.AnyThread;
48 import androidx.annotation.NonNull;
49 import androidx.annotation.Nullable;
50 import androidx.annotation.VisibleForTesting;
51 
52 import com.android.modules.utils.build.SdkLevel;
53 import com.android.wifi.resources.R;
54 
55 import java.util.Set;
56 
57 import javax.annotation.concurrent.ThreadSafe;
58 
59 /**
60  * Class to manage launching dialogs and returning the user reply.
61  * All methods run on the main Wi-Fi thread runner except those annotated with @AnyThread, which can
62  * run on any thread.
63  */
64 public class WifiDialogManager {
65     private static final String TAG = "WifiDialogManager";
66     @VisibleForTesting
67     static final String WIFI_DIALOG_ACTIVITY_CLASSNAME =
68             "com.android.wifi.dialog.WifiDialogActivity";
69 
70     private boolean mVerboseLoggingEnabled;
71 
72     private int mNextDialogId = 0;
73     private final Set<Integer> mActiveDialogIds = new ArraySet<>();
74     private final @NonNull SparseArray<DialogHandleInternal> mActiveDialogHandles =
75             new SparseArray<>();
76     private final @NonNull ArraySet<LegacySimpleDialogHandle> mActiveLegacySimpleDialogs =
77             new ArraySet<>();
78 
79     private final @NonNull WifiContext mContext;
80     private final @NonNull WifiThreadRunner mWifiThreadRunner;
81     private final @NonNull FrameworkFacade mFrameworkFacade;
82 
83     private final BroadcastReceiver mBroadcastReceiver =
84             new BroadcastReceiver() {
85                 @Override
86                 public void onReceive(Context context, Intent intent) {
87                     mWifiThreadRunner.post(
88                             () -> {
89                                 String action = intent.getAction();
90                                 if (mVerboseLoggingEnabled) {
91                                     Log.v(TAG, "Received action: " + action);
92                                 }
93                                 if (Intent.ACTION_USER_PRESENT.equals(action)) {
94                                     // Change all window types to TYPE_KEYGUARD_DIALOG to show the
95                                     // dialogs over the QuickSettings after the screen is unlocked.
96                                     for (LegacySimpleDialogHandle dialogHandle :
97                                             mActiveLegacySimpleDialogs) {
98                                         dialogHandle.changeWindowType(
99                                                 WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
100                                     }
101                                 } else if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) {
102                                     if (intent.getBooleanExtra(
103                                             WifiManager.EXTRA_CLOSE_SYSTEM_DIALOGS_EXCEPT_WIFI,
104                                             false)) {
105                                         return;
106                                     }
107                                     if (mVerboseLoggingEnabled) {
108                                         Log.v(
109                                                 TAG,
110                                                 "ACTION_CLOSE_SYSTEM_DIALOGS received, cancelling"
111                                                         + " all legacy dialogs.");
112                                     }
113                                     for (LegacySimpleDialogHandle dialogHandle :
114                                             mActiveLegacySimpleDialogs) {
115                                         dialogHandle.cancelDialog();
116                                     }
117                                 }
118                             }, TAG + "#onReceive");
119                 }
120             };
121 
122     /**
123      * Constructs a WifiDialogManager
124      *
125      * @param context          Main Wi-Fi context.
126      * @param wifiThreadRunner Main Wi-Fi thread runner.
127      * @param frameworkFacade  FrameworkFacade for launching legacy dialogs.
128      */
WifiDialogManager( @onNull WifiContext context, @NonNull WifiThreadRunner wifiThreadRunner, @NonNull FrameworkFacade frameworkFacade, WifiInjector wifiInjector)129     public WifiDialogManager(
130             @NonNull WifiContext context,
131             @NonNull WifiThreadRunner wifiThreadRunner,
132             @NonNull FrameworkFacade frameworkFacade, WifiInjector wifiInjector) {
133         mContext = context;
134         mWifiThreadRunner = wifiThreadRunner;
135         mFrameworkFacade = frameworkFacade;
136         IntentFilter intentFilter = new IntentFilter();
137         intentFilter.addAction(Intent.ACTION_USER_PRESENT);
138         intentFilter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
139         int flags = 0;
140         if (SdkLevel.isAtLeastT()) {
141             flags = Context.RECEIVER_EXPORTED;
142         }
143         mContext.registerReceiver(mBroadcastReceiver, intentFilter, flags);
144         wifiInjector.getWifiDeviceStateChangeManager()
145                 .registerStateChangeCallback(
146                         new WifiDeviceStateChangeManager.StateChangeCallback() {
147                             @Override
148                             public void onScreenStateChanged(boolean screenOn) {
149                                 handleScreenStateChanged(screenOn);
150                             }
151                         });
152     }
153 
handleScreenStateChanged(boolean screenOn)154     private void handleScreenStateChanged(boolean screenOn) {
155         // Change all window types to TYPE_APPLICATION_OVERLAY to
156         // prevent the dialogs from appearing over the lock screen when
157         // the screen turns on again.
158         if (!screenOn) {
159             if (mVerboseLoggingEnabled) {
160                 Log.d(TAG, "onScreenStateChanged: screen off");
161             }
162             // Change all window types to TYPE_APPLICATION_OVERLAY to
163             // prevent the dialogs from appearing over the lock screen when
164             // the screen turns on again.
165             for (LegacySimpleDialogHandle dialogHandle :
166                     mActiveLegacySimpleDialogs) {
167                 dialogHandle.changeWindowType(
168                         WindowManager.LayoutParams
169                                 .TYPE_APPLICATION_OVERLAY);
170             }
171         }
172     }
173 
174     /**
175      * Enables verbose logging.
176      */
enableVerboseLogging(boolean enabled)177     public void enableVerboseLogging(boolean enabled) {
178         mVerboseLoggingEnabled = enabled;
179     }
180 
getNextDialogId()181     private int getNextDialogId() {
182         if (mActiveDialogIds.isEmpty() || mNextDialogId == WifiManager.INVALID_DIALOG_ID) {
183             mNextDialogId = 0;
184         }
185         return mNextDialogId++;
186     }
187 
getBaseLaunchIntent(@ifiManager.DialogType int dialogType)188     private @Nullable Intent getBaseLaunchIntent(@WifiManager.DialogType int dialogType) {
189         Intent intent = new Intent(WifiManager.ACTION_LAUNCH_DIALOG)
190                 .putExtra(WifiManager.EXTRA_DIALOG_TYPE, dialogType)
191                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
192         String wifiDialogApkPkgName = mContext.getWifiDialogApkPkgName();
193         if (wifiDialogApkPkgName == null) {
194             Log.w(TAG, "Could not get WifiDialog APK package name!");
195             return null;
196         }
197         intent.setClassName(wifiDialogApkPkgName, WIFI_DIALOG_ACTIVITY_CLASSNAME);
198         return intent;
199     }
200 
getDismissIntent(int dialogId)201     private @Nullable Intent getDismissIntent(int dialogId) {
202         Intent intent = new Intent(WifiManager.ACTION_DISMISS_DIALOG);
203         intent.putExtra(WifiManager.EXTRA_DIALOG_ID, dialogId);
204         String wifiDialogApkPkgName = mContext.getWifiDialogApkPkgName();
205         if (wifiDialogApkPkgName == null) {
206             Log.w(TAG, "Could not get WifiDialog APK package name!");
207             return null;
208         }
209         intent.setClassName(wifiDialogApkPkgName, WIFI_DIALOG_ACTIVITY_CLASSNAME);
210         return intent;
211     }
212 
213     /**
214      * Handle for launching and dismissing a dialog from any thread.
215      */
216     @ThreadSafe
217     public class DialogHandle {
218         DialogHandleInternal mInternalHandle;
219         LegacySimpleDialogHandle mLegacyHandle;
220 
DialogHandle(DialogHandleInternal internalHandle)221         private DialogHandle(DialogHandleInternal internalHandle) {
222             mInternalHandle = internalHandle;
223         }
224 
DialogHandle(LegacySimpleDialogHandle legacyHandle)225         private DialogHandle(LegacySimpleDialogHandle legacyHandle) {
226             mLegacyHandle = legacyHandle;
227         }
228 
229         /**
230          * Launches the dialog.
231          */
232         @AnyThread
launchDialog()233         public void launchDialog() {
234             if (mInternalHandle != null) {
235                 mWifiThreadRunner.post(() -> mInternalHandle.launchDialog(),
236                         TAG + "#launchDialog");
237             } else if (mLegacyHandle != null) {
238                 mWifiThreadRunner.post(() -> mLegacyHandle.launchDialog(),
239                         TAG + "#launchDialog");
240             }
241         }
242 
243         /**
244          * Dismisses the dialog. Dialogs will automatically be dismissed once the user replies, but
245          * this method may be used to dismiss unanswered dialogs that are no longer needed.
246          */
247         @AnyThread
dismissDialog()248         public void dismissDialog() {
249             if (mInternalHandle != null) {
250                 mWifiThreadRunner.post(() -> mInternalHandle.dismissDialog(),
251                         TAG + "#dismissDialog");
252             } else if (mLegacyHandle != null) {
253                 mWifiThreadRunner.post(() -> mLegacyHandle.dismissDialog(),
254                         TAG + "#dismissDialog");
255             }
256         }
257     }
258 
259     /**
260      * Internal handle for launching and dismissing a dialog via the WifiDialog app from the main
261      * Wi-Fi thread runner.
262      * @see {@link DialogHandle}
263      */
264     private class DialogHandleInternal {
265         private int mDialogId = WifiManager.INVALID_DIALOG_ID;
266         private @Nullable Intent mIntent;
267         private int mDisplayId = Display.DEFAULT_DISPLAY;
268 
setIntent(@ullable Intent intent)269         void setIntent(@Nullable Intent intent) {
270             mIntent = intent;
271         }
272 
setDisplayId(int displayId)273         void setDisplayId(int displayId) {
274             mDisplayId = displayId;
275         }
276 
277         /**
278          * @see DialogHandle#launchDialog()
279          */
launchDialog()280         void launchDialog() {
281             if (mIntent == null) {
282                 Log.e(TAG, "Cannot launch dialog with null Intent!");
283                 return;
284             }
285             if (mDialogId != WifiManager.INVALID_DIALOG_ID) {
286                 // Dialog is already active, ignore.
287                 return;
288             }
289             registerDialog();
290             mIntent.putExtra(WifiManager.EXTRA_DIALOG_ID, mDialogId);
291             boolean launched = false;
292             // Collapse the QuickSettings since we can't show WifiDialog dialogs over it.
293             mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)
294                     .putExtra(WifiManager.EXTRA_CLOSE_SYSTEM_DIALOGS_EXCEPT_WIFI, true));
295             if (SdkLevel.isAtLeastT() && mDisplayId != Display.DEFAULT_DISPLAY) {
296                 try {
297                     mContext.startActivityAsUser(mIntent,
298                             ActivityOptions.makeBasic().setLaunchDisplayId(mDisplayId).toBundle(),
299                             UserHandle.CURRENT);
300                     launched = true;
301                 } catch (Exception e) {
302                     Log.e(TAG, "Error startActivityAsUser - " + e);
303                 }
304             }
305             if (!launched) {
306                 mContext.startActivityAsUser(mIntent, UserHandle.CURRENT);
307             }
308             if (mVerboseLoggingEnabled) {
309                 Log.v(TAG, "Launching dialog with id=" + mDialogId);
310             }
311         }
312 
313         /**
314          * @see {@link DialogHandle#dismissDialog()}
315          */
dismissDialog()316         void dismissDialog() {
317             if (mDialogId == WifiManager.INVALID_DIALOG_ID) {
318                 // Dialog is not active, ignore.
319                 return;
320             }
321             Intent dismissIntent = getDismissIntent(mDialogId);
322             if (dismissIntent == null) {
323                 Log.e(TAG, "Could not create intent for dismissing dialog with id: "
324                         + mDialogId);
325                 return;
326             }
327             mContext.startActivityAsUser(dismissIntent, UserHandle.CURRENT);
328             if (mVerboseLoggingEnabled) {
329                 Log.v(TAG, "Dismissing dialog with id=" + mDialogId);
330             }
331             unregisterDialog();
332         }
333 
334         /**
335          * Assigns a dialog id to the dialog and registers it as an active dialog.
336          */
registerDialog()337         void registerDialog() {
338             if (mDialogId != WifiManager.INVALID_DIALOG_ID) {
339                 // Already registered.
340                 return;
341             }
342             mDialogId = getNextDialogId();
343             mActiveDialogIds.add(mDialogId);
344             mActiveDialogHandles.put(mDialogId, this);
345             if (mVerboseLoggingEnabled) {
346                 Log.v(TAG, "Registered dialog with id=" + mDialogId
347                         + ". Active dialogs ids: " + mActiveDialogIds);
348             }
349         }
350 
351         /**
352          * Unregisters the dialog as an active dialog and removes its dialog id.
353          * This should be called after a dialog is replied to or dismissed.
354          */
unregisterDialog()355         void unregisterDialog() {
356             if (mDialogId == WifiManager.INVALID_DIALOG_ID) {
357                 // Already unregistered.
358                 return;
359             }
360             mActiveDialogIds.remove(mDialogId);
361             mActiveDialogHandles.remove(mDialogId);
362             if (mVerboseLoggingEnabled) {
363                 Log.v(TAG, "Unregistered dialog with id=" + mDialogId
364                         + ". Active dialogs ids: " + mActiveDialogIds);
365             }
366             mDialogId = WifiManager.INVALID_DIALOG_ID;
367             if (mActiveDialogIds.isEmpty()) {
368                 String wifiDialogApkPkgName = mContext.getWifiDialogApkPkgName();
369                 if (wifiDialogApkPkgName == null) {
370                     Log.wtf(TAG, "Could not get WifiDialog APK package name to force stop!");
371                     return;
372                 }
373                 if (mVerboseLoggingEnabled) {
374                     Log.v(TAG, "Force stopping WifiDialog app");
375                 }
376                 mContext.getSystemService(ActivityManager.class)
377                         .forceStopPackage(wifiDialogApkPkgName);
378             }
379         }
380     }
381 
382     private class SimpleDialogHandle extends DialogHandleInternal {
383         @Nullable private final SimpleDialogCallback mCallback;
384         @Nullable private final WifiThreadRunner mCallbackThreadRunner;
385         private final String mTitle;
386 
SimpleDialogHandle( final String title, final String message, final String messageUrl, final int messageUrlStart, final int messageUrlEnd, final String positiveButtonText, final String negativeButtonText, final String neutralButtonText, @Nullable final SimpleDialogCallback callback, @Nullable final WifiThreadRunner callbackThreadRunner)387         SimpleDialogHandle(
388                 final String title,
389                 final String message,
390                 final String messageUrl,
391                 final int messageUrlStart,
392                 final int messageUrlEnd,
393                 final String positiveButtonText,
394                 final String negativeButtonText,
395                 final String neutralButtonText,
396                 @Nullable final SimpleDialogCallback callback,
397                 @Nullable final WifiThreadRunner callbackThreadRunner) {
398             mTitle = title;
399             Intent intent = getBaseLaunchIntent(WifiManager.DIALOG_TYPE_SIMPLE);
400             if (intent != null) {
401                 intent.putExtra(WifiManager.EXTRA_DIALOG_TITLE, title)
402                         .putExtra(WifiManager.EXTRA_DIALOG_MESSAGE, message)
403                         .putExtra(WifiManager.EXTRA_DIALOG_MESSAGE_URL, messageUrl)
404                         .putExtra(WifiManager.EXTRA_DIALOG_MESSAGE_URL_START, messageUrlStart)
405                         .putExtra(WifiManager.EXTRA_DIALOG_MESSAGE_URL_END, messageUrlEnd)
406                         .putExtra(WifiManager.EXTRA_DIALOG_POSITIVE_BUTTON_TEXT, positiveButtonText)
407                         .putExtra(WifiManager.EXTRA_DIALOG_NEGATIVE_BUTTON_TEXT, negativeButtonText)
408                         .putExtra(WifiManager.EXTRA_DIALOG_NEUTRAL_BUTTON_TEXT, neutralButtonText);
409                 setIntent(intent);
410             }
411             setDisplayId(Display.DEFAULT_DISPLAY);
412             mCallback = callback;
413             mCallbackThreadRunner = callbackThreadRunner;
414         }
415 
notifyOnPositiveButtonClicked()416         void notifyOnPositiveButtonClicked() {
417             if (mCallbackThreadRunner != null && mCallback != null) {
418                 mCallbackThreadRunner.post(mCallback::onPositiveButtonClicked,
419                         mTitle + "#onPositiveButtonClicked");
420             }
421             unregisterDialog();
422         }
423 
notifyOnNegativeButtonClicked()424         void notifyOnNegativeButtonClicked() {
425             if (mCallbackThreadRunner != null && mCallback != null) {
426                 mCallbackThreadRunner.post(mCallback::onNegativeButtonClicked,
427                         mTitle + "#onNegativeButtonClicked");
428             }
429             unregisterDialog();
430         }
431 
notifyOnNeutralButtonClicked()432         void notifyOnNeutralButtonClicked() {
433             if (mCallbackThreadRunner != null && mCallback != null) {
434                 mCallbackThreadRunner.post(mCallback::onNeutralButtonClicked,
435                         mTitle + "#onNeutralButtonClicked");
436             }
437             unregisterDialog();
438         }
439 
notifyOnCancelled()440         void notifyOnCancelled() {
441             if (mCallbackThreadRunner != null && mCallback != null) {
442                 mCallbackThreadRunner.post(mCallback::onCancelled,
443                         mTitle + "#onCancelled");
444             }
445             unregisterDialog();
446         }
447     }
448 
449     /**
450      * Implementation of a simple dialog using AlertDialogs created directly in the system process.
451      */
452     private class LegacySimpleDialogHandle {
453         final String mTitle;
454         final SpannableString mMessage;
455         final String mPositiveButtonText;
456         final String mNegativeButtonText;
457         final String mNeutralButtonText;
458         @Nullable final SimpleDialogCallback mCallback;
459         @Nullable final WifiThreadRunner mCallbackThreadRunner;
460         private AlertDialog mAlertDialog;
461         int mWindowType = WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
462 
LegacySimpleDialogHandle( final String title, final String message, final String messageUrl, final int messageUrlStart, final int messageUrlEnd, final String positiveButtonText, final String negativeButtonText, final String neutralButtonText, @Nullable final SimpleDialogCallback callback, @Nullable final WifiThreadRunner callbackThreadRunner)463         LegacySimpleDialogHandle(
464                 final String title,
465                 final String message,
466                 final String messageUrl,
467                 final int messageUrlStart,
468                 final int messageUrlEnd,
469                 final String positiveButtonText,
470                 final String negativeButtonText,
471                 final String neutralButtonText,
472                 @Nullable final SimpleDialogCallback callback,
473                 @Nullable final WifiThreadRunner callbackThreadRunner) {
474             mTitle = title;
475             if (message != null) {
476                 mMessage = new SpannableString(message);
477                 if (messageUrl != null) {
478                     if (messageUrlStart < 0) {
479                         Log.w(TAG, "Span start cannot be less than 0!");
480                     } else if (messageUrlEnd > message.length()) {
481                         Log.w(TAG, "Span end index " + messageUrlEnd + " cannot be greater than "
482                                 + "message length " + message.length() + "!");
483                     } else if (messageUrlStart > messageUrlEnd) {
484                         Log.w(TAG, "Span start index cannot be greater than end index!");
485                     } else {
486                         mMessage.setSpan(new URLSpan(messageUrl) {
487                             @Override
488                             public void onClick(@NonNull View widget) {
489                                 Context c = widget.getContext();
490                                 Intent openLinkIntent = new Intent(Intent.ACTION_VIEW)
491                                         .setData(Uri.parse(messageUrl))
492                                         .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
493                                         .putExtra(Browser.EXTRA_APPLICATION_ID, c.getPackageName());
494                                 c.startActivityAsUser(openLinkIntent, UserHandle.CURRENT);
495                                 LegacySimpleDialogHandle.this.dismissDialog();
496                             }}, messageUrlStart, messageUrlEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
497                     }
498                 }
499             } else {
500                 mMessage = null;
501             }
502             mPositiveButtonText = positiveButtonText;
503             mNegativeButtonText = negativeButtonText;
504             mNeutralButtonText = neutralButtonText;
505             mCallback = callback;
506             mCallbackThreadRunner = callbackThreadRunner;
507         }
508 
launchDialog()509         void launchDialog() {
510             if (mAlertDialog != null && mAlertDialog.isShowing()) {
511                 // Dialog is already launched. Dismiss and create a new one.
512                 mAlertDialog.setOnDismissListener(null);
513                 mAlertDialog.dismiss();
514             }
515             mAlertDialog = mFrameworkFacade.makeAlertDialogBuilder(
516                     new ContextThemeWrapper(mContext, R.style.wifi_dialog))
517                     .setTitle(mTitle)
518                     .setMessage(mMessage)
519                     .setPositiveButton(mPositiveButtonText, (dialogPositive, which) -> {
520                         if (mVerboseLoggingEnabled) {
521                             Log.v(TAG, "Positive button pressed for legacy simple dialog");
522                         }
523                         if (mCallbackThreadRunner != null && mCallback != null) {
524                             mCallbackThreadRunner.post(mCallback::onPositiveButtonClicked,
525                                     mTitle + "#onPositiveButtonClicked");
526                         }
527                     })
528                     .setNegativeButton(mNegativeButtonText, (dialogNegative, which) -> {
529                         if (mVerboseLoggingEnabled) {
530                             Log.v(TAG, "Negative button pressed for legacy simple dialog");
531                         }
532                         if (mCallbackThreadRunner != null && mCallback != null) {
533                             mCallbackThreadRunner.post(mCallback::onNegativeButtonClicked,
534                                     mTitle + "#onNegativeButtonClicked");
535                         }
536                     })
537                     .setNeutralButton(mNeutralButtonText, (dialogNeutral, which) -> {
538                         if (mVerboseLoggingEnabled) {
539                             Log.v(TAG, "Neutral button pressed for legacy simple dialog");
540                         }
541                         if (mCallbackThreadRunner != null && mCallback != null) {
542                             mCallbackThreadRunner.post(mCallback::onNeutralButtonClicked,
543                                     mTitle + "#onNeutralButtonClicked");
544                         }
545                     })
546                     .setOnCancelListener((dialogCancel) -> {
547                         if (mVerboseLoggingEnabled) {
548                             Log.v(TAG, "Legacy simple dialog cancelled.");
549                         }
550                         if (mCallbackThreadRunner != null && mCallback != null) {
551                             mCallbackThreadRunner.post(mCallback::onCancelled,
552                                     mTitle + "#onCancelled");
553                         }
554                     })
555                     .setOnDismissListener((dialogDismiss) -> {
556                         mWifiThreadRunner.post(() -> {
557                             mAlertDialog = null;
558                             mActiveLegacySimpleDialogs.remove(this);
559                         }, mTitle + "#onDismiss");
560                     })
561                     .create();
562             mAlertDialog.setCanceledOnTouchOutside(mContext.getResources().getBoolean(
563                     R.bool.config_wifiDialogCanceledOnTouchOutside));
564             final Window window = mAlertDialog.getWindow();
565             int gravity = mContext.getResources().getInteger(R.integer.config_wifiDialogGravity);
566             if (gravity != Gravity.NO_GRAVITY) {
567                 window.setGravity(gravity);
568             }
569             final WindowManager.LayoutParams lp = window.getAttributes();
570             window.setType(mWindowType);
571             lp.setFitInsetsTypes(WindowInsets.Type.statusBars()
572                     | WindowInsets.Type.navigationBars());
573             lp.setFitInsetsSides(WindowInsets.Side.all());
574             lp.setFitInsetsIgnoringVisibility(true);
575             window.setAttributes(lp);
576             window.addSystemFlags(
577                     WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS);
578             mAlertDialog.show();
579             TextView messageView = mAlertDialog.findViewById(android.R.id.message);
580             if (messageView != null) {
581                 messageView.setMovementMethod(LinkMovementMethod.getInstance());
582             }
583             mActiveLegacySimpleDialogs.add(this);
584         }
585 
dismissDialog()586         void dismissDialog() {
587             if (mAlertDialog != null) {
588                 mAlertDialog.dismiss();
589             }
590         }
591 
cancelDialog()592         void cancelDialog() {
593             if (mAlertDialog != null) {
594                 mAlertDialog.cancel();
595             }
596         }
597 
changeWindowType(int windowType)598         void changeWindowType(int windowType) {
599             mWindowType = windowType;
600             if (mActiveLegacySimpleDialogs.contains(this)) {
601                 launchDialog();
602             }
603         }
604     }
605 
606     /**
607      * Callback for receiving simple dialog responses.
608      */
609     public interface SimpleDialogCallback {
610         /**
611          * The positive button was clicked.
612          */
onPositiveButtonClicked()613         void onPositiveButtonClicked();
614 
615         /**
616          * The negative button was clicked.
617          */
onNegativeButtonClicked()618         void onNegativeButtonClicked();
619 
620         /**
621          * The neutral button was clicked.
622          */
onNeutralButtonClicked()623         void onNeutralButtonClicked();
624 
625         /**
626          * The dialog was cancelled (back button or home button).
627          */
onCancelled()628         void onCancelled();
629     }
630 
631     /**
632      * Creates a simple dialog with optional title, message, and positive/negative/neutral buttons.
633      *
634      * @param title                Title of the dialog.
635      * @param message              Message of the dialog.
636      * @param positiveButtonText   Text of the positive button or {@code null} for no button.
637      * @param negativeButtonText   Text of the negative button or {@code null} for no button.
638      * @param neutralButtonText    Text of the neutral button or {@code null} for no button.
639      * @param callback             Callback to receive the dialog response.
640      * @param callbackThreadRunner WifiThreadRunner to run the callback on.
641      * @return DialogHandle        Handle for the dialog, or {@code null} if no dialog could
642      *                             be created.
643      */
644     @AnyThread
645     @NonNull
createSimpleDialog( @ullable String title, @Nullable String message, @Nullable String positiveButtonText, @Nullable String negativeButtonText, @Nullable String neutralButtonText, @NonNull SimpleDialogCallback callback, @NonNull WifiThreadRunner callbackThreadRunner)646     public DialogHandle createSimpleDialog(
647             @Nullable String title,
648             @Nullable String message,
649             @Nullable String positiveButtonText,
650             @Nullable String negativeButtonText,
651             @Nullable String neutralButtonText,
652             @NonNull SimpleDialogCallback callback,
653             @NonNull WifiThreadRunner callbackThreadRunner) {
654         return createSimpleDialogWithUrl(
655                 title,
656                 message,
657                 null /* messageUrl */,
658                 0 /* messageUrlStart */,
659                 0 /* messageUrlEnd */,
660                 positiveButtonText,
661                 negativeButtonText,
662                 neutralButtonText,
663                 callback,
664                 callbackThreadRunner);
665     }
666 
667     /**
668      * Creates a simple dialog with a URL embedded in the message.
669      *
670      * @param title                Title of the dialog.
671      * @param message              Message of the dialog.
672      * @param messageUrl           URL to embed in the message. If non-null, then message must also
673      *                             be non-null.
674      * @param messageUrlStart      Start index (inclusive) of the URL in the message. Must be
675      *                             non-negative.
676      * @param messageUrlEnd        End index (exclusive) of the URL in the message. Must be less
677      *                             than the length of message.
678      * @param positiveButtonText   Text of the positive button or {@code null} for no button.
679      * @param negativeButtonText   Text of the negative button or {@code null} for no button.
680      * @param neutralButtonText    Text of the neutral button or {@code null} for no button.
681      * @param callback             Callback to receive the dialog response.
682      * @param callbackThreadRunner WifiThreadRunner to run the callback on.
683      * @return DialogHandle        Handle for the dialog, or {@code null} if no dialog could
684      *                             be created.
685      */
686     @AnyThread
687     @NonNull
createSimpleDialogWithUrl( @ullable String title, @Nullable String message, @Nullable String messageUrl, int messageUrlStart, int messageUrlEnd, @Nullable String positiveButtonText, @Nullable String negativeButtonText, @Nullable String neutralButtonText, @NonNull SimpleDialogCallback callback, @NonNull WifiThreadRunner callbackThreadRunner)688     public DialogHandle createSimpleDialogWithUrl(
689             @Nullable String title,
690             @Nullable String message,
691             @Nullable String messageUrl,
692             int messageUrlStart,
693             int messageUrlEnd,
694             @Nullable String positiveButtonText,
695             @Nullable String negativeButtonText,
696             @Nullable String neutralButtonText,
697             @NonNull SimpleDialogCallback callback,
698             @NonNull WifiThreadRunner callbackThreadRunner) {
699         if (SdkLevel.isAtLeastT()) {
700             return new DialogHandle(
701                     new SimpleDialogHandle(
702                             title,
703                             message,
704                             messageUrl,
705                             messageUrlStart,
706                             messageUrlEnd,
707                             positiveButtonText,
708                             negativeButtonText,
709                             neutralButtonText,
710                             callback,
711                             callbackThreadRunner)
712             );
713         } else {
714             // TODO(b/238353074): Remove this fallback to the legacy implementation once the
715             //                    AlertDialog style on pre-T platform is fixed.
716             return new DialogHandle(
717                     new LegacySimpleDialogHandle(
718                             title,
719                             message,
720                             messageUrl,
721                             messageUrlStart,
722                             messageUrlEnd,
723                             positiveButtonText,
724                             negativeButtonText,
725                             neutralButtonText,
726                             callback,
727                             callbackThreadRunner)
728             );
729         }
730     }
731 
732     /**
733      * Creates a legacy simple dialog on the system process with optional title, message, and
734      * positive/negative/neutral buttons.
735      *
736      * @param title                Title of the dialog.
737      * @param message              Message of the dialog.
738      * @param positiveButtonText   Text of the positive button or {@code null} for no button.
739      * @param negativeButtonText   Text of the negative button or {@code null} for no button.
740      * @param neutralButtonText    Text of the neutral button or {@code null} for no button.
741      * @param callback             Callback to receive the dialog response.
742      * @param callbackThreadRunner WifiThreadRunner to run the callback on.
743      * @return DialogHandle        Handle for the dialog, or {@code null} if no dialog could
744      *                             be created.
745      */
746     @AnyThread
747     @NonNull
createLegacySimpleDialog( @ullable String title, @Nullable String message, @Nullable String positiveButtonText, @Nullable String negativeButtonText, @Nullable String neutralButtonText, @NonNull SimpleDialogCallback callback, @NonNull WifiThreadRunner callbackThreadRunner)748     public DialogHandle createLegacySimpleDialog(
749             @Nullable String title,
750             @Nullable String message,
751             @Nullable String positiveButtonText,
752             @Nullable String negativeButtonText,
753             @Nullable String neutralButtonText,
754             @NonNull SimpleDialogCallback callback,
755             @NonNull WifiThreadRunner callbackThreadRunner) {
756         return createLegacySimpleDialogWithUrl(
757                 title,
758                 message,
759                 null /* messageUrl */,
760                 0 /* messageUrlStart */,
761                 0 /* messageUrlEnd */,
762                 positiveButtonText,
763                 negativeButtonText,
764                 neutralButtonText,
765                 callback,
766                 callbackThreadRunner);
767     }
768 
769     /**
770      * Creates a legacy simple dialog on the system process with a URL embedded in the message.
771      *
772      * @param title                Title of the dialog.
773      * @param message              Message of the dialog.
774      * @param messageUrl           URL to embed in the message. If non-null, then message must also
775      *                             be non-null.
776      * @param messageUrlStart      Start index (inclusive) of the URL in the message. Must be
777      *                             non-negative.
778      * @param messageUrlEnd        End index (exclusive) of the URL in the message. Must be less
779      *                             than the length of message.
780      * @param positiveButtonText   Text of the positive button or {@code null} for no button.
781      * @param negativeButtonText   Text of the negative button or {@code null} for no button.
782      * @param neutralButtonText    Text of the neutral button or {@code null} for no button.
783      * @param callback             Callback to receive the dialog response.
784      * @param callbackThreadRunner WifiThreadRunner to run the callback on.
785      * @return DialogHandle        Handle for the dialog, or {@code null} if no dialog could
786      *                             be created.
787      */
788     @AnyThread
789     @NonNull
createLegacySimpleDialogWithUrl( @ullable String title, @Nullable String message, @Nullable String messageUrl, int messageUrlStart, int messageUrlEnd, @Nullable String positiveButtonText, @Nullable String negativeButtonText, @Nullable String neutralButtonText, @Nullable SimpleDialogCallback callback, @Nullable WifiThreadRunner callbackThreadRunner)790     public DialogHandle createLegacySimpleDialogWithUrl(
791             @Nullable String title,
792             @Nullable String message,
793             @Nullable String messageUrl,
794             int messageUrlStart,
795             int messageUrlEnd,
796             @Nullable String positiveButtonText,
797             @Nullable String negativeButtonText,
798             @Nullable String neutralButtonText,
799             @Nullable SimpleDialogCallback callback,
800             @Nullable WifiThreadRunner callbackThreadRunner) {
801         return new DialogHandle(
802                 new LegacySimpleDialogHandle(
803                         title,
804                         message,
805                         messageUrl,
806                         messageUrlStart,
807                         messageUrlEnd,
808                         positiveButtonText,
809                         negativeButtonText,
810                         neutralButtonText,
811                         callback,
812                         callbackThreadRunner)
813         );
814     }
815 
816     /**
817      * Returns the reply to a simple dialog to the callback of matching dialogId.
818      * @param dialogId id of the replying dialog.
819      * @param reply    reply of the dialog.
820      */
replyToSimpleDialog(int dialogId, @WifiManager.DialogReply int reply)821     public void replyToSimpleDialog(int dialogId, @WifiManager.DialogReply int reply) {
822         if (mVerboseLoggingEnabled) {
823             Log.i(TAG, "Response received for simple dialog. id=" + dialogId + " reply=" + reply);
824         }
825         DialogHandleInternal internalHandle = mActiveDialogHandles.get(dialogId);
826         if (internalHandle == null) {
827             if (mVerboseLoggingEnabled) {
828                 Log.w(TAG, "No matching dialog handle for simple dialog id=" + dialogId);
829             }
830             return;
831         }
832         if (!(internalHandle instanceof SimpleDialogHandle)) {
833             if (mVerboseLoggingEnabled) {
834                 Log.w(TAG, "Dialog handle with id " + dialogId + " is not for a simple dialog.");
835             }
836             return;
837         }
838         switch (reply) {
839             case WifiManager.DIALOG_REPLY_POSITIVE:
840                 ((SimpleDialogHandle) internalHandle).notifyOnPositiveButtonClicked();
841                 break;
842             case WifiManager.DIALOG_REPLY_NEGATIVE:
843                 ((SimpleDialogHandle) internalHandle).notifyOnNegativeButtonClicked();
844                 break;
845             case WifiManager.DIALOG_REPLY_NEUTRAL:
846                 ((SimpleDialogHandle) internalHandle).notifyOnNeutralButtonClicked();
847                 break;
848             case WifiManager.DIALOG_REPLY_CANCELLED:
849                 ((SimpleDialogHandle) internalHandle).notifyOnCancelled();
850                 break;
851             default:
852                 if (mVerboseLoggingEnabled) {
853                     Log.w(TAG, "Received invalid reply=" + reply);
854                 }
855         }
856     }
857 
858     private class P2pInvitationReceivedDialogHandle extends DialogHandleInternal {
859         @Nullable private final P2pInvitationReceivedDialogCallback mCallback;
860         @Nullable private final WifiThreadRunner mCallbackThreadRunner;
861 
P2pInvitationReceivedDialogHandle( final @Nullable String deviceName, final boolean isPinRequested, @Nullable String displayPin, int timeoutMs, int displayId, @Nullable P2pInvitationReceivedDialogCallback callback, @Nullable WifiThreadRunner callbackThreadRunner)862         P2pInvitationReceivedDialogHandle(
863                 final @Nullable String deviceName,
864                 final boolean isPinRequested,
865                 @Nullable String displayPin,
866                 int timeoutMs,
867                 int displayId,
868                 @Nullable P2pInvitationReceivedDialogCallback callback,
869                 @Nullable WifiThreadRunner callbackThreadRunner) {
870             Intent intent = getBaseLaunchIntent(WifiManager.DIALOG_TYPE_P2P_INVITATION_RECEIVED);
871             if (intent != null) {
872                 intent.putExtra(WifiManager.EXTRA_P2P_DEVICE_NAME, deviceName)
873                         .putExtra(WifiManager.EXTRA_P2P_PIN_REQUESTED, isPinRequested)
874                         .putExtra(WifiManager.EXTRA_P2P_DISPLAY_PIN, displayPin)
875                         .putExtra(WifiManager.EXTRA_DIALOG_TIMEOUT_MS, timeoutMs);
876                 setIntent(intent);
877             }
878             setDisplayId(displayId);
879             mCallback = callback;
880             mCallbackThreadRunner = callbackThreadRunner;
881         }
882 
notifyOnAccepted(@ullable String optionalPin)883         void notifyOnAccepted(@Nullable String optionalPin) {
884             if (mCallbackThreadRunner != null && mCallback != null) {
885                 mCallbackThreadRunner.post(() -> mCallback.onAccepted(optionalPin),
886                         "P2pInvitationReceivedDialogHandle" + "#notifyOnAccepted");
887             }
888             unregisterDialog();
889         }
890 
notifyOnDeclined()891         void notifyOnDeclined() {
892             if (mCallbackThreadRunner != null && mCallback != null) {
893                 mCallbackThreadRunner.post(mCallback::onDeclined,
894                         "P2pInvitationReceivedDialogHandle" + "#notifyOnDeclined");
895             }
896             unregisterDialog();
897         }
898     }
899 
900     /**
901      * Callback for receiving P2P Invitation Received dialog responses.
902      */
903     public interface P2pInvitationReceivedDialogCallback {
904         /**
905          * Invitation was accepted.
906          *
907          * @param optionalPin Optional PIN if a PIN was requested, or {@code null} otherwise.
908          */
onAccepted(@ullable String optionalPin)909         void onAccepted(@Nullable String optionalPin);
910 
911         /**
912          * Invitation was declined or cancelled (back button or home button or timeout).
913          */
onDeclined()914         void onDeclined();
915     }
916 
917     /**
918      * Creates a P2P Invitation Received dialog.
919      *
920      * @param deviceName           Name of the device sending the invitation.
921      * @param isPinRequested       True if a PIN was requested and a PIN input UI should be shown.
922      * @param displayPin           Display PIN, or {@code null} if no PIN should be displayed
923      * @param timeoutMs            Timeout for the dialog in milliseconds. 0 indicates no timeout.
924      * @param displayId            The ID of the Display on which to place the dialog
925      *                             (Display.DEFAULT_DISPLAY
926      *                             refers to the default display)
927      * @param callback             Callback to receive the dialog response.
928      * @param callbackThreadRunner WifiThreadRunner to run the callback on.
929      * @return DialogHandle        Handle for the dialog, or {@code null} if no dialog could
930      * be created.
931      */
932     @AnyThread
933     @NonNull
createP2pInvitationReceivedDialog( @ullable String deviceName, boolean isPinRequested, @Nullable String displayPin, int timeoutMs, int displayId, @Nullable P2pInvitationReceivedDialogCallback callback, @Nullable WifiThreadRunner callbackThreadRunner)934     public DialogHandle createP2pInvitationReceivedDialog(
935             @Nullable String deviceName,
936             boolean isPinRequested,
937             @Nullable String displayPin,
938             int timeoutMs,
939             int displayId,
940             @Nullable P2pInvitationReceivedDialogCallback callback,
941             @Nullable WifiThreadRunner callbackThreadRunner) {
942         return new DialogHandle(
943                 new P2pInvitationReceivedDialogHandle(
944                         deviceName,
945                         isPinRequested,
946                         displayPin,
947                         timeoutMs,
948                         displayId,
949                         callback,
950                         callbackThreadRunner)
951         );
952     }
953 
954     /**
955      * Returns the reply to a P2P Invitation Received dialog to the callback of matching dialogId.
956      * Note: Must be invoked only from the main Wi-Fi thread.
957      *
958      * @param dialogId    id of the replying dialog.
959      * @param accepted    Whether the invitation was accepted.
960      * @param optionalPin PIN of the reply, or {@code null} if none was supplied.
961      */
replyToP2pInvitationReceivedDialog( int dialogId, boolean accepted, @Nullable String optionalPin)962     public void replyToP2pInvitationReceivedDialog(
963             int dialogId,
964             boolean accepted,
965             @Nullable String optionalPin) {
966         if (mVerboseLoggingEnabled) {
967             Log.i(TAG, "Response received for P2P Invitation Received dialog."
968                     + " id=" + dialogId
969                     + " accepted=" + accepted
970                     + " pin=" + optionalPin);
971         }
972         DialogHandleInternal internalHandle = mActiveDialogHandles.get(dialogId);
973         if (internalHandle == null) {
974             if (mVerboseLoggingEnabled) {
975                 Log.w(TAG, "No matching dialog handle for P2P Invitation Received dialog"
976                         + " id=" + dialogId);
977             }
978             return;
979         }
980         if (!(internalHandle instanceof P2pInvitationReceivedDialogHandle)) {
981             if (mVerboseLoggingEnabled) {
982                 Log.w(TAG, "Dialog handle with id " + dialogId
983                         + " is not for a P2P Invitation Received dialog.");
984             }
985             return;
986         }
987         if (accepted) {
988             ((P2pInvitationReceivedDialogHandle) internalHandle).notifyOnAccepted(optionalPin);
989         } else {
990             ((P2pInvitationReceivedDialogHandle) internalHandle).notifyOnDeclined();
991         }
992     }
993 
994     private class P2pInvitationSentDialogHandle extends DialogHandleInternal {
P2pInvitationSentDialogHandle( @ullable final String deviceName, @Nullable final String displayPin, int displayId)995         P2pInvitationSentDialogHandle(
996                 @Nullable final String deviceName,
997                 @Nullable final String displayPin,
998                 int displayId) {
999             Intent intent = getBaseLaunchIntent(WifiManager.DIALOG_TYPE_P2P_INVITATION_SENT);
1000             if (intent != null) {
1001                 intent.putExtra(WifiManager.EXTRA_P2P_DEVICE_NAME, deviceName)
1002                         .putExtra(WifiManager.EXTRA_P2P_DISPLAY_PIN, displayPin);
1003                 setIntent(intent);
1004             }
1005             setDisplayId(displayId);
1006         }
1007     }
1008 
1009     /**
1010      * Creates a P2P Invitation Sent dialog.
1011      *
1012      * @param deviceName           Name of the device the invitation was sent to.
1013      * @param displayPin           display PIN
1014      * @param displayId            display ID
1015      * @return DialogHandle        Handle for the dialog, or {@code null} if no dialog could
1016      *                             be created.
1017      */
1018     @AnyThread
1019     @NonNull
createP2pInvitationSentDialog( @ullable String deviceName, @Nullable String displayPin, int displayId)1020     public DialogHandle createP2pInvitationSentDialog(
1021             @Nullable String deviceName,
1022             @Nullable String displayPin,
1023             int displayId) {
1024         return new DialogHandle(new P2pInvitationSentDialogHandle(deviceName, displayPin,
1025                 displayId));
1026     }
1027 }
1028