/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.wifi; import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.net.wifi.WifiContext; import android.net.wifi.WifiManager; import android.os.UserHandle; import android.provider.Browser; import android.text.SpannableString; import android.text.Spanned; import android.text.method.LinkMovementMethod; import android.text.style.URLSpan; import android.util.ArraySet; import android.util.Log; import android.util.SparseArray; import android.view.ContextThemeWrapper; import android.view.Display; import android.view.Gravity; import android.view.View; import android.view.Window; import android.view.WindowInsets; import android.view.WindowManager; import android.widget.TextView; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.modules.utils.build.SdkLevel; import com.android.wifi.resources.R; import java.util.Set; import javax.annotation.concurrent.ThreadSafe; /** * Class to manage launching dialogs and returning the user reply. * All methods run on the main Wi-Fi thread runner except those annotated with @AnyThread, which can * run on any thread. */ public class WifiDialogManager { private static final String TAG = "WifiDialogManager"; @VisibleForTesting static final String WIFI_DIALOG_ACTIVITY_CLASSNAME = "com.android.wifi.dialog.WifiDialogActivity"; private boolean mVerboseLoggingEnabled; private int mNextDialogId = 0; private final Set mActiveDialogIds = new ArraySet<>(); private final @NonNull SparseArray mActiveDialogHandles = new SparseArray<>(); private final @NonNull ArraySet mActiveLegacySimpleDialogs = new ArraySet<>(); private final @NonNull WifiContext mContext; private final @NonNull WifiThreadRunner mWifiThreadRunner; private final @NonNull FrameworkFacade mFrameworkFacade; private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { mWifiThreadRunner.post( () -> { String action = intent.getAction(); if (mVerboseLoggingEnabled) { Log.v(TAG, "Received action: " + action); } if (Intent.ACTION_USER_PRESENT.equals(action)) { // Change all window types to TYPE_KEYGUARD_DIALOG to show the // dialogs over the QuickSettings after the screen is unlocked. for (LegacySimpleDialogHandle dialogHandle : mActiveLegacySimpleDialogs) { dialogHandle.changeWindowType( WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); } } else if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) { if (intent.getBooleanExtra( WifiManager.EXTRA_CLOSE_SYSTEM_DIALOGS_EXCEPT_WIFI, false)) { return; } if (mVerboseLoggingEnabled) { Log.v( TAG, "ACTION_CLOSE_SYSTEM_DIALOGS received, cancelling" + " all legacy dialogs."); } for (LegacySimpleDialogHandle dialogHandle : mActiveLegacySimpleDialogs) { dialogHandle.cancelDialog(); } } }, TAG + "#onReceive"); } }; /** * Constructs a WifiDialogManager * * @param context Main Wi-Fi context. * @param wifiThreadRunner Main Wi-Fi thread runner. * @param frameworkFacade FrameworkFacade for launching legacy dialogs. */ public WifiDialogManager( @NonNull WifiContext context, @NonNull WifiThreadRunner wifiThreadRunner, @NonNull FrameworkFacade frameworkFacade, WifiInjector wifiInjector) { mContext = context; mWifiThreadRunner = wifiThreadRunner; mFrameworkFacade = frameworkFacade; IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_USER_PRESENT); intentFilter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); int flags = 0; if (SdkLevel.isAtLeastT()) { flags = Context.RECEIVER_EXPORTED; } mContext.registerReceiver(mBroadcastReceiver, intentFilter, flags); wifiInjector.getWifiDeviceStateChangeManager() .registerStateChangeCallback( new WifiDeviceStateChangeManager.StateChangeCallback() { @Override public void onScreenStateChanged(boolean screenOn) { handleScreenStateChanged(screenOn); } }); } private void handleScreenStateChanged(boolean screenOn) { // Change all window types to TYPE_APPLICATION_OVERLAY to // prevent the dialogs from appearing over the lock screen when // the screen turns on again. if (!screenOn) { if (mVerboseLoggingEnabled) { Log.d(TAG, "onScreenStateChanged: screen off"); } // Change all window types to TYPE_APPLICATION_OVERLAY to // prevent the dialogs from appearing over the lock screen when // the screen turns on again. for (LegacySimpleDialogHandle dialogHandle : mActiveLegacySimpleDialogs) { dialogHandle.changeWindowType( WindowManager.LayoutParams .TYPE_APPLICATION_OVERLAY); } } } /** * Enables verbose logging. */ public void enableVerboseLogging(boolean enabled) { mVerboseLoggingEnabled = enabled; } private int getNextDialogId() { if (mActiveDialogIds.isEmpty() || mNextDialogId == WifiManager.INVALID_DIALOG_ID) { mNextDialogId = 0; } return mNextDialogId++; } private @Nullable Intent getBaseLaunchIntent(@WifiManager.DialogType int dialogType) { Intent intent = new Intent(WifiManager.ACTION_LAUNCH_DIALOG) .putExtra(WifiManager.EXTRA_DIALOG_TYPE, dialogType) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); String wifiDialogApkPkgName = mContext.getWifiDialogApkPkgName(); if (wifiDialogApkPkgName == null) { Log.w(TAG, "Could not get WifiDialog APK package name!"); return null; } intent.setClassName(wifiDialogApkPkgName, WIFI_DIALOG_ACTIVITY_CLASSNAME); return intent; } private @Nullable Intent getDismissIntent(int dialogId) { Intent intent = new Intent(WifiManager.ACTION_DISMISS_DIALOG); intent.putExtra(WifiManager.EXTRA_DIALOG_ID, dialogId); String wifiDialogApkPkgName = mContext.getWifiDialogApkPkgName(); if (wifiDialogApkPkgName == null) { Log.w(TAG, "Could not get WifiDialog APK package name!"); return null; } intent.setClassName(wifiDialogApkPkgName, WIFI_DIALOG_ACTIVITY_CLASSNAME); return intent; } /** * Handle for launching and dismissing a dialog from any thread. */ @ThreadSafe public class DialogHandle { DialogHandleInternal mInternalHandle; LegacySimpleDialogHandle mLegacyHandle; private DialogHandle(DialogHandleInternal internalHandle) { mInternalHandle = internalHandle; } private DialogHandle(LegacySimpleDialogHandle legacyHandle) { mLegacyHandle = legacyHandle; } /** * Launches the dialog. */ @AnyThread public void launchDialog() { if (mInternalHandle != null) { mWifiThreadRunner.post(() -> mInternalHandle.launchDialog(0), TAG + "#launchDialog"); } else if (mLegacyHandle != null) { mWifiThreadRunner.post(() -> mLegacyHandle.launchDialog(0), TAG + "#launchDialog"); } } /** * Launches the dialog with a timeout before it is auto-cancelled. * @param timeoutMs timeout in milliseconds before the dialog is auto-cancelled. A value <=0 * indicates no timeout. */ @AnyThread public void launchDialog(long timeoutMs) { if (mInternalHandle != null) { mWifiThreadRunner.post(() -> mInternalHandle.launchDialog(timeoutMs), TAG + "#launchDialogTimeout"); } else if (mLegacyHandle != null) { mWifiThreadRunner.post(() -> mLegacyHandle.launchDialog(timeoutMs), TAG + "#launchDialogTimeout"); } } /** * Dismisses the dialog. Dialogs will automatically be dismissed once the user replies, but * this method may be used to dismiss unanswered dialogs that are no longer needed. */ @AnyThread public void dismissDialog() { if (mInternalHandle != null) { mWifiThreadRunner.post(() -> mInternalHandle.dismissDialog(), TAG + "#dismissDialog"); } else if (mLegacyHandle != null) { mWifiThreadRunner.post(() -> mLegacyHandle.dismissDialog(), TAG + "#dismissDialog"); } } } /** * Internal handle for launching and dismissing a dialog via the WifiDialog app from the main * Wi-Fi thread runner. * @see {@link DialogHandle} */ private class DialogHandleInternal { private int mDialogId = WifiManager.INVALID_DIALOG_ID; private @Nullable Intent mIntent; private int mDisplayId = Display.DEFAULT_DISPLAY; void setIntent(@Nullable Intent intent) { mIntent = intent; } void setDisplayId(int displayId) { mDisplayId = displayId; } /** * @see {@link DialogHandle#launchDialog(long)} */ void launchDialog(long timeoutMs) { if (mIntent == null) { Log.e(TAG, "Cannot launch dialog with null Intent!"); return; } if (mDialogId != WifiManager.INVALID_DIALOG_ID) { // Dialog is already active, ignore. return; } registerDialog(); mIntent.putExtra(WifiManager.EXTRA_DIALOG_TIMEOUT_MS, timeoutMs); mIntent.putExtra(WifiManager.EXTRA_DIALOG_ID, mDialogId); boolean launched = false; // Collapse the QuickSettings since we can't show WifiDialog dialogs over it. mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS) .putExtra(WifiManager.EXTRA_CLOSE_SYSTEM_DIALOGS_EXCEPT_WIFI, true)); if (SdkLevel.isAtLeastT() && mDisplayId != Display.DEFAULT_DISPLAY) { try { mContext.startActivityAsUser(mIntent, ActivityOptions.makeBasic().setLaunchDisplayId(mDisplayId).toBundle(), UserHandle.CURRENT); launched = true; } catch (Exception e) { Log.e(TAG, "Error startActivityAsUser - " + e); } } if (!launched) { mContext.startActivityAsUser(mIntent, UserHandle.CURRENT); } if (mVerboseLoggingEnabled) { Log.v(TAG, "Launching dialog with id=" + mDialogId); } } /** * @see {@link DialogHandle#dismissDialog()} */ void dismissDialog() { if (mDialogId == WifiManager.INVALID_DIALOG_ID) { // Dialog is not active, ignore. return; } Intent dismissIntent = getDismissIntent(mDialogId); if (dismissIntent == null) { Log.e(TAG, "Could not create intent for dismissing dialog with id: " + mDialogId); return; } mContext.startActivityAsUser(dismissIntent, UserHandle.CURRENT); if (mVerboseLoggingEnabled) { Log.v(TAG, "Dismissing dialog with id=" + mDialogId); } unregisterDialog(); } /** * Assigns a dialog id to the dialog and registers it as an active dialog. */ void registerDialog() { if (mDialogId != WifiManager.INVALID_DIALOG_ID) { // Already registered. return; } mDialogId = getNextDialogId(); mActiveDialogIds.add(mDialogId); mActiveDialogHandles.put(mDialogId, this); if (mVerboseLoggingEnabled) { Log.v(TAG, "Registered dialog with id=" + mDialogId + ". Active dialogs ids: " + mActiveDialogIds); } } /** * Unregisters the dialog as an active dialog and removes its dialog id. * This should be called after a dialog is replied to or dismissed. */ void unregisterDialog() { if (mDialogId == WifiManager.INVALID_DIALOG_ID) { // Already unregistered. return; } mActiveDialogIds.remove(mDialogId); mActiveDialogHandles.remove(mDialogId); if (mVerboseLoggingEnabled) { Log.v(TAG, "Unregistered dialog with id=" + mDialogId + ". Active dialogs ids: " + mActiveDialogIds); } mDialogId = WifiManager.INVALID_DIALOG_ID; if (mActiveDialogIds.isEmpty()) { String wifiDialogApkPkgName = mContext.getWifiDialogApkPkgName(); if (wifiDialogApkPkgName == null) { Log.wtf(TAG, "Could not get WifiDialog APK package name to force stop!"); return; } if (mVerboseLoggingEnabled) { Log.v(TAG, "Force stopping WifiDialog app"); } mContext.getSystemService(ActivityManager.class) .forceStopPackage(wifiDialogApkPkgName); } } } private class SimpleDialogHandle extends DialogHandleInternal { @Nullable private final SimpleDialogCallback mCallback; @Nullable private final WifiThreadRunner mCallbackThreadRunner; private final String mTitle; 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) { mTitle = title; Intent intent = getBaseLaunchIntent(WifiManager.DIALOG_TYPE_SIMPLE); if (intent != null) { intent.putExtra(WifiManager.EXTRA_DIALOG_TITLE, title) .putExtra(WifiManager.EXTRA_DIALOG_MESSAGE, message) .putExtra(WifiManager.EXTRA_DIALOG_MESSAGE_URL, messageUrl) .putExtra(WifiManager.EXTRA_DIALOG_MESSAGE_URL_START, messageUrlStart) .putExtra(WifiManager.EXTRA_DIALOG_MESSAGE_URL_END, messageUrlEnd) .putExtra(WifiManager.EXTRA_DIALOG_POSITIVE_BUTTON_TEXT, positiveButtonText) .putExtra(WifiManager.EXTRA_DIALOG_NEGATIVE_BUTTON_TEXT, negativeButtonText) .putExtra(WifiManager.EXTRA_DIALOG_NEUTRAL_BUTTON_TEXT, neutralButtonText); setIntent(intent); } setDisplayId(Display.DEFAULT_DISPLAY); mCallback = callback; mCallbackThreadRunner = callbackThreadRunner; } void notifyOnPositiveButtonClicked() { if (mCallbackThreadRunner != null && mCallback != null) { mCallbackThreadRunner.post(mCallback::onPositiveButtonClicked, mTitle + "#onPositiveButtonClicked"); } unregisterDialog(); } void notifyOnNegativeButtonClicked() { if (mCallbackThreadRunner != null && mCallback != null) { mCallbackThreadRunner.post(mCallback::onNegativeButtonClicked, mTitle + "#onNegativeButtonClicked"); } unregisterDialog(); } void notifyOnNeutralButtonClicked() { if (mCallbackThreadRunner != null && mCallback != null) { mCallbackThreadRunner.post(mCallback::onNeutralButtonClicked, mTitle + "#onNeutralButtonClicked"); } unregisterDialog(); } void notifyOnCancelled() { if (mCallbackThreadRunner != null && mCallback != null) { mCallbackThreadRunner.post(mCallback::onCancelled, mTitle + "#onCancelled"); } unregisterDialog(); } } /** * Implementation of a simple dialog using AlertDialogs created directly in the system process. */ private class LegacySimpleDialogHandle { final String mTitle; final SpannableString mMessage; final String mPositiveButtonText; final String mNegativeButtonText; final String mNeutralButtonText; @Nullable final SimpleDialogCallback mCallback; @Nullable final WifiThreadRunner mCallbackThreadRunner; private Runnable mTimeoutRunnable; private AlertDialog mAlertDialog; int mWindowType = WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG; long mTimeoutMs = 0; 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) { mTitle = title; if (message != null) { mMessage = new SpannableString(message); if (messageUrl != null) { if (messageUrlStart < 0) { Log.w(TAG, "Span start cannot be less than 0!"); } else if (messageUrlEnd > message.length()) { Log.w(TAG, "Span end index " + messageUrlEnd + " cannot be greater than " + "message length " + message.length() + "!"); } else if (messageUrlStart > messageUrlEnd) { Log.w(TAG, "Span start index cannot be greater than end index!"); } else { mMessage.setSpan(new URLSpan(messageUrl) { @Override public void onClick(@NonNull View widget) { Context c = widget.getContext(); Intent openLinkIntent = new Intent(Intent.ACTION_VIEW) .setData(Uri.parse(messageUrl)) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .putExtra(Browser.EXTRA_APPLICATION_ID, c.getPackageName()); c.startActivityAsUser(openLinkIntent, UserHandle.CURRENT); LegacySimpleDialogHandle.this.dismissDialog(); }}, messageUrlStart, messageUrlEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } } else { mMessage = null; } mPositiveButtonText = positiveButtonText; mNegativeButtonText = negativeButtonText; mNeutralButtonText = neutralButtonText; mCallback = callback; mCallbackThreadRunner = callbackThreadRunner; } void launchDialog(long timeoutMs) { if (mAlertDialog != null && mAlertDialog.isShowing()) { // Dialog is already launched. Dismiss and create a new one. mAlertDialog.setOnDismissListener(null); mAlertDialog.dismiss(); } if (mTimeoutRunnable != null) { // Reset the timeout runnable if one has already been created. mWifiThreadRunner.removeCallbacks(mTimeoutRunnable); mTimeoutRunnable = null; } mTimeoutMs = timeoutMs; mAlertDialog = mFrameworkFacade.makeAlertDialogBuilder( new ContextThemeWrapper(mContext, R.style.wifi_dialog)) .setTitle(mTitle) .setMessage(mMessage) .setPositiveButton(mPositiveButtonText, (dialogPositive, which) -> { if (mVerboseLoggingEnabled) { Log.v(TAG, "Positive button pressed for legacy simple dialog"); } if (mCallbackThreadRunner != null && mCallback != null) { mCallbackThreadRunner.post(mCallback::onPositiveButtonClicked, mTitle + "#onPositiveButtonClicked"); } }) .setNegativeButton(mNegativeButtonText, (dialogNegative, which) -> { if (mVerboseLoggingEnabled) { Log.v(TAG, "Negative button pressed for legacy simple dialog"); } if (mCallbackThreadRunner != null && mCallback != null) { mCallbackThreadRunner.post(mCallback::onNegativeButtonClicked, mTitle + "#onNegativeButtonClicked"); } }) .setNeutralButton(mNeutralButtonText, (dialogNeutral, which) -> { if (mVerboseLoggingEnabled) { Log.v(TAG, "Neutral button pressed for legacy simple dialog"); } if (mCallbackThreadRunner != null && mCallback != null) { mCallbackThreadRunner.post(mCallback::onNeutralButtonClicked, mTitle + "#onNeutralButtonClicked"); } }) .setOnCancelListener((dialogCancel) -> { if (mVerboseLoggingEnabled) { Log.v(TAG, "Legacy simple dialog cancelled."); } if (mCallbackThreadRunner != null && mCallback != null) { mCallbackThreadRunner.post(mCallback::onCancelled, mTitle + "#onCancelled"); } }) .setOnDismissListener((dialogDismiss) -> { mWifiThreadRunner.post(() -> { if (mTimeoutRunnable != null) { mWifiThreadRunner.removeCallbacks(mTimeoutRunnable); mTimeoutRunnable = null; } mAlertDialog = null; mActiveLegacySimpleDialogs.remove(this); }, mTitle + "#onDismiss"); }) .create(); mAlertDialog.setCanceledOnTouchOutside(mContext.getResources().getBoolean( R.bool.config_wifiDialogCanceledOnTouchOutside)); final Window window = mAlertDialog.getWindow(); int gravity = mContext.getResources().getInteger(R.integer.config_wifiDialogGravity); if (gravity != Gravity.NO_GRAVITY) { window.setGravity(gravity); } final WindowManager.LayoutParams lp = window.getAttributes(); window.setType(mWindowType); lp.setFitInsetsTypes(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars()); lp.setFitInsetsSides(WindowInsets.Side.all()); lp.setFitInsetsIgnoringVisibility(true); window.setAttributes(lp); window.addSystemFlags( WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS); mAlertDialog.show(); TextView messageView = mAlertDialog.findViewById(android.R.id.message); if (messageView != null) { messageView.setMovementMethod(LinkMovementMethod.getInstance()); } if (mTimeoutMs > 0) { mTimeoutRunnable = mAlertDialog::cancel; mWifiThreadRunner.postDelayed(mTimeoutRunnable, mTimeoutMs, TAG + "#cancelDialog"); } mActiveLegacySimpleDialogs.add(this); } void dismissDialog() { if (mAlertDialog != null) { mAlertDialog.dismiss(); } } void cancelDialog() { if (mAlertDialog != null) { mAlertDialog.cancel(); } } void changeWindowType(int windowType) { mWindowType = windowType; if (mActiveLegacySimpleDialogs.contains(this)) { launchDialog(mTimeoutMs); } } } /** * Callback for receiving simple dialog responses. */ public interface SimpleDialogCallback { /** * The positive button was clicked. */ void onPositiveButtonClicked(); /** * The negative button was clicked. */ void onNegativeButtonClicked(); /** * The neutral button was clicked. */ void onNeutralButtonClicked(); /** * The dialog was cancelled (back button or home button or timeout). */ void onCancelled(); } /** * Creates a simple dialog with optional title, message, and positive/negative/neutral buttons. * * @param title Title of the dialog. * @param message Message of the dialog. * @param positiveButtonText Text of the positive button or {@code null} for no button. * @param negativeButtonText Text of the negative button or {@code null} for no button. * @param neutralButtonText Text of the neutral button or {@code null} for no button. * @param callback Callback to receive the dialog response. * @param callbackThreadRunner WifiThreadRunner to run the callback on. * @return DialogHandle Handle for the dialog, or {@code null} if no dialog could * be created. */ @AnyThread @NonNull public DialogHandle createSimpleDialog( @Nullable String title, @Nullable String message, @Nullable String positiveButtonText, @Nullable String negativeButtonText, @Nullable String neutralButtonText, @NonNull SimpleDialogCallback callback, @NonNull WifiThreadRunner callbackThreadRunner) { return createSimpleDialogWithUrl( title, message, null /* messageUrl */, 0 /* messageUrlStart */, 0 /* messageUrlEnd */, positiveButtonText, negativeButtonText, neutralButtonText, callback, callbackThreadRunner); } /** * Creates a simple dialog with a URL embedded in the message. * * @param title Title of the dialog. * @param message Message of the dialog. * @param messageUrl URL to embed in the message. If non-null, then message must also * be non-null. * @param messageUrlStart Start index (inclusive) of the URL in the message. Must be * non-negative. * @param messageUrlEnd End index (exclusive) of the URL in the message. Must be less * than the length of message. * @param positiveButtonText Text of the positive button or {@code null} for no button. * @param negativeButtonText Text of the negative button or {@code null} for no button. * @param neutralButtonText Text of the neutral button or {@code null} for no button. * @param callback Callback to receive the dialog response. * @param callbackThreadRunner WifiThreadRunner to run the callback on. * @return DialogHandle Handle for the dialog, or {@code null} if no dialog could * be created. */ @AnyThread @NonNull public DialogHandle createSimpleDialogWithUrl( @Nullable 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) { if (SdkLevel.isAtLeastT()) { return new DialogHandle( new SimpleDialogHandle( title, message, messageUrl, messageUrlStart, messageUrlEnd, positiveButtonText, negativeButtonText, neutralButtonText, callback, callbackThreadRunner) ); } else { // TODO(b/238353074): Remove this fallback to the legacy implementation once the // AlertDialog style on pre-T platform is fixed. return new DialogHandle( new LegacySimpleDialogHandle( title, message, messageUrl, messageUrlStart, messageUrlEnd, positiveButtonText, negativeButtonText, neutralButtonText, callback, callbackThreadRunner) ); } } /** * Creates a legacy simple dialog on the system process with optional title, message, and * positive/negative/neutral buttons. * * @param title Title of the dialog. * @param message Message of the dialog. * @param positiveButtonText Text of the positive button or {@code null} for no button. * @param negativeButtonText Text of the negative button or {@code null} for no button. * @param neutralButtonText Text of the neutral button or {@code null} for no button. * @param callback Callback to receive the dialog response. * @param callbackThreadRunner WifiThreadRunner to run the callback on. * @return DialogHandle Handle for the dialog, or {@code null} if no dialog could * be created. */ @AnyThread @NonNull public DialogHandle createLegacySimpleDialog( @Nullable String title, @Nullable String message, @Nullable String positiveButtonText, @Nullable String negativeButtonText, @Nullable String neutralButtonText, @NonNull SimpleDialogCallback callback, @NonNull WifiThreadRunner callbackThreadRunner) { return createLegacySimpleDialogWithUrl( title, message, null /* messageUrl */, 0 /* messageUrlStart */, 0 /* messageUrlEnd */, positiveButtonText, negativeButtonText, neutralButtonText, callback, callbackThreadRunner); } /** * Creates a legacy simple dialog on the system process with a URL embedded in the message. * * @param title Title of the dialog. * @param message Message of the dialog. * @param messageUrl URL to embed in the message. If non-null, then message must also * be non-null. * @param messageUrlStart Start index (inclusive) of the URL in the message. Must be * non-negative. * @param messageUrlEnd End index (exclusive) of the URL in the message. Must be less * than the length of message. * @param positiveButtonText Text of the positive button or {@code null} for no button. * @param negativeButtonText Text of the negative button or {@code null} for no button. * @param neutralButtonText Text of the neutral button or {@code null} for no button. * @param callback Callback to receive the dialog response. * @param callbackThreadRunner WifiThreadRunner to run the callback on. * @return DialogHandle Handle for the dialog, or {@code null} if no dialog could * be created. */ @AnyThread @NonNull public DialogHandle createLegacySimpleDialogWithUrl( @Nullable 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) { return new DialogHandle( new LegacySimpleDialogHandle( title, message, messageUrl, messageUrlStart, messageUrlEnd, positiveButtonText, negativeButtonText, neutralButtonText, callback, callbackThreadRunner) ); } /** * Returns the reply to a simple dialog to the callback of matching dialogId. * @param dialogId id of the replying dialog. * @param reply reply of the dialog. */ public void replyToSimpleDialog(int dialogId, @WifiManager.DialogReply int reply) { if (mVerboseLoggingEnabled) { Log.i(TAG, "Response received for simple dialog. id=" + dialogId + " reply=" + reply); } DialogHandleInternal internalHandle = mActiveDialogHandles.get(dialogId); if (internalHandle == null) { if (mVerboseLoggingEnabled) { Log.w(TAG, "No matching dialog handle for simple dialog id=" + dialogId); } return; } if (!(internalHandle instanceof SimpleDialogHandle)) { if (mVerboseLoggingEnabled) { Log.w(TAG, "Dialog handle with id " + dialogId + " is not for a simple dialog."); } return; } switch (reply) { case WifiManager.DIALOG_REPLY_POSITIVE: ((SimpleDialogHandle) internalHandle).notifyOnPositiveButtonClicked(); break; case WifiManager.DIALOG_REPLY_NEGATIVE: ((SimpleDialogHandle) internalHandle).notifyOnNegativeButtonClicked(); break; case WifiManager.DIALOG_REPLY_NEUTRAL: ((SimpleDialogHandle) internalHandle).notifyOnNeutralButtonClicked(); break; case WifiManager.DIALOG_REPLY_CANCELLED: ((SimpleDialogHandle) internalHandle).notifyOnCancelled(); break; default: if (mVerboseLoggingEnabled) { Log.w(TAG, "Received invalid reply=" + reply); } } } private class P2pInvitationReceivedDialogHandle extends DialogHandleInternal { @Nullable private final P2pInvitationReceivedDialogCallback mCallback; @Nullable private final WifiThreadRunner mCallbackThreadRunner; P2pInvitationReceivedDialogHandle( final @Nullable String deviceName, final boolean isPinRequested, @Nullable String displayPin, int displayId, @Nullable P2pInvitationReceivedDialogCallback callback, @Nullable WifiThreadRunner callbackThreadRunner) { Intent intent = getBaseLaunchIntent(WifiManager.DIALOG_TYPE_P2P_INVITATION_RECEIVED); if (intent != null) { intent.putExtra(WifiManager.EXTRA_P2P_DEVICE_NAME, deviceName) .putExtra(WifiManager.EXTRA_P2P_PIN_REQUESTED, isPinRequested) .putExtra(WifiManager.EXTRA_P2P_DISPLAY_PIN, displayPin); setIntent(intent); } setDisplayId(displayId); mCallback = callback; mCallbackThreadRunner = callbackThreadRunner; } void notifyOnAccepted(@Nullable String optionalPin) { if (mCallbackThreadRunner != null && mCallback != null) { mCallbackThreadRunner.post(() -> mCallback.onAccepted(optionalPin), "P2pInvitationReceivedDialogHandle" + "#notifyOnAccepted"); } unregisterDialog(); } void notifyOnDeclined() { if (mCallbackThreadRunner != null && mCallback != null) { mCallbackThreadRunner.post(mCallback::onDeclined, "P2pInvitationReceivedDialogHandle" + "#notifyOnDeclined"); } unregisterDialog(); } } /** * Callback for receiving P2P Invitation Received dialog responses. */ public interface P2pInvitationReceivedDialogCallback { /** * Invitation was accepted. * * @param optionalPin Optional PIN if a PIN was requested, or {@code null} otherwise. */ void onAccepted(@Nullable String optionalPin); /** * Invitation was declined or cancelled (back button or home button or timeout). */ void onDeclined(); } /** * Creates a P2P Invitation Received dialog. * * @param deviceName Name of the device sending the invitation. * @param isPinRequested True if a PIN was requested and a PIN input UI should be shown. * @param displayPin Display PIN, or {@code null} if no PIN should be displayed * @param displayId The ID of the Display on which to place the dialog * (Display.DEFAULT_DISPLAY * refers to the default display) * @param callback Callback to receive the dialog response. * @param callbackThreadRunner WifiThreadRunner to run the callback on. * @return DialogHandle Handle for the dialog, or {@code null} if no dialog could * be created. */ @AnyThread @NonNull public DialogHandle createP2pInvitationReceivedDialog( @Nullable String deviceName, boolean isPinRequested, @Nullable String displayPin, int displayId, @Nullable P2pInvitationReceivedDialogCallback callback, @Nullable WifiThreadRunner callbackThreadRunner) { return new DialogHandle( new P2pInvitationReceivedDialogHandle( deviceName, isPinRequested, displayPin, displayId, callback, callbackThreadRunner) ); } /** * Returns the reply to a P2P Invitation Received dialog to the callback of matching dialogId. * Note: Must be invoked only from the main Wi-Fi thread. * * @param dialogId id of the replying dialog. * @param accepted Whether the invitation was accepted. * @param optionalPin PIN of the reply, or {@code null} if none was supplied. */ public void replyToP2pInvitationReceivedDialog( int dialogId, boolean accepted, @Nullable String optionalPin) { if (mVerboseLoggingEnabled) { Log.i(TAG, "Response received for P2P Invitation Received dialog." + " id=" + dialogId + " accepted=" + accepted + " pin=" + optionalPin); } DialogHandleInternal internalHandle = mActiveDialogHandles.get(dialogId); if (internalHandle == null) { if (mVerboseLoggingEnabled) { Log.w(TAG, "No matching dialog handle for P2P Invitation Received dialog" + " id=" + dialogId); } return; } if (!(internalHandle instanceof P2pInvitationReceivedDialogHandle)) { if (mVerboseLoggingEnabled) { Log.w(TAG, "Dialog handle with id " + dialogId + " is not for a P2P Invitation Received dialog."); } return; } if (accepted) { ((P2pInvitationReceivedDialogHandle) internalHandle).notifyOnAccepted(optionalPin); } else { ((P2pInvitationReceivedDialogHandle) internalHandle).notifyOnDeclined(); } } private class P2pInvitationSentDialogHandle extends DialogHandleInternal { P2pInvitationSentDialogHandle( @Nullable final String deviceName, @Nullable final String displayPin, int displayId) { Intent intent = getBaseLaunchIntent(WifiManager.DIALOG_TYPE_P2P_INVITATION_SENT); if (intent != null) { intent.putExtra(WifiManager.EXTRA_P2P_DEVICE_NAME, deviceName) .putExtra(WifiManager.EXTRA_P2P_DISPLAY_PIN, displayPin); setIntent(intent); } setDisplayId(displayId); } } /** * Creates a P2P Invitation Sent dialog. * * @param deviceName Name of the device the invitation was sent to. * @param displayPin display PIN * @param displayId display ID * @return DialogHandle Handle for the dialog, or {@code null} if no dialog could * be created. */ @AnyThread @NonNull public DialogHandle createP2pInvitationSentDialog( @Nullable String deviceName, @Nullable String displayPin, int displayId) { return new DialogHandle(new P2pInvitationSentDialogHandle(deviceName, displayPin, displayId)); } }