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