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