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