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.wifi.dialog; 18 19 import android.app.Activity; 20 import android.app.AlertDialog; 21 import android.app.Dialog; 22 import android.content.BroadcastReceiver; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.IntentFilter; 26 import android.content.res.Configuration; 27 import android.icu.text.MessageFormat; 28 import android.media.AudioManager; 29 import android.media.Ringtone; 30 import android.media.RingtoneManager; 31 import android.net.Uri; 32 import android.net.wifi.WifiContext; 33 import android.net.wifi.WifiManager; 34 import android.os.Bundle; 35 import android.os.CountDownTimer; 36 import android.os.Handler; 37 import android.os.Looper; 38 import android.os.SystemClock; 39 import android.os.Vibrator; 40 import android.text.Editable; 41 import android.text.SpannableString; 42 import android.text.Spanned; 43 import android.text.TextUtils; 44 import android.text.TextWatcher; 45 import android.text.method.LinkMovementMethod; 46 import android.text.style.URLSpan; 47 import android.util.ArraySet; 48 import android.util.Log; 49 import android.util.SparseArray; 50 import android.view.ContextThemeWrapper; 51 import android.view.Gravity; 52 import android.view.KeyEvent; 53 import android.view.LayoutInflater; 54 import android.view.View; 55 import android.view.ViewGroup; 56 import android.view.Window; 57 import android.view.WindowManager; 58 import android.widget.EditText; 59 import android.widget.TextView; 60 61 import androidx.annotation.NonNull; 62 import androidx.annotation.Nullable; 63 import androidx.core.os.BuildCompat; 64 65 import java.util.ArrayList; 66 import java.util.List; 67 import java.util.Set; 68 69 /** 70 * Main Activity of the WifiDialog application. All dialogs should be created and managed from here. 71 */ 72 public class WifiDialogActivity extends Activity { 73 private static final String TAG = "WifiDialog"; 74 private static final String KEY_DIALOG_INTENTS = "KEY_DIALOG_INTENTS"; 75 private static final String EXTRA_DIALOG_EXPIRATION_TIME_MS = 76 "com.android.wifi.dialog.DIALOG_START_TIME_MS"; 77 private static final String EXTRA_DIALOG_P2P_PIN_INPUT = 78 "com.android.wifi.dialog.DIALOG_P2P_PIN_INPUT"; 79 80 private @NonNull Handler mHandler = new Handler(Looper.getMainLooper()); 81 private @Nullable WifiContext mWifiContext; 82 private @Nullable WifiManager mWifiManager; 83 private boolean mIsVerboseLoggingEnabled; 84 private int mGravity = Gravity.NO_GRAVITY; 85 86 private @NonNull Set<Intent> mSavedStateIntents = new ArraySet<>(); 87 private @NonNull SparseArray<Intent> mLaunchIntentsPerId = new SparseArray<>(); 88 private @NonNull SparseArray<Dialog> mActiveDialogsPerId = new SparseArray<>(); 89 private @NonNull SparseArray<CountDownTimer> mActiveCountDownTimersPerId = new SparseArray<>(); 90 getWifiContext()91 private WifiContext getWifiContext() { 92 if (mWifiContext == null) { 93 mWifiContext = new WifiContext(this); 94 } 95 return mWifiContext; 96 } 97 getWifiResourceId(@onNull String name, @NonNull String type)98 private int getWifiResourceId(@NonNull String name, @NonNull String type) { 99 return getWifiContext().getResources().getIdentifier( 100 name, type, getWifiContext().getWifiOverlayApkPkgName()); 101 } 102 getWifiString(@onNull String name)103 private String getWifiString(@NonNull String name) { 104 return getWifiContext().getString(getWifiResourceId(name, "string")); 105 } 106 getWifiInteger(@onNull String name)107 private int getWifiInteger(@NonNull String name) { 108 return getWifiContext().getResources().getInteger(getWifiResourceId(name, "integer")); 109 } 110 getWifiBoolean(@onNull String name)111 private boolean getWifiBoolean(@NonNull String name) { 112 return getWifiContext().getResources().getBoolean(getWifiResourceId(name, "bool")); 113 } 114 getWifiLayoutId(@onNull String name)115 private int getWifiLayoutId(@NonNull String name) { 116 return getWifiResourceId(name, "layout"); 117 } 118 getWifiViewId(@onNull String name)119 private int getWifiViewId(@NonNull String name) { 120 return getWifiResourceId(name, "id"); 121 } 122 getWifiStyleId(@onNull String name)123 private int getWifiStyleId(@NonNull String name) { 124 return getWifiResourceId(name, "style"); 125 } 126 getWifiLayoutInflater()127 private LayoutInflater getWifiLayoutInflater() { 128 return getLayoutInflater().cloneInContext(getWifiContext()); 129 } 130 131 /** 132 * Returns an AlertDialog builder with the specified ServiceWifiResources theme applied. 133 */ getWifiAlertDialogBuilder(@onNull String styleName)134 private AlertDialog.Builder getWifiAlertDialogBuilder(@NonNull String styleName) { 135 return new AlertDialog.Builder( 136 new ContextThemeWrapper(getWifiContext(), getWifiStyleId(styleName))); 137 } 138 getWifiManager()139 private WifiManager getWifiManager() { 140 if (mWifiManager == null) { 141 mWifiManager = getSystemService(WifiManager.class); 142 } 143 return mWifiManager; 144 } 145 146 private final BroadcastReceiver mBroadcastReceiver = 147 new BroadcastReceiver() { 148 @Override 149 public void onReceive(Context context, Intent intent) { 150 if (!Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) { 151 return; 152 } 153 if (intent.getBooleanExtra( 154 WifiManager.EXTRA_CLOSE_SYSTEM_DIALOGS_EXCEPT_WIFI, false)) { 155 return; 156 } 157 // Cancel all dialogs for ACTION_CLOSE_SYSTEM_DIALOGS (e.g. Home button 158 // pressed). 159 for (int i = 0; i < mActiveDialogsPerId.size(); i++) { 160 mActiveDialogsPerId.valueAt(i).cancel(); 161 } 162 } 163 }; 164 165 @Override onCreate(Bundle savedInstanceState)166 protected void onCreate(Bundle savedInstanceState) { 167 super.onCreate(savedInstanceState); 168 registerReceiver(mBroadcastReceiver, new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), 169 RECEIVER_NOT_EXPORTED); 170 requestWindowFeature(Window.FEATURE_NO_TITLE); 171 172 mIsVerboseLoggingEnabled = getWifiManager().isVerboseLoggingEnabled(); 173 if (mIsVerboseLoggingEnabled) { 174 Log.v(TAG, "Creating WifiDialogActivity."); 175 } 176 mGravity = getWifiInteger("config_wifiDialogGravity"); 177 List<Intent> receivedIntents = new ArrayList<>(); 178 if (savedInstanceState != null) { 179 if (mIsVerboseLoggingEnabled) { 180 Log.v(TAG, "Restoring WifiDialog saved state."); 181 } 182 List<Intent> savedStateIntents = 183 savedInstanceState.getParcelableArrayList(KEY_DIALOG_INTENTS); 184 mSavedStateIntents.addAll(savedStateIntents); 185 receivedIntents.addAll(savedStateIntents); 186 } else { 187 receivedIntents.add(getIntent()); 188 } 189 for (Intent intent : receivedIntents) { 190 int dialogId = intent.getIntExtra(WifiManager.EXTRA_DIALOG_ID, 191 WifiManager.INVALID_DIALOG_ID); 192 if (dialogId == WifiManager.INVALID_DIALOG_ID) { 193 if (mIsVerboseLoggingEnabled) { 194 Log.v(TAG, "Received Intent with invalid dialogId!"); 195 } 196 continue; 197 } 198 mLaunchIntentsPerId.put(dialogId, intent); 199 } 200 } 201 202 /** 203 * Create and display a dialog for the currently held Intents. 204 */ 205 @Override onStart()206 protected void onStart() { 207 super.onStart(); 208 ArraySet<Integer> invalidDialogIds = new ArraySet<>(); 209 for (int i = 0; i < mLaunchIntentsPerId.size(); i++) { 210 int dialogId = mLaunchIntentsPerId.keyAt(i); 211 if (!createAndShowDialogForIntent(dialogId, mLaunchIntentsPerId.get(dialogId))) { 212 invalidDialogIds.add(dialogId); 213 } 214 } 215 invalidDialogIds.forEach(this::removeIntentAndPossiblyFinish); 216 } 217 218 /** 219 * Create and display a dialog for a new Intent received by a pre-existing WifiDialogActivity. 220 */ 221 @Override onNewIntent(Intent intent)222 protected void onNewIntent(Intent intent) { 223 super.onNewIntent(intent); 224 if (intent == null) { 225 return; 226 } 227 int dialogId = intent.getIntExtra(WifiManager.EXTRA_DIALOG_ID, 228 WifiManager.INVALID_DIALOG_ID); 229 if (dialogId == WifiManager.INVALID_DIALOG_ID) { 230 if (mIsVerboseLoggingEnabled) { 231 Log.v(TAG, "Received Intent with invalid dialogId!"); 232 } 233 return; 234 } 235 String action = intent.getAction(); 236 if (WifiManager.ACTION_DISMISS_DIALOG.equals(action)) { 237 removeIntentAndPossiblyFinish(dialogId); 238 return; 239 } 240 mLaunchIntentsPerId.put(dialogId, intent); 241 if (!createAndShowDialogForIntent(dialogId, intent)) { 242 removeIntentAndPossiblyFinish(dialogId); 243 } 244 } 245 246 @Override onStop()247 protected void onStop() { 248 super.onStop(); 249 if (!isChangingConfigurations() && !BuildCompat.isAtLeastU()) { 250 // Before U, we don't have INTERNAL_SYSTEM_WINDOW permission to always show at the 251 // top, so close all dialogs when we're not visible anymore (i.e. another app launches 252 // on top of us). 253 for (int i = 0; i < mActiveDialogsPerId.size(); i++) { 254 mActiveDialogsPerId.valueAt(i).cancel(); 255 } 256 return; 257 } 258 // Dismiss all the dialogs without removing it from mLaunchIntentsPerId to prevent window 259 // leaking. The dialogs will be recreated from mLaunchIntentsPerId in onStart(). 260 for (int i = 0; i < mActiveDialogsPerId.size(); i++) { 261 Dialog dialog = mActiveDialogsPerId.valueAt(i); 262 // Set the dismiss listener to null to prevent removing the Intent from 263 // mLaunchIntentsPerId. 264 dialog.setOnDismissListener(null); 265 dialog.dismiss(); 266 } 267 mActiveDialogsPerId.clear(); 268 for (int i = 0; i < mActiveCountDownTimersPerId.size(); i++) { 269 mActiveCountDownTimersPerId.valueAt(i).cancel(); 270 } 271 mActiveCountDownTimersPerId.clear(); 272 } 273 274 @Override onDestroy()275 protected void onDestroy() { 276 super.onDestroy(); 277 unregisterReceiver(mBroadcastReceiver); 278 // We don't expect to be destroyed while dialogs are still up, but make sure to cancel them 279 // just in case. 280 for (int i = 0; i < mActiveDialogsPerId.size(); i++) { 281 mActiveDialogsPerId.valueAt(i).cancel(); 282 } 283 } 284 285 @Override onSaveInstanceState(Bundle outState)286 protected void onSaveInstanceState(Bundle outState) { 287 ArrayList<Intent> intentList = new ArrayList<>(); 288 for (int i = 0; i < mLaunchIntentsPerId.size(); i++) { 289 intentList.add(mLaunchIntentsPerId.valueAt(i)); 290 } 291 outState.putParcelableArrayList(KEY_DIALOG_INTENTS, intentList); 292 super.onSaveInstanceState(outState); 293 } 294 295 /** 296 * Remove the Intent and corresponding dialog of the given dialogId (cancelling it if it is 297 * showing) and finish the Activity if there are no dialogs left to show. 298 */ removeIntentAndPossiblyFinish(int dialogId)299 private void removeIntentAndPossiblyFinish(int dialogId) { 300 mLaunchIntentsPerId.remove(dialogId); 301 Dialog dialog = mActiveDialogsPerId.get(dialogId); 302 mActiveDialogsPerId.remove(dialogId); 303 if (dialog != null && dialog.isShowing()) { 304 dialog.cancel(); 305 } 306 CountDownTimer timer = mActiveCountDownTimersPerId.get(dialogId); 307 mActiveCountDownTimersPerId.remove(dialogId); 308 if (timer != null) { 309 timer.cancel(); 310 } 311 if (mIsVerboseLoggingEnabled) { 312 Log.v(TAG, "Dialog id " + dialogId + " removed."); 313 } 314 if (mLaunchIntentsPerId.size() == 0) { 315 if (mIsVerboseLoggingEnabled) { 316 Log.v(TAG, "No dialogs left to show, finishing."); 317 } 318 finishAndRemoveTask(); 319 } 320 } 321 322 /** 323 * Creates and shows a dialog for the given dialogId and Intent. 324 * Returns {@code true} if the dialog was successfully created, {@code false} otherwise. 325 */ createAndShowDialogForIntent(int dialogId, @NonNull Intent intent)326 private boolean createAndShowDialogForIntent(int dialogId, @NonNull Intent intent) { 327 String action = intent.getAction(); 328 if (!WifiManager.ACTION_LAUNCH_DIALOG.equals(action)) { 329 return false; 330 } 331 final AlertDialog dialog; 332 int dialogType = intent.getIntExtra( 333 WifiManager.EXTRA_DIALOG_TYPE, WifiManager.DIALOG_TYPE_UNKNOWN); 334 switch (dialogType) { 335 case WifiManager.DIALOG_TYPE_SIMPLE: 336 dialog = createSimpleDialog(dialogId, 337 intent.getStringExtra(WifiManager.EXTRA_DIALOG_TITLE), 338 intent.getStringExtra(WifiManager.EXTRA_DIALOG_MESSAGE), 339 intent.getStringExtra(WifiManager.EXTRA_DIALOG_MESSAGE_URL), 340 intent.getIntExtra(WifiManager.EXTRA_DIALOG_MESSAGE_URL_START, 0), 341 intent.getIntExtra(WifiManager.EXTRA_DIALOG_MESSAGE_URL_END, 0), 342 intent.getStringExtra(WifiManager.EXTRA_DIALOG_POSITIVE_BUTTON_TEXT), 343 intent.getStringExtra(WifiManager.EXTRA_DIALOG_NEGATIVE_BUTTON_TEXT), 344 intent.getStringExtra(WifiManager.EXTRA_DIALOG_NEUTRAL_BUTTON_TEXT)); 345 break; 346 case WifiManager.DIALOG_TYPE_P2P_INVITATION_SENT: 347 dialog = createP2pInvitationSentDialog( 348 dialogId, 349 intent.getStringExtra(WifiManager.EXTRA_P2P_DEVICE_NAME), 350 intent.getStringExtra(WifiManager.EXTRA_P2P_DISPLAY_PIN)); 351 break; 352 case WifiManager.DIALOG_TYPE_P2P_INVITATION_RECEIVED: 353 dialog = createP2pInvitationReceivedDialog( 354 dialogId, 355 intent.getStringExtra(WifiManager.EXTRA_P2P_DEVICE_NAME), 356 intent.getBooleanExtra(WifiManager.EXTRA_P2P_PIN_REQUESTED, false), 357 intent.getStringExtra(WifiManager.EXTRA_P2P_DISPLAY_PIN)); 358 break; 359 default: 360 if (mIsVerboseLoggingEnabled) { 361 Log.v(TAG, "Could not create dialog with id= " + dialogId 362 + " for unknown type: " + dialogType); 363 } 364 return false; 365 } 366 dialog.setOnDismissListener((dialogDismiss) -> { 367 if (mIsVerboseLoggingEnabled) { 368 Log.v(TAG, "Dialog id=" + dialogId 369 + " dismissed."); 370 } 371 removeIntentAndPossiblyFinish(dialogId); 372 }); 373 dialog.setCanceledOnTouchOutside(getWifiBoolean("config_wifiDialogCanceledOnTouchOutside")); 374 if (mGravity != Gravity.NO_GRAVITY) { 375 dialog.getWindow().setGravity(mGravity); 376 } 377 if (BuildCompat.isAtLeastU()) { 378 dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG); 379 } 380 mActiveDialogsPerId.put(dialogId, dialog); 381 long timeoutMs = intent.getLongExtra(WifiManager.EXTRA_DIALOG_TIMEOUT_MS, 0); 382 if (timeoutMs > 0) { 383 // Use the original expiration time in case we've reloaded this dialog after a 384 // configuration change. 385 long expirationTimeMs = intent.getLongExtra(EXTRA_DIALOG_EXPIRATION_TIME_MS, 0); 386 if (expirationTimeMs > 0) { 387 timeoutMs = expirationTimeMs - SystemClock.uptimeMillis(); 388 if (timeoutMs < 0) { 389 timeoutMs = 0; 390 } 391 } else { 392 intent.putExtra( 393 EXTRA_DIALOG_EXPIRATION_TIME_MS, SystemClock.uptimeMillis() + timeoutMs); 394 } 395 CountDownTimer countDownTimer = new CountDownTimer(timeoutMs, 100) { 396 @Override 397 public void onTick(long millisUntilFinished) { 398 if (dialogType == WifiManager.DIALOG_TYPE_P2P_INVITATION_RECEIVED) { 399 int secondsRemaining = (int) millisUntilFinished / 1000; 400 if (millisUntilFinished % 1000 != 0) { 401 // Round up to the nearest whole second. 402 secondsRemaining++; 403 } 404 TextView timeRemaining = dialog.getWindow().findViewById( 405 getWifiViewId("time_remaining")); 406 timeRemaining.setText(MessageFormat.format( 407 getWifiString("wifi_p2p_invitation_seconds_remaining"), 408 secondsRemaining)); 409 timeRemaining.setVisibility(View.VISIBLE); 410 } 411 } 412 413 @Override 414 public void onFinish() { 415 removeIntentAndPossiblyFinish(dialogId); 416 } 417 }.start(); 418 mActiveCountDownTimersPerId.put(dialogId, countDownTimer); 419 } else { 420 if (dialogType == WifiManager.DIALOG_TYPE_P2P_INVITATION_RECEIVED) { 421 // Set the message back to null if we aren't using a timeout. 422 dialog.setMessage(null); 423 } 424 } 425 dialog.show(); 426 if (mIsVerboseLoggingEnabled) { 427 Log.v(TAG, "Showing dialog " + dialogId); 428 } 429 // Allow message URLs to be clickable. 430 TextView messageView = dialog.findViewById(android.R.id.message); 431 if (messageView != null) { 432 messageView.setMovementMethod(LinkMovementMethod.getInstance()); 433 } 434 // Play a notification sound/vibration if the dialog just came in (i.e. not read from the 435 // saved instance state after a configuration change), and the overlays specify a 436 // sound/vibration for the specific dialog type. 437 if (!mSavedStateIntents.contains(intent) 438 && dialogType == WifiManager.DIALOG_TYPE_P2P_INVITATION_RECEIVED 439 && getWifiBoolean("config_p2pInvitationReceivedDialogNotificationSound")) { 440 Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); 441 Ringtone r = RingtoneManager.getRingtone(this, notification); 442 r.play(); 443 if (mIsVerboseLoggingEnabled) { 444 Log.v(TAG, "Played notification sound for " + " dialogId=" + dialogId); 445 } 446 if (getSystemService(AudioManager.class).getRingerMode() 447 == AudioManager.RINGER_MODE_VIBRATE) { 448 getSystemService(Vibrator.class).vibrate(1_000); 449 if (mIsVerboseLoggingEnabled) { 450 Log.v(TAG, "Vibrated for " + " dialogId=" + dialogId); 451 } 452 } 453 } 454 return true; 455 } 456 457 /** 458 * Returns a simple dialog for the given Intent. 459 */ createSimpleDialog( int dialogId, @Nullable String title, @Nullable String message, @Nullable String messageUrl, int messageUrlStart, int messageUrlEnd, @Nullable String positiveButtonText, @Nullable String negativeButtonText, @Nullable String neutralButtonText)460 private @NonNull AlertDialog createSimpleDialog( 461 int dialogId, 462 @Nullable String title, 463 @Nullable String message, 464 @Nullable String messageUrl, 465 int messageUrlStart, 466 int messageUrlEnd, 467 @Nullable String positiveButtonText, 468 @Nullable String negativeButtonText, 469 @Nullable String neutralButtonText) { 470 SpannableString spannableMessage = null; 471 if (message != null) { 472 spannableMessage = new SpannableString(message); 473 if (messageUrl != null) { 474 if (messageUrlStart < 0) { 475 Log.w(TAG, "Span start cannot be less than 0!"); 476 } else if (messageUrlEnd > message.length()) { 477 Log.w(TAG, "Span end index " + messageUrlEnd 478 + " cannot be greater than message length " + message.length() + "!"); 479 } else if (messageUrlStart > messageUrlEnd) { 480 Log.w(TAG, "Span start index cannot be greater than end index!"); 481 } else { 482 spannableMessage.setSpan(new URLSpan(messageUrl), messageUrlStart, 483 messageUrlEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 484 } 485 } 486 } 487 AlertDialog dialog = getWifiAlertDialogBuilder("wifi_dialog") 488 .setTitle(title) 489 .setMessage(spannableMessage) 490 .setPositiveButton(positiveButtonText, (dialogPositive, which) -> { 491 if (mIsVerboseLoggingEnabled) { 492 Log.v(TAG, "Positive button pressed for simple dialog id=" 493 + dialogId); 494 } 495 getWifiManager().replyToSimpleDialog(dialogId, 496 WifiManager.DIALOG_REPLY_POSITIVE); 497 }) 498 .setNegativeButton(negativeButtonText, (dialogNegative, which) -> { 499 if (mIsVerboseLoggingEnabled) { 500 Log.v(TAG, "Negative button pressed for simple dialog id=" 501 + dialogId); 502 } 503 getWifiManager().replyToSimpleDialog(dialogId, 504 WifiManager.DIALOG_REPLY_NEGATIVE); 505 }) 506 .setNeutralButton(neutralButtonText, (dialogNeutral, which) -> { 507 if (mIsVerboseLoggingEnabled) { 508 Log.v(TAG, "Neutral button pressed for simple dialog id=" 509 + dialogId); 510 } 511 getWifiManager().replyToSimpleDialog(dialogId, 512 WifiManager.DIALOG_REPLY_NEUTRAL); 513 }) 514 .setOnCancelListener((dialogCancel) -> { 515 if (mIsVerboseLoggingEnabled) { 516 Log.v(TAG, "Simple dialog id=" + dialogId 517 + " cancelled."); 518 } 519 getWifiManager().replyToSimpleDialog(dialogId, 520 WifiManager.DIALOG_REPLY_CANCELLED); 521 }) 522 .create(); 523 if (mIsVerboseLoggingEnabled) { 524 Log.v(TAG, "Created a simple dialog." 525 + " id=" + dialogId 526 + " title=" + title 527 + " message=" + message 528 + " url=[" + messageUrl + "," + messageUrlStart + "," + messageUrlEnd + "]" 529 + " positiveButtonText=" + positiveButtonText 530 + " negativeButtonText=" + negativeButtonText 531 + " neutralButtonText=" + neutralButtonText); 532 } 533 return dialog; 534 } 535 536 /** 537 * Returns a P2P Invitation Sent Dialog for the given Intent. 538 */ createP2pInvitationSentDialog( final int dialogId, @Nullable final String deviceName, @Nullable final String displayPin)539 private @NonNull AlertDialog createP2pInvitationSentDialog( 540 final int dialogId, 541 @Nullable final String deviceName, 542 @Nullable final String displayPin) { 543 final View textEntryView = getWifiLayoutInflater() 544 .inflate(getWifiLayoutId("wifi_p2p_dialog"), null); 545 ViewGroup group = textEntryView.findViewById(getWifiViewId("info")); 546 if (TextUtils.isEmpty(deviceName)) { 547 Log.w(TAG, "P2P Invitation Sent dialog device name is null or empty." 548 + " id=" + dialogId 549 + " deviceName=" + deviceName 550 + " displayPin=" + displayPin); 551 } 552 addRowToP2pDialog(group, getWifiString("wifi_p2p_to_message"), deviceName); 553 554 if (displayPin != null) { 555 addRowToP2pDialog(group, getWifiString("wifi_p2p_show_pin_message"), displayPin); 556 } 557 558 AlertDialog dialog = getWifiAlertDialogBuilder("wifi_dialog") 559 .setTitle(getWifiString("wifi_p2p_invitation_sent_title")) 560 .setView(textEntryView) 561 .setPositiveButton(getWifiString("ok"), 562 (dialogPositive, which) -> { 563 // No-op 564 if (mIsVerboseLoggingEnabled) { 565 Log.v(TAG, "P2P Invitation Sent Dialog id=" + dialogId 566 + " accepted."); 567 } 568 }) 569 .create(); 570 if (mIsVerboseLoggingEnabled) { 571 Log.v(TAG, "Created P2P Invitation Sent dialog." 572 + " id=" + dialogId 573 + " deviceName=" + deviceName 574 + " displayPin=" + displayPin); 575 } 576 return dialog; 577 } 578 579 /** 580 * Returns a P2P Invitation Received Dialog for the given Intent. 581 */ createP2pInvitationReceivedDialog( final int dialogId, @Nullable final String deviceName, final boolean isPinRequested, @Nullable final String displayPin)582 private @NonNull AlertDialog createP2pInvitationReceivedDialog( 583 final int dialogId, 584 @Nullable final String deviceName, 585 final boolean isPinRequested, 586 @Nullable final String displayPin) { 587 if (TextUtils.isEmpty(deviceName)) { 588 Log.w(TAG, "P2P Invitation Received dialog device name is null or empty." 589 + " id=" + dialogId 590 + " deviceName=" + deviceName 591 + " displayPin=" + displayPin); 592 } 593 final View textEntryView = getWifiLayoutInflater() 594 .inflate(getWifiLayoutId("wifi_p2p_dialog"), null); 595 ViewGroup group = textEntryView.findViewById(getWifiViewId("info")); 596 addRowToP2pDialog(group, getWifiString("wifi_p2p_from_message"), deviceName); 597 598 final EditText pinEditText; 599 if (isPinRequested) { 600 textEntryView.findViewById(getWifiViewId("enter_pin_section")) 601 .setVisibility(View.VISIBLE); 602 pinEditText = textEntryView.findViewById(getWifiViewId("wifi_p2p_wps_pin")); 603 pinEditText.setVisibility(View.VISIBLE); 604 } else { 605 pinEditText = null; 606 } 607 if (displayPin != null) { 608 addRowToP2pDialog(group, getWifiString("wifi_p2p_show_pin_message"), displayPin); 609 } 610 611 AlertDialog dialog = getWifiAlertDialogBuilder("wifi_p2p_invitation_received_dialog") 612 .setTitle(getWifiString("wifi_p2p_invitation_to_connect_title")) 613 .setView(textEntryView) 614 .setPositiveButton(getWifiString("accept"), (dialogPositive, which) -> { 615 String pin = null; 616 if (pinEditText != null) { 617 pin = pinEditText.getText().toString(); 618 } 619 if (mIsVerboseLoggingEnabled) { 620 Log.v(TAG, "P2P Invitation Received Dialog id=" + dialogId 621 + " accepted with pin=" + pin); 622 } 623 getWifiManager().replyToP2pInvitationReceivedDialog(dialogId, true, pin); 624 }) 625 .setNegativeButton(getWifiString("decline"), (dialogNegative, which) -> { 626 if (mIsVerboseLoggingEnabled) { 627 Log.v(TAG, "P2P Invitation Received dialog id=" + dialogId 628 + " declined."); 629 } 630 getWifiManager().replyToP2pInvitationReceivedDialog(dialogId, false, null); 631 }) 632 .setOnCancelListener((dialogCancel) -> { 633 if (mIsVerboseLoggingEnabled) { 634 Log.v(TAG, "P2P Invitation Received dialog id=" + dialogId 635 + " cancelled."); 636 } 637 getWifiManager().replyToP2pInvitationReceivedDialog(dialogId, false, null); 638 }) 639 .create(); 640 if (pinEditText != null) { 641 dialog.getWindow().setSoftInputMode( 642 WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); 643 dialog.setOnShowListener(dialogShow -> { 644 Intent intent = mLaunchIntentsPerId.get(dialogId); 645 if (intent != null) { 646 // Populate the pin EditText with the previous user input if we're recreating 647 // the dialog after a configuration change. 648 CharSequence previousPin = 649 intent.getCharSequenceExtra(EXTRA_DIALOG_P2P_PIN_INPUT); 650 if (previousPin != null) { 651 pinEditText.setText(previousPin); 652 } 653 } 654 if (getResources().getConfiguration().orientation 655 == Configuration.ORIENTATION_PORTRAIT 656 || (getResources().getConfiguration().screenLayout 657 & Configuration.SCREENLAYOUT_SIZE_MASK) 658 >= Configuration.SCREENLAYOUT_SIZE_LARGE) { 659 pinEditText.requestFocus(); 660 pinEditText.setSelection(pinEditText.getText().length()); 661 } 662 dialog.getButton(Dialog.BUTTON_POSITIVE).setEnabled( 663 pinEditText.length() == 4 || pinEditText.length() == 8); 664 }); 665 pinEditText.addTextChangedListener(new TextWatcher() { 666 @Override 667 public void onTextChanged(CharSequence s, int start, int before, int count) { 668 // No-op. 669 } 670 671 @Override 672 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 673 // No-op. 674 } 675 676 @Override 677 public void afterTextChanged(Editable s) { 678 Intent intent = mLaunchIntentsPerId.get(dialogId); 679 if (intent != null) { 680 // Store the current input in the Intent in case we need to reload from a 681 // configuration change. 682 intent.putExtra(EXTRA_DIALOG_P2P_PIN_INPUT, s); 683 } 684 dialog.getButton(Dialog.BUTTON_POSITIVE).setEnabled( 685 s.length() == 4 || s.length() == 8); 686 } 687 }); 688 } else { 689 dialog.setOnShowListener(dialogShow -> { 690 dialog.getButton(Dialog.BUTTON_NEGATIVE).requestFocus(); 691 }); 692 } 693 if ((getResources().getConfiguration().uiMode & Configuration.UI_MODE_TYPE_APPLIANCE) 694 == Configuration.UI_MODE_TYPE_APPLIANCE) { 695 // For appliance devices, add a key listener which accepts. 696 dialog.setOnKeyListener((dialogKey, keyCode, event) -> { 697 if (keyCode == KeyEvent.KEYCODE_VOLUME_MUTE) { 698 // TODO: Plumb this response to framework. 699 dialog.dismiss(); 700 return true; 701 } 702 return true; 703 }); 704 } 705 if (mIsVerboseLoggingEnabled) { 706 Log.v(TAG, "Created P2P Invitation Received dialog." 707 + " id=" + dialogId 708 + " deviceName=" + deviceName 709 + " isPinRequested=" + isPinRequested 710 + " displayPin=" + displayPin); 711 } 712 return dialog; 713 } 714 715 /** 716 * Helper method to add a row to a ViewGroup for a P2P Invitation Received/Sent Dialog. 717 */ addRowToP2pDialog(ViewGroup group, String name, String value)718 private void addRowToP2pDialog(ViewGroup group, String name, String value) { 719 View row = getWifiLayoutInflater() 720 .inflate(getWifiLayoutId("wifi_p2p_dialog_row"), group, false); 721 ((TextView) row.findViewById(getWifiViewId("name"))).setText(name); 722 ((TextView) row.findViewById(getWifiViewId("value"))).setText(value); 723 group.addView(row); 724 } 725 } 726