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