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(0),
236                         TAG + "#launchDialog");
237             } else if (mLegacyHandle != null) {
238                 mWifiThreadRunner.post(() -> mLegacyHandle.launchDialog(0),
239                         TAG + "#launchDialog");
240             }
241         }
242 
243         /**
244          * Launches the dialog with a timeout before it is auto-cancelled.
245          * @param timeoutMs timeout in milliseconds before the dialog is auto-cancelled. A value <=0
246          *                  indicates no timeout.
247          */
248         @AnyThread
launchDialog(long timeoutMs)249         public void launchDialog(long timeoutMs) {
250             if (mInternalHandle != null) {
251                 mWifiThreadRunner.post(() -> mInternalHandle.launchDialog(timeoutMs),
252                         TAG + "#launchDialogTimeout");
253             } else if (mLegacyHandle != null) {
254                 mWifiThreadRunner.post(() -> mLegacyHandle.launchDialog(timeoutMs),
255                         TAG + "#launchDialogTimeout");
256             }
257         }
258 
259         /**
260          * Dismisses the dialog. Dialogs will automatically be dismissed once the user replies, but
261          * this method may be used to dismiss unanswered dialogs that are no longer needed.
262          */
263         @AnyThread
dismissDialog()264         public void dismissDialog() {
265             if (mInternalHandle != null) {
266                 mWifiThreadRunner.post(() -> mInternalHandle.dismissDialog(),
267                         TAG + "#dismissDialog");
268             } else if (mLegacyHandle != null) {
269                 mWifiThreadRunner.post(() -> mLegacyHandle.dismissDialog(),
270                         TAG + "#dismissDialog");
271             }
272         }
273     }
274 
275     /**
276      * Internal handle for launching and dismissing a dialog via the WifiDialog app from the main
277      * Wi-Fi thread runner.
278      * @see {@link DialogHandle}
279      */
280     private class DialogHandleInternal {
281         private int mDialogId = WifiManager.INVALID_DIALOG_ID;
282         private @Nullable Intent mIntent;
283         private int mDisplayId = Display.DEFAULT_DISPLAY;
284 
setIntent(@ullable Intent intent)285         void setIntent(@Nullable Intent intent) {
286             mIntent = intent;
287         }
288 
setDisplayId(int displayId)289         void setDisplayId(int displayId) {
290             mDisplayId = displayId;
291         }
292 
293         /**
294          * @see {@link DialogHandle#launchDialog(long)}
295          */
launchDialog(long timeoutMs)296         void launchDialog(long timeoutMs) {
297             if (mIntent == null) {
298                 Log.e(TAG, "Cannot launch dialog with null Intent!");
299                 return;
300             }
301             if (mDialogId != WifiManager.INVALID_DIALOG_ID) {
302                 // Dialog is already active, ignore.
303                 return;
304             }
305             registerDialog();
306             mIntent.putExtra(WifiManager.EXTRA_DIALOG_TIMEOUT_MS, timeoutMs);
307             mIntent.putExtra(WifiManager.EXTRA_DIALOG_ID, mDialogId);
308             boolean launched = false;
309             // Collapse the QuickSettings since we can't show WifiDialog dialogs over it.
310             mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)
311                     .putExtra(WifiManager.EXTRA_CLOSE_SYSTEM_DIALOGS_EXCEPT_WIFI, true));
312             if (SdkLevel.isAtLeastT() && mDisplayId != Display.DEFAULT_DISPLAY) {
313                 try {
314                     mContext.startActivityAsUser(mIntent,
315                             ActivityOptions.makeBasic().setLaunchDisplayId(mDisplayId).toBundle(),
316                             UserHandle.CURRENT);
317                     launched = true;
318                 } catch (Exception e) {
319                     Log.e(TAG, "Error startActivityAsUser - " + e);
320                 }
321             }
322             if (!launched) {
323                 mContext.startActivityAsUser(mIntent, UserHandle.CURRENT);
324             }
325             if (mVerboseLoggingEnabled) {
326                 Log.v(TAG, "Launching dialog with id=" + mDialogId);
327             }
328         }
329 
330         /**
331          * @see {@link DialogHandle#dismissDialog()}
332          */
dismissDialog()333         void dismissDialog() {
334             if (mDialogId == WifiManager.INVALID_DIALOG_ID) {
335                 // Dialog is not active, ignore.
336                 return;
337             }
338             Intent dismissIntent = getDismissIntent(mDialogId);
339             if (dismissIntent == null) {
340                 Log.e(TAG, "Could not create intent for dismissing dialog with id: "
341                         + mDialogId);
342                 return;
343             }
344             mContext.startActivityAsUser(dismissIntent, UserHandle.CURRENT);
345             if (mVerboseLoggingEnabled) {
346                 Log.v(TAG, "Dismissing dialog with id=" + mDialogId);
347             }
348             unregisterDialog();
349         }
350 
351         /**
352          * Assigns a dialog id to the dialog and registers it as an active dialog.
353          */
registerDialog()354         void registerDialog() {
355             if (mDialogId != WifiManager.INVALID_DIALOG_ID) {
356                 // Already registered.
357                 return;
358             }
359             mDialogId = getNextDialogId();
360             mActiveDialogIds.add(mDialogId);
361             mActiveDialogHandles.put(mDialogId, this);
362             if (mVerboseLoggingEnabled) {
363                 Log.v(TAG, "Registered dialog with id=" + mDialogId
364                         + ". Active dialogs ids: " + mActiveDialogIds);
365             }
366         }
367 
368         /**
369          * Unregisters the dialog as an active dialog and removes its dialog id.
370          * This should be called after a dialog is replied to or dismissed.
371          */
unregisterDialog()372         void unregisterDialog() {
373             if (mDialogId == WifiManager.INVALID_DIALOG_ID) {
374                 // Already unregistered.
375                 return;
376             }
377             mActiveDialogIds.remove(mDialogId);
378             mActiveDialogHandles.remove(mDialogId);
379             if (mVerboseLoggingEnabled) {
380                 Log.v(TAG, "Unregistered dialog with id=" + mDialogId
381                         + ". Active dialogs ids: " + mActiveDialogIds);
382             }
383             mDialogId = WifiManager.INVALID_DIALOG_ID;
384             if (mActiveDialogIds.isEmpty()) {
385                 String wifiDialogApkPkgName = mContext.getWifiDialogApkPkgName();
386                 if (wifiDialogApkPkgName == null) {
387                     Log.wtf(TAG, "Could not get WifiDialog APK package name to force stop!");
388                     return;
389                 }
390                 if (mVerboseLoggingEnabled) {
391                     Log.v(TAG, "Force stopping WifiDialog app");
392                 }
393                 mContext.getSystemService(ActivityManager.class)
394                         .forceStopPackage(wifiDialogApkPkgName);
395             }
396         }
397     }
398 
399     private class SimpleDialogHandle extends DialogHandleInternal {
400         @Nullable private final SimpleDialogCallback mCallback;
401         @Nullable private final WifiThreadRunner mCallbackThreadRunner;
402         private final String mTitle;
403 
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)404         SimpleDialogHandle(
405                 final String title,
406                 final String message,
407                 final String messageUrl,
408                 final int messageUrlStart,
409                 final int messageUrlEnd,
410                 final String positiveButtonText,
411                 final String negativeButtonText,
412                 final String neutralButtonText,
413                 @Nullable final SimpleDialogCallback callback,
414                 @Nullable final WifiThreadRunner callbackThreadRunner) {
415             mTitle = title;
416             Intent intent = getBaseLaunchIntent(WifiManager.DIALOG_TYPE_SIMPLE);
417             if (intent != null) {
418                 intent.putExtra(WifiManager.EXTRA_DIALOG_TITLE, title)
419                         .putExtra(WifiManager.EXTRA_DIALOG_MESSAGE, message)
420                         .putExtra(WifiManager.EXTRA_DIALOG_MESSAGE_URL, messageUrl)
421                         .putExtra(WifiManager.EXTRA_DIALOG_MESSAGE_URL_START, messageUrlStart)
422                         .putExtra(WifiManager.EXTRA_DIALOG_MESSAGE_URL_END, messageUrlEnd)
423                         .putExtra(WifiManager.EXTRA_DIALOG_POSITIVE_BUTTON_TEXT, positiveButtonText)
424                         .putExtra(WifiManager.EXTRA_DIALOG_NEGATIVE_BUTTON_TEXT, negativeButtonText)
425                         .putExtra(WifiManager.EXTRA_DIALOG_NEUTRAL_BUTTON_TEXT, neutralButtonText);
426                 setIntent(intent);
427             }
428             setDisplayId(Display.DEFAULT_DISPLAY);
429             mCallback = callback;
430             mCallbackThreadRunner = callbackThreadRunner;
431         }
432 
notifyOnPositiveButtonClicked()433         void notifyOnPositiveButtonClicked() {
434             if (mCallbackThreadRunner != null && mCallback != null) {
435                 mCallbackThreadRunner.post(mCallback::onPositiveButtonClicked,
436                         mTitle + "#onPositiveButtonClicked");
437             }
438             unregisterDialog();
439         }
440 
notifyOnNegativeButtonClicked()441         void notifyOnNegativeButtonClicked() {
442             if (mCallbackThreadRunner != null && mCallback != null) {
443                 mCallbackThreadRunner.post(mCallback::onNegativeButtonClicked,
444                         mTitle + "#onNegativeButtonClicked");
445             }
446             unregisterDialog();
447         }
448 
notifyOnNeutralButtonClicked()449         void notifyOnNeutralButtonClicked() {
450             if (mCallbackThreadRunner != null && mCallback != null) {
451                 mCallbackThreadRunner.post(mCallback::onNeutralButtonClicked,
452                         mTitle + "#onNeutralButtonClicked");
453             }
454             unregisterDialog();
455         }
456 
notifyOnCancelled()457         void notifyOnCancelled() {
458             if (mCallbackThreadRunner != null && mCallback != null) {
459                 mCallbackThreadRunner.post(mCallback::onCancelled,
460                         mTitle + "#onCancelled");
461             }
462             unregisterDialog();
463         }
464     }
465 
466     /**
467      * Implementation of a simple dialog using AlertDialogs created directly in the system process.
468      */
469     private class LegacySimpleDialogHandle {
470         final String mTitle;
471         final SpannableString mMessage;
472         final String mPositiveButtonText;
473         final String mNegativeButtonText;
474         final String mNeutralButtonText;
475         @Nullable final SimpleDialogCallback mCallback;
476         @Nullable final WifiThreadRunner mCallbackThreadRunner;
477         private Runnable mTimeoutRunnable;
478         private AlertDialog mAlertDialog;
479         int mWindowType = WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
480         long mTimeoutMs = 0;
481 
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)482         LegacySimpleDialogHandle(
483                 final String title,
484                 final String message,
485                 final String messageUrl,
486                 final int messageUrlStart,
487                 final int messageUrlEnd,
488                 final String positiveButtonText,
489                 final String negativeButtonText,
490                 final String neutralButtonText,
491                 @Nullable final SimpleDialogCallback callback,
492                 @Nullable final WifiThreadRunner callbackThreadRunner) {
493             mTitle = title;
494             if (message != null) {
495                 mMessage = new SpannableString(message);
496                 if (messageUrl != null) {
497                     if (messageUrlStart < 0) {
498                         Log.w(TAG, "Span start cannot be less than 0!");
499                     } else if (messageUrlEnd > message.length()) {
500                         Log.w(TAG, "Span end index " + messageUrlEnd + " cannot be greater than "
501                                 + "message length " + message.length() + "!");
502                     } else if (messageUrlStart > messageUrlEnd) {
503                         Log.w(TAG, "Span start index cannot be greater than end index!");
504                     } else {
505                         mMessage.setSpan(new URLSpan(messageUrl) {
506                             @Override
507                             public void onClick(@NonNull View widget) {
508                                 Context c = widget.getContext();
509                                 Intent openLinkIntent = new Intent(Intent.ACTION_VIEW)
510                                         .setData(Uri.parse(messageUrl))
511                                         .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
512                                         .putExtra(Browser.EXTRA_APPLICATION_ID, c.getPackageName());
513                                 c.startActivityAsUser(openLinkIntent, UserHandle.CURRENT);
514                                 LegacySimpleDialogHandle.this.dismissDialog();
515                             }}, messageUrlStart, messageUrlEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
516                     }
517                 }
518             } else {
519                 mMessage = null;
520             }
521             mPositiveButtonText = positiveButtonText;
522             mNegativeButtonText = negativeButtonText;
523             mNeutralButtonText = neutralButtonText;
524             mCallback = callback;
525             mCallbackThreadRunner = callbackThreadRunner;
526         }
527 
launchDialog(long timeoutMs)528         void launchDialog(long timeoutMs) {
529             if (mAlertDialog != null && mAlertDialog.isShowing()) {
530                 // Dialog is already launched. Dismiss and create a new one.
531                 mAlertDialog.setOnDismissListener(null);
532                 mAlertDialog.dismiss();
533             }
534             if (mTimeoutRunnable != null) {
535                 // Reset the timeout runnable if one has already been created.
536                 mWifiThreadRunner.removeCallbacks(mTimeoutRunnable);
537                 mTimeoutRunnable = null;
538             }
539             mTimeoutMs = timeoutMs;
540             mAlertDialog = mFrameworkFacade.makeAlertDialogBuilder(
541                     new ContextThemeWrapper(mContext, R.style.wifi_dialog))
542                     .setTitle(mTitle)
543                     .setMessage(mMessage)
544                     .setPositiveButton(mPositiveButtonText, (dialogPositive, which) -> {
545                         if (mVerboseLoggingEnabled) {
546                             Log.v(TAG, "Positive button pressed for legacy simple dialog");
547                         }
548                         if (mCallbackThreadRunner != null && mCallback != null) {
549                             mCallbackThreadRunner.post(mCallback::onPositiveButtonClicked,
550                                     mTitle + "#onPositiveButtonClicked");
551                         }
552                     })
553                     .setNegativeButton(mNegativeButtonText, (dialogNegative, which) -> {
554                         if (mVerboseLoggingEnabled) {
555                             Log.v(TAG, "Negative button pressed for legacy simple dialog");
556                         }
557                         if (mCallbackThreadRunner != null && mCallback != null) {
558                             mCallbackThreadRunner.post(mCallback::onNegativeButtonClicked,
559                                     mTitle + "#onNegativeButtonClicked");
560                         }
561                     })
562                     .setNeutralButton(mNeutralButtonText, (dialogNeutral, which) -> {
563                         if (mVerboseLoggingEnabled) {
564                             Log.v(TAG, "Neutral button pressed for legacy simple dialog");
565                         }
566                         if (mCallbackThreadRunner != null && mCallback != null) {
567                             mCallbackThreadRunner.post(mCallback::onNeutralButtonClicked,
568                                     mTitle + "#onNeutralButtonClicked");
569                         }
570                     })
571                     .setOnCancelListener((dialogCancel) -> {
572                         if (mVerboseLoggingEnabled) {
573                             Log.v(TAG, "Legacy simple dialog cancelled.");
574                         }
575                         if (mCallbackThreadRunner != null && mCallback != null) {
576                             mCallbackThreadRunner.post(mCallback::onCancelled,
577                                     mTitle + "#onCancelled");
578                         }
579                     })
580                     .setOnDismissListener((dialogDismiss) -> {
581                         mWifiThreadRunner.post(() -> {
582                             if (mTimeoutRunnable != null) {
583                                 mWifiThreadRunner.removeCallbacks(mTimeoutRunnable);
584                                 mTimeoutRunnable = null;
585                             }
586                             mAlertDialog = null;
587                             mActiveLegacySimpleDialogs.remove(this);
588                         }, mTitle + "#onDismiss");
589                     })
590                     .create();
591             mAlertDialog.setCanceledOnTouchOutside(mContext.getResources().getBoolean(
592                     R.bool.config_wifiDialogCanceledOnTouchOutside));
593             final Window window = mAlertDialog.getWindow();
594             int gravity = mContext.getResources().getInteger(R.integer.config_wifiDialogGravity);
595             if (gravity != Gravity.NO_GRAVITY) {
596                 window.setGravity(gravity);
597             }
598             final WindowManager.LayoutParams lp = window.getAttributes();
599             window.setType(mWindowType);
600             lp.setFitInsetsTypes(WindowInsets.Type.statusBars()
601                     | WindowInsets.Type.navigationBars());
602             lp.setFitInsetsSides(WindowInsets.Side.all());
603             lp.setFitInsetsIgnoringVisibility(true);
604             window.setAttributes(lp);
605             window.addSystemFlags(
606                     WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS);
607             mAlertDialog.show();
608             TextView messageView = mAlertDialog.findViewById(android.R.id.message);
609             if (messageView != null) {
610                 messageView.setMovementMethod(LinkMovementMethod.getInstance());
611             }
612             if (mTimeoutMs > 0) {
613                 mTimeoutRunnable = mAlertDialog::cancel;
614                 mWifiThreadRunner.postDelayed(mTimeoutRunnable, mTimeoutMs,
615                         TAG + "#cancelDialog");
616             }
617             mActiveLegacySimpleDialogs.add(this);
618         }
619 
dismissDialog()620         void dismissDialog() {
621             if (mAlertDialog != null) {
622                 mAlertDialog.dismiss();
623             }
624         }
625 
cancelDialog()626         void cancelDialog() {
627             if (mAlertDialog != null) {
628                 mAlertDialog.cancel();
629             }
630         }
631 
changeWindowType(int windowType)632         void changeWindowType(int windowType) {
633             mWindowType = windowType;
634             if (mActiveLegacySimpleDialogs.contains(this)) {
635                 launchDialog(mTimeoutMs);
636             }
637         }
638     }
639 
640     /**
641      * Callback for receiving simple dialog responses.
642      */
643     public interface SimpleDialogCallback {
644         /**
645          * The positive button was clicked.
646          */
onPositiveButtonClicked()647         void onPositiveButtonClicked();
648 
649         /**
650          * The negative button was clicked.
651          */
onNegativeButtonClicked()652         void onNegativeButtonClicked();
653 
654         /**
655          * The neutral button was clicked.
656          */
onNeutralButtonClicked()657         void onNeutralButtonClicked();
658 
659         /**
660          * The dialog was cancelled (back button or home button or timeout).
661          */
onCancelled()662         void onCancelled();
663     }
664 
665     /**
666      * Creates a simple dialog with optional title, message, and positive/negative/neutral buttons.
667      *
668      * @param title                Title of the dialog.
669      * @param message              Message of the dialog.
670      * @param positiveButtonText   Text of the positive button or {@code null} for no button.
671      * @param negativeButtonText   Text of the negative button or {@code null} for no button.
672      * @param neutralButtonText    Text of the neutral button or {@code null} for no button.
673      * @param callback             Callback to receive the dialog response.
674      * @param callbackThreadRunner WifiThreadRunner to run the callback on.
675      * @return DialogHandle        Handle for the dialog, or {@code null} if no dialog could
676      *                             be created.
677      */
678     @AnyThread
679     @NonNull
createSimpleDialog( @ullable String title, @Nullable String message, @Nullable String positiveButtonText, @Nullable String negativeButtonText, @Nullable String neutralButtonText, @NonNull SimpleDialogCallback callback, @NonNull WifiThreadRunner callbackThreadRunner)680     public DialogHandle createSimpleDialog(
681             @Nullable String title,
682             @Nullable String message,
683             @Nullable String positiveButtonText,
684             @Nullable String negativeButtonText,
685             @Nullable String neutralButtonText,
686             @NonNull SimpleDialogCallback callback,
687             @NonNull WifiThreadRunner callbackThreadRunner) {
688         return createSimpleDialogWithUrl(
689                 title,
690                 message,
691                 null /* messageUrl */,
692                 0 /* messageUrlStart */,
693                 0 /* messageUrlEnd */,
694                 positiveButtonText,
695                 negativeButtonText,
696                 neutralButtonText,
697                 callback,
698                 callbackThreadRunner);
699     }
700 
701     /**
702      * Creates a simple dialog with a URL embedded in the message.
703      *
704      * @param title                Title of the dialog.
705      * @param message              Message of the dialog.
706      * @param messageUrl           URL to embed in the message. If non-null, then message must also
707      *                             be non-null.
708      * @param messageUrlStart      Start index (inclusive) of the URL in the message. Must be
709      *                             non-negative.
710      * @param messageUrlEnd        End index (exclusive) of the URL in the message. Must be less
711      *                             than the length of message.
712      * @param positiveButtonText   Text of the positive button or {@code null} for no button.
713      * @param negativeButtonText   Text of the negative button or {@code null} for no button.
714      * @param neutralButtonText    Text of the neutral button or {@code null} for no button.
715      * @param callback             Callback to receive the dialog response.
716      * @param callbackThreadRunner WifiThreadRunner to run the callback on.
717      * @return DialogHandle        Handle for the dialog, or {@code null} if no dialog could
718      *                             be created.
719      */
720     @AnyThread
721     @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)722     public DialogHandle createSimpleDialogWithUrl(
723             @Nullable String title,
724             @Nullable String message,
725             @Nullable String messageUrl,
726             int messageUrlStart,
727             int messageUrlEnd,
728             @Nullable String positiveButtonText,
729             @Nullable String negativeButtonText,
730             @Nullable String neutralButtonText,
731             @NonNull SimpleDialogCallback callback,
732             @NonNull WifiThreadRunner callbackThreadRunner) {
733         if (SdkLevel.isAtLeastT()) {
734             return new DialogHandle(
735                     new SimpleDialogHandle(
736                             title,
737                             message,
738                             messageUrl,
739                             messageUrlStart,
740                             messageUrlEnd,
741                             positiveButtonText,
742                             negativeButtonText,
743                             neutralButtonText,
744                             callback,
745                             callbackThreadRunner)
746             );
747         } else {
748             // TODO(b/238353074): Remove this fallback to the legacy implementation once the
749             //                    AlertDialog style on pre-T platform is fixed.
750             return new DialogHandle(
751                     new LegacySimpleDialogHandle(
752                             title,
753                             message,
754                             messageUrl,
755                             messageUrlStart,
756                             messageUrlEnd,
757                             positiveButtonText,
758                             negativeButtonText,
759                             neutralButtonText,
760                             callback,
761                             callbackThreadRunner)
762             );
763         }
764     }
765 
766     /**
767      * Creates a legacy simple dialog on the system process with optional title, message, and
768      * positive/negative/neutral buttons.
769      *
770      * @param title                Title of the dialog.
771      * @param message              Message of the dialog.
772      * @param positiveButtonText   Text of the positive button or {@code null} for no button.
773      * @param negativeButtonText   Text of the negative button or {@code null} for no button.
774      * @param neutralButtonText    Text of the neutral button or {@code null} for no button.
775      * @param callback             Callback to receive the dialog response.
776      * @param callbackThreadRunner WifiThreadRunner to run the callback on.
777      * @return DialogHandle        Handle for the dialog, or {@code null} if no dialog could
778      *                             be created.
779      */
780     @AnyThread
781     @NonNull
createLegacySimpleDialog( @ullable String title, @Nullable String message, @Nullable String positiveButtonText, @Nullable String negativeButtonText, @Nullable String neutralButtonText, @NonNull SimpleDialogCallback callback, @NonNull WifiThreadRunner callbackThreadRunner)782     public DialogHandle createLegacySimpleDialog(
783             @Nullable String title,
784             @Nullable String message,
785             @Nullable String positiveButtonText,
786             @Nullable String negativeButtonText,
787             @Nullable String neutralButtonText,
788             @NonNull SimpleDialogCallback callback,
789             @NonNull WifiThreadRunner callbackThreadRunner) {
790         return createLegacySimpleDialogWithUrl(
791                 title,
792                 message,
793                 null /* messageUrl */,
794                 0 /* messageUrlStart */,
795                 0 /* messageUrlEnd */,
796                 positiveButtonText,
797                 negativeButtonText,
798                 neutralButtonText,
799                 callback,
800                 callbackThreadRunner);
801     }
802 
803     /**
804      * Creates a legacy simple dialog on the system process with a URL embedded in the message.
805      *
806      * @param title                Title of the dialog.
807      * @param message              Message of the dialog.
808      * @param messageUrl           URL to embed in the message. If non-null, then message must also
809      *                             be non-null.
810      * @param messageUrlStart      Start index (inclusive) of the URL in the message. Must be
811      *                             non-negative.
812      * @param messageUrlEnd        End index (exclusive) of the URL in the message. Must be less
813      *                             than the length of message.
814      * @param positiveButtonText   Text of the positive button or {@code null} for no button.
815      * @param negativeButtonText   Text of the negative button or {@code null} for no button.
816      * @param neutralButtonText    Text of the neutral button or {@code null} for no button.
817      * @param callback             Callback to receive the dialog response.
818      * @param callbackThreadRunner WifiThreadRunner to run the callback on.
819      * @return DialogHandle        Handle for the dialog, or {@code null} if no dialog could
820      *                             be created.
821      */
822     @AnyThread
823     @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)824     public DialogHandle createLegacySimpleDialogWithUrl(
825             @Nullable String title,
826             @Nullable String message,
827             @Nullable String messageUrl,
828             int messageUrlStart,
829             int messageUrlEnd,
830             @Nullable String positiveButtonText,
831             @Nullable String negativeButtonText,
832             @Nullable String neutralButtonText,
833             @Nullable SimpleDialogCallback callback,
834             @Nullable WifiThreadRunner callbackThreadRunner) {
835         return new DialogHandle(
836                 new LegacySimpleDialogHandle(
837                         title,
838                         message,
839                         messageUrl,
840                         messageUrlStart,
841                         messageUrlEnd,
842                         positiveButtonText,
843                         negativeButtonText,
844                         neutralButtonText,
845                         callback,
846                         callbackThreadRunner)
847         );
848     }
849 
850     /**
851      * Returns the reply to a simple dialog to the callback of matching dialogId.
852      * @param dialogId id of the replying dialog.
853      * @param reply    reply of the dialog.
854      */
replyToSimpleDialog(int dialogId, @WifiManager.DialogReply int reply)855     public void replyToSimpleDialog(int dialogId, @WifiManager.DialogReply int reply) {
856         if (mVerboseLoggingEnabled) {
857             Log.i(TAG, "Response received for simple dialog. id=" + dialogId + " reply=" + reply);
858         }
859         DialogHandleInternal internalHandle = mActiveDialogHandles.get(dialogId);
860         if (internalHandle == null) {
861             if (mVerboseLoggingEnabled) {
862                 Log.w(TAG, "No matching dialog handle for simple dialog id=" + dialogId);
863             }
864             return;
865         }
866         if (!(internalHandle instanceof SimpleDialogHandle)) {
867             if (mVerboseLoggingEnabled) {
868                 Log.w(TAG, "Dialog handle with id " + dialogId + " is not for a simple dialog.");
869             }
870             return;
871         }
872         switch (reply) {
873             case WifiManager.DIALOG_REPLY_POSITIVE:
874                 ((SimpleDialogHandle) internalHandle).notifyOnPositiveButtonClicked();
875                 break;
876             case WifiManager.DIALOG_REPLY_NEGATIVE:
877                 ((SimpleDialogHandle) internalHandle).notifyOnNegativeButtonClicked();
878                 break;
879             case WifiManager.DIALOG_REPLY_NEUTRAL:
880                 ((SimpleDialogHandle) internalHandle).notifyOnNeutralButtonClicked();
881                 break;
882             case WifiManager.DIALOG_REPLY_CANCELLED:
883                 ((SimpleDialogHandle) internalHandle).notifyOnCancelled();
884                 break;
885             default:
886                 if (mVerboseLoggingEnabled) {
887                     Log.w(TAG, "Received invalid reply=" + reply);
888                 }
889         }
890     }
891 
892     private class P2pInvitationReceivedDialogHandle extends DialogHandleInternal {
893         @Nullable private final P2pInvitationReceivedDialogCallback mCallback;
894         @Nullable private final WifiThreadRunner mCallbackThreadRunner;
895 
P2pInvitationReceivedDialogHandle( final @Nullable String deviceName, final boolean isPinRequested, @Nullable String displayPin, int displayId, @Nullable P2pInvitationReceivedDialogCallback callback, @Nullable WifiThreadRunner callbackThreadRunner)896         P2pInvitationReceivedDialogHandle(
897                 final @Nullable String deviceName,
898                 final boolean isPinRequested,
899                 @Nullable String displayPin,
900                 int displayId,
901                 @Nullable P2pInvitationReceivedDialogCallback callback,
902                 @Nullable WifiThreadRunner callbackThreadRunner) {
903             Intent intent = getBaseLaunchIntent(WifiManager.DIALOG_TYPE_P2P_INVITATION_RECEIVED);
904             if (intent != null) {
905                 intent.putExtra(WifiManager.EXTRA_P2P_DEVICE_NAME, deviceName)
906                         .putExtra(WifiManager.EXTRA_P2P_PIN_REQUESTED, isPinRequested)
907                         .putExtra(WifiManager.EXTRA_P2P_DISPLAY_PIN, displayPin);
908                 setIntent(intent);
909             }
910             setDisplayId(displayId);
911             mCallback = callback;
912             mCallbackThreadRunner = callbackThreadRunner;
913         }
914 
notifyOnAccepted(@ullable String optionalPin)915         void notifyOnAccepted(@Nullable String optionalPin) {
916             if (mCallbackThreadRunner != null && mCallback != null) {
917                 mCallbackThreadRunner.post(() -> mCallback.onAccepted(optionalPin),
918                         "P2pInvitationReceivedDialogHandle" + "#notifyOnAccepted");
919             }
920             unregisterDialog();
921         }
922 
notifyOnDeclined()923         void notifyOnDeclined() {
924             if (mCallbackThreadRunner != null && mCallback != null) {
925                 mCallbackThreadRunner.post(mCallback::onDeclined,
926                         "P2pInvitationReceivedDialogHandle" + "#notifyOnDeclined");
927             }
928             unregisterDialog();
929         }
930     }
931 
932     /**
933      * Callback for receiving P2P Invitation Received dialog responses.
934      */
935     public interface P2pInvitationReceivedDialogCallback {
936         /**
937          * Invitation was accepted.
938          *
939          * @param optionalPin Optional PIN if a PIN was requested, or {@code null} otherwise.
940          */
onAccepted(@ullable String optionalPin)941         void onAccepted(@Nullable String optionalPin);
942 
943         /**
944          * Invitation was declined or cancelled (back button or home button or timeout).
945          */
onDeclined()946         void onDeclined();
947     }
948 
949     /**
950      * Creates a P2P Invitation Received dialog.
951      *
952      * @param deviceName           Name of the device sending the invitation.
953      * @param isPinRequested       True if a PIN was requested and a PIN input UI should be shown.
954      * @param displayPin           Display PIN, or {@code null} if no PIN should be displayed
955      * @param displayId            The ID of the Display on which to place the dialog
956      *                             (Display.DEFAULT_DISPLAY
957      *                             refers to the default display)
958      * @param callback             Callback to receive the dialog response.
959      * @param callbackThreadRunner WifiThreadRunner to run the callback on.
960      * @return DialogHandle        Handle for the dialog, or {@code null} if no dialog could
961      *                             be created.
962      */
963     @AnyThread
964     @NonNull
createP2pInvitationReceivedDialog( @ullable String deviceName, boolean isPinRequested, @Nullable String displayPin, int displayId, @Nullable P2pInvitationReceivedDialogCallback callback, @Nullable WifiThreadRunner callbackThreadRunner)965     public DialogHandle createP2pInvitationReceivedDialog(
966             @Nullable String deviceName,
967             boolean isPinRequested,
968             @Nullable String displayPin,
969             int displayId,
970             @Nullable P2pInvitationReceivedDialogCallback callback,
971             @Nullable WifiThreadRunner callbackThreadRunner) {
972         return new DialogHandle(
973                 new P2pInvitationReceivedDialogHandle(
974                         deviceName,
975                         isPinRequested,
976                         displayPin,
977                         displayId,
978                         callback,
979                         callbackThreadRunner)
980         );
981     }
982 
983     /**
984      * Returns the reply to a P2P Invitation Received dialog to the callback of matching dialogId.
985      * Note: Must be invoked only from the main Wi-Fi thread.
986      *
987      * @param dialogId    id of the replying dialog.
988      * @param accepted    Whether the invitation was accepted.
989      * @param optionalPin PIN of the reply, or {@code null} if none was supplied.
990      */
replyToP2pInvitationReceivedDialog( int dialogId, boolean accepted, @Nullable String optionalPin)991     public void replyToP2pInvitationReceivedDialog(
992             int dialogId,
993             boolean accepted,
994             @Nullable String optionalPin) {
995         if (mVerboseLoggingEnabled) {
996             Log.i(TAG, "Response received for P2P Invitation Received dialog."
997                     + " id=" + dialogId
998                     + " accepted=" + accepted
999                     + " pin=" + optionalPin);
1000         }
1001         DialogHandleInternal internalHandle = mActiveDialogHandles.get(dialogId);
1002         if (internalHandle == null) {
1003             if (mVerboseLoggingEnabled) {
1004                 Log.w(TAG, "No matching dialog handle for P2P Invitation Received dialog"
1005                         + " id=" + dialogId);
1006             }
1007             return;
1008         }
1009         if (!(internalHandle instanceof P2pInvitationReceivedDialogHandle)) {
1010             if (mVerboseLoggingEnabled) {
1011                 Log.w(TAG, "Dialog handle with id " + dialogId
1012                         + " is not for a P2P Invitation Received dialog.");
1013             }
1014             return;
1015         }
1016         if (accepted) {
1017             ((P2pInvitationReceivedDialogHandle) internalHandle).notifyOnAccepted(optionalPin);
1018         } else {
1019             ((P2pInvitationReceivedDialogHandle) internalHandle).notifyOnDeclined();
1020         }
1021     }
1022 
1023     private class P2pInvitationSentDialogHandle extends DialogHandleInternal {
P2pInvitationSentDialogHandle( @ullable final String deviceName, @Nullable final String displayPin, int displayId)1024         P2pInvitationSentDialogHandle(
1025                 @Nullable final String deviceName,
1026                 @Nullable final String displayPin,
1027                 int displayId) {
1028             Intent intent = getBaseLaunchIntent(WifiManager.DIALOG_TYPE_P2P_INVITATION_SENT);
1029             if (intent != null) {
1030                 intent.putExtra(WifiManager.EXTRA_P2P_DEVICE_NAME, deviceName)
1031                         .putExtra(WifiManager.EXTRA_P2P_DISPLAY_PIN, displayPin);
1032                 setIntent(intent);
1033             }
1034             setDisplayId(displayId);
1035         }
1036     }
1037 
1038     /**
1039      * Creates a P2P Invitation Sent dialog.
1040      *
1041      * @param deviceName           Name of the device the invitation was sent to.
1042      * @param displayPin           display PIN
1043      * @param displayId            display ID
1044      * @return DialogHandle        Handle for the dialog, or {@code null} if no dialog could
1045      *                             be created.
1046      */
1047     @AnyThread
1048     @NonNull
createP2pInvitationSentDialog( @ullable String deviceName, @Nullable String displayPin, int displayId)1049     public DialogHandle createP2pInvitationSentDialog(
1050             @Nullable String deviceName,
1051             @Nullable String displayPin,
1052             int displayId) {
1053         return new DialogHandle(new P2pInvitationSentDialogHandle(deviceName, displayPin,
1054                 displayId));
1055     }
1056 }
1057