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