diff options
-rw-r--r-- | WifiDialog/Android.bp | 3 | ||||
-rw-r--r-- | WifiDialog/AndroidManifest.xml | 20 | ||||
-rw-r--r-- | WifiDialog/src/com/android/wifi/dialog/WifiDialogActivity.java | 388 | ||||
-rw-r--r-- | framework/java/android/net/wifi/WifiContext.java | 53 | ||||
-rw-r--r-- | framework/java/android/net/wifi/WifiManager.java | 50 | ||||
-rw-r--r-- | service/java/com/android/server/wifi/WifiDialogManager.java | 127 | ||||
-rw-r--r-- | service/java/com/android/server/wifi/WifiInjector.java | 8 | ||||
-rw-r--r-- | service/java/com/android/server/wifi/WifiShellCommand.java | 51 |
8 files changed, 688 insertions, 12 deletions
diff --git a/WifiDialog/Android.bp b/WifiDialog/Android.bp index 5ccbe1a8e7..e9264ac61e 100644 --- a/WifiDialog/Android.bp +++ b/WifiDialog/Android.bp @@ -21,6 +21,9 @@ android_app { defaults: ["wifi-module-sdk-version-defaults"], certificate: ":com.android.wifi.dialog.certificate", + libs: [ + "framework-wifi.impl", + ], static_libs: [ "androidx.appcompat_appcompat", ], diff --git a/WifiDialog/AndroidManifest.xml b/WifiDialog/AndroidManifest.xml index f53625bb4e..35e6f2f5f4 100644 --- a/WifiDialog/AndroidManifest.xml +++ b/WifiDialog/AndroidManifest.xml @@ -18,9 +18,29 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.wifi.dialog"> + <queries> + <intent> + <action android:name="com.android.server.wifi.intent.action.SERVICE_WIFI_RESOURCES_APK" /> + </intent> + </queries> + <application android:enabled="true" android:configChanges="keyboardHidden|orientation|screenSize" android:supportsRtl="true"> + + <!-- This is only used to identify this app by resolving the action. + The activity is never actually triggered. --> + <activity android:name="android.app.Activity" android:exported="true" android:enabled="true"> + <intent-filter> + <action android:name="com.android.server.wifi.intent.action.WIFI_DIALOG_APK" /> + </intent-filter> + </activity>> + + <activity android:name=".WifiDialogActivity" + android:configChanges="keyboardHidden|orientation|screenSize" + android:hardwareAccelerated="true" + android:launchMode="singleInstance" + android:theme="@*android:style/Theme.DeviceDefault.Dialog.Alert.DayNight" /> </application> </manifest> diff --git a/WifiDialog/src/com/android/wifi/dialog/WifiDialogActivity.java b/WifiDialog/src/com/android/wifi/dialog/WifiDialogActivity.java new file mode 100644 index 0000000000..dadebcc62f --- /dev/null +++ b/WifiDialog/src/com/android/wifi/dialog/WifiDialogActivity.java @@ -0,0 +1,388 @@ +/* + * 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.wifi.dialog; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.net.wifi.WifiContext; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.os.Process; +import android.text.TextUtils; +import android.util.ArraySet; +import android.util.Log; +import android.util.SparseArray; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.EditText; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Main Activity of the WifiDialog application. All dialogs should be created and managed from here. + */ +public class WifiDialogActivity extends Activity { + private static final String TAG = "WifiDialog"; + private static final String KEY_DIALOG_INTENTS = "KEY_DIALOG_INTENTS"; + + private @Nullable WifiContext mWifiContext; + private @Nullable WifiManager mWifiManager; + private boolean mIsVerboseLoggingEnabled; + + private @NonNull SparseArray<Intent> mIntentsPerId = new SparseArray<>(); + private @NonNull SparseArray<Dialog> mActiveDialogsPerId = new SparseArray<>(); + + // Broadcast receiver for listening to ACTION_CLOSE_SYSTEM_DIALOGS + private BroadcastReceiver mCloseSystemDialogsReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (mIsVerboseLoggingEnabled) { + Log.v(TAG, "ACTION_CLOSE_SYSTEM_DIALOGS received, cancelling all dialogs."); + } + for (int i = 0; i < mActiveDialogsPerId.size(); i++) { + Dialog dialog = mActiveDialogsPerId.valueAt(i); + if (dialog.isShowing()) { + dialog.cancel(); + } + } + } + }; + + private WifiContext getWifiContext() { + if (mWifiContext == null) { + mWifiContext = new WifiContext(this); + } + return mWifiContext; + } + + /** + * Override the default Resources with the Resources from the active ServiceWifiResources APK. + */ + @Override + public Resources getResources() { + return getWifiContext().getResources(); + } + + // TODO(b/215605937): Remove these getXxxId() methods with the actual resource ID references + // once the build system is fixed to allow importing ServiceWifiResources. + private int getStringId(@NonNull String name) { + Resources res = getResources(); + return res.getIdentifier( + name, "string", getWifiContext().getWifiOverlayApkPkgName()); + } + + private int getLayoutId(@NonNull String name) { + Resources res = getResources(); + return res.getIdentifier( + name, "layout", getWifiContext().getWifiOverlayApkPkgName()); + } + + private int getViewId(@NonNull String name) { + Resources res = getResources(); + return res.getIdentifier( + name, "id", getWifiContext().getWifiOverlayApkPkgName()); + } + + private WifiManager getWifiManager() { + if (mWifiManager == null) { + mWifiManager = getSystemService(WifiManager.class); + } + return mWifiManager; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + + mIsVerboseLoggingEnabled = getWifiManager().isVerboseLoggingEnabled(); + if (mIsVerboseLoggingEnabled) { + Log.v(TAG, "Creating WifiDialogActivity."); + } + List<Intent> receivedIntents = new ArrayList<>(); + if (savedInstanceState != null) { + if (mIsVerboseLoggingEnabled) { + Log.v(TAG, "Restoring WifiDialog saved state."); + } + receivedIntents.addAll(savedInstanceState.getParcelableArrayList(KEY_DIALOG_INTENTS)); + } else { + receivedIntents.add(getIntent()); + } + for (Intent intent : receivedIntents) { + int dialogId = intent.getIntExtra(WifiManager.EXTRA_DIALOG_ID, -1); + if (dialogId < 0) { + if (mIsVerboseLoggingEnabled) { + Log.v(TAG, "Received Intent with negative dialogId=" + dialogId); + } + continue; + } + mIntentsPerId.put(dialogId, intent); + } + } + + /** + * Create and display a dialog for the currently held Intents. + */ + @Override + protected void onStart() { + super.onStart(); + registerReceiver( + mCloseSystemDialogsReceiver, new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + ArraySet<Integer> invalidDialogIds = new ArraySet<>(); + for (int i = 0; i < mIntentsPerId.size(); i++) { + int dialogId = mIntentsPerId.keyAt(i); + if (!createAndShowDialogForIntent(dialogId, mIntentsPerId.get(dialogId))) { + invalidDialogIds.add(dialogId); + } + } + invalidDialogIds.forEach(this::removeIntentAndPossiblyFinish); + } + + /** + * Create and display a dialog for a new Intent received by a pre-existing WifiDialogActivity. + */ + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + if (intent == null) { + return; + } + int dialogId = intent.getIntExtra(WifiManager.EXTRA_DIALOG_ID, -1); + if (dialogId < 0) { + if (mIsVerboseLoggingEnabled) { + Log.v(TAG, "Received Intent with negative dialogId=" + dialogId); + } + return; + } + mIntentsPerId.put(dialogId, intent); + if (!createAndShowDialogForIntent(dialogId, intent)) { + removeIntentAndPossiblyFinish(dialogId); + } + } + + @Override + protected void onStop() { + super.onStop(); + unregisterReceiver(mCloseSystemDialogsReceiver); + // Dismiss and remove any active Dialogs to prevent window leaking. + for (int i = 0; i < mActiveDialogsPerId.size(); i++) { + Dialog dialog = mActiveDialogsPerId.valueAt(i); + dialog.setOnDismissListener(null); + dialog.dismiss(); + } + mActiveDialogsPerId.clear(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + ArrayList<Intent> intentList = new ArrayList<>(); + for (int i = 0; i < mIntentsPerId.size(); i++) { + intentList.add(mIntentsPerId.valueAt(i)); + } + outState.putParcelableArrayList(KEY_DIALOG_INTENTS, intentList); + super.onSaveInstanceState(outState); + } + + /** + * Remove the Intent and corresponding Dialog of the given dialogId and finish the Activity if + * there are no dialogs left to show. + */ + private void removeIntentAndPossiblyFinish(int dialogId) { + mIntentsPerId.remove(dialogId); + Dialog dialog = mActiveDialogsPerId.get(dialogId); + mActiveDialogsPerId.remove(dialogId); + if (dialog != null && dialog.isShowing()) { + dialog.cancel(); + } + if (mIsVerboseLoggingEnabled) { + Log.v(TAG, "Dialog id " + dialogId + " removed."); + } + if (mIntentsPerId.size() == 0) { + if (mIsVerboseLoggingEnabled) { + Log.v(TAG, "No dialogs left to show, finishing."); + } + finishAndRemoveTask(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (isFinishing()) { + // Kill the process now instead of waiting indefinitely for ActivityManager to kill it. + Process.killProcess(android.os.Process.myPid()); + } + } + + /** + * Creates and shows a dialog for the given dialogId and Intent. + * Returns {@code true} if the dialog was successfully created, {@code false} otherwise. + */ + private @Nullable boolean createAndShowDialogForIntent(int dialogId, @NonNull Intent intent) { + Dialog dialog = null; + int dialogType = intent.getIntExtra( + WifiManager.EXTRA_DIALOG_TYPE, WifiManager.DIALOG_TYPE_UNKNOWN); + switch (dialogType) { + case WifiManager.DIALOG_TYPE_P2P_INVITATION_RECEIVED: + dialog = createP2pInvitationReceivedDialog( + dialogId, + intent.getStringExtra(WifiManager.EXTRA_P2P_DEVICE_NAME), + intent.getBooleanExtra(WifiManager.EXTRA_P2P_PIN_REQUESTED, false), + intent.getStringExtra(WifiManager.EXTRA_P2P_DISPLAY_PIN)); + break; + default: + if (mIsVerboseLoggingEnabled) { + Log.v(TAG, "Could not create dialog with id= " + dialogId + + " for unknown type: " + dialogType); + } + break; + } + if (dialog == null) { + return false; + } + mActiveDialogsPerId.put(dialogId, dialog); + dialog.show(); + if (mIsVerboseLoggingEnabled) { + Log.v(TAG, "Showing dialog " + dialogId); + } + return true; + } + + /** + * Returns a P2P Invitation Received Dialog for the given Intent, or {@code null} if no Dialog + * could be created. + */ + private @Nullable Dialog createP2pInvitationReceivedDialog( + final int dialogId, + final @NonNull String deviceName, + final boolean isPinRequested, + @Nullable String displayPin) { + if (TextUtils.isEmpty(deviceName)) { + if (mIsVerboseLoggingEnabled) { + Log.v(TAG, "Could not create P2P Invitation Received dialog with null or empty" + + " device name." + + " id=" + dialogId + + " deviceName=" + deviceName + + " isPinRequested=" + isPinRequested + + " displayPin=" + displayPin); + } + return null; + } + + final View textEntryView = LayoutInflater.from(this) + .inflate(getLayoutId("wifi_p2p_dialog"), null); + ViewGroup group = textEntryView.findViewById(getViewId("info")); + addRowToP2pDialog(group, getStringId("wifi_p2p_from_message"), deviceName); + + final EditText pinEditText; + if (isPinRequested) { + textEntryView.findViewById(getViewId("enter_pin_section")).setVisibility(View.VISIBLE); + pinEditText = textEntryView.findViewById(getViewId("wifi_p2p_wps_pin")); + pinEditText.setVisibility(View.VISIBLE); + } else { + pinEditText = null; + } + if (displayPin != null) { + addRowToP2pDialog(group, getStringId("wifi_p2p_show_pin_message"), displayPin); + } + + AlertDialog dialog = new AlertDialog.Builder(this) + .setTitle(getString(getStringId("wifi_p2p_invitation_to_connect_title"))) + .setView(textEntryView) + .setPositiveButton(getStringId("accept"), (dialogPositive, which) -> { + String pin = null; + if (pinEditText != null) { + pin = pinEditText.getText().toString(); + } + if (mIsVerboseLoggingEnabled) { + Log.v(TAG, "P2P Invitation Received Dialog id=" + dialogId + + " accepted with pin=" + pin); + } + // TODO: Plumb this response to framework. + }) + .setNegativeButton(getStringId("decline"), (dialogNegative, which) -> { + if (mIsVerboseLoggingEnabled) { + Log.v(TAG, "P2P Invitation Received dialog id=" + dialogId + + " declined."); + } + // TODO: Plumb this response to framework. + }) + .setOnCancelListener((dialogCancel) -> { + if (mIsVerboseLoggingEnabled) { + Log.v(TAG, "P2P Invitation Received dialog id=" + dialogId + + " cancelled."); + } + // TODO: Plumb this response to framework. + }) + .setOnDismissListener((dialogDismiss) -> { + if (mIsVerboseLoggingEnabled) { + Log.v(TAG, "P2P Invitation Received dialog id=" + dialogId + + " dismissed."); + } + // TODO: Plumb this response to framework. + removeIntentAndPossiblyFinish(dialogId); + }) + .create(); + dialog.setCanceledOnTouchOutside(false); + if ((getResources().getConfiguration().uiMode & Configuration.UI_MODE_TYPE_APPLIANCE) + == Configuration.UI_MODE_TYPE_APPLIANCE) { + // For appliance devices, add a key listener which accepts. + dialog.setOnKeyListener((dialogKey, keyCode, event) -> { + if (keyCode == KeyEvent.KEYCODE_VOLUME_MUTE) { + // TODO: Plumb this response to framework. + dialog.dismiss(); + return true; + } + return true; + }); + } + if (mIsVerboseLoggingEnabled) { + Log.v(TAG, "Created P2P Invitation Received dialog." + + " id=" + dialogId + + " deviceName=" + deviceName + + " isPinRequested=" + isPinRequested + + " displayPin=" + displayPin); + } + return dialog; + } + + /** + * Helper method to add a row to a ViewGroup for a P2P Invitation Received/Sent Dialog. + */ + private void addRowToP2pDialog(ViewGroup group, int stringId, String value) { + View row = LayoutInflater.from(this) + .inflate(getLayoutId("wifi_p2p_dialog_row"), group, false); + ((TextView) row.findViewById(getViewId("name"))).setText(getString(stringId)); + ((TextView) row.findViewById(getViewId("value"))).setText(value); + group.addView(row); + } +} diff --git a/framework/java/android/net/wifi/WifiContext.java b/framework/java/android/net/wifi/WifiContext.java index 9df4b8c4a3..8348d0531c 100644 --- a/framework/java/android/net/wifi/WifiContext.java +++ b/framework/java/android/net/wifi/WifiContext.java @@ -27,6 +27,8 @@ import android.content.res.Resources; import android.net.wifi.util.Environment; import android.util.Log; +import androidx.annotation.Nullable; + import java.util.List; import java.util.stream.Collectors; @@ -41,11 +43,15 @@ public class WifiContext extends ContextWrapper { /** Intent action that is used to identify ServiceWifiResources.apk */ private static final String ACTION_RESOURCES_APK = "com.android.server.wifi.intent.action.SERVICE_WIFI_RESOURCES_APK"; + /** Intent action that is used to identify WifiDialog.apk */ + private static final String ACTION_WIFI_DIALOG_APK = + "com.android.server.wifi.intent.action.WIFI_DIALOG_APK"; /** Since service-wifi runs within system_server, its package name is "android". */ private static final String SERVICE_WIFI_PACKAGE_NAME = "android"; private String mWifiOverlayApkPkgName; + private String mWifiDialogApkPkgName; // Cached resources from the resources APK. private AssetManager mWifiAssetsFromApk; @@ -57,40 +63,63 @@ public class WifiContext extends ContextWrapper { } /** Get the package name of ServiceWifiResources.apk */ - public String getWifiOverlayApkPkgName() { + public @Nullable String getWifiOverlayApkPkgName() { if (mWifiOverlayApkPkgName != null) { return mWifiOverlayApkPkgName; } + mWifiOverlayApkPkgName = getApkPkgNameForAction(ACTION_RESOURCES_APK); + if (mWifiOverlayApkPkgName == null) { + // Resource APK not loaded yet, print a stack trace to see where this is called from + Log.e(TAG, "Attempted to fetch resources before Wifi Resources APK is loaded!", + new IllegalStateException()); + return null; + } + Log.i(TAG, "Found Wifi Resources APK at: " + mWifiOverlayApkPkgName); + return mWifiOverlayApkPkgName; + } + + /** Get the package name of WifiDialog.apk */ + public @Nullable String getWifiDialogApkPkgName() { + if (mWifiDialogApkPkgName != null) { + return mWifiDialogApkPkgName; + } + mWifiDialogApkPkgName = getApkPkgNameForAction(ACTION_WIFI_DIALOG_APK); + if (mWifiDialogApkPkgName == null) { + // WifiDialog APK not loaded yet, print a stack trace to see where this is called from + Log.e(TAG, "Attempted to fetch WifiDialog apk before it is loaded!", + new IllegalStateException()); + return null; + } + Log.i(TAG, "Found Wifi Dialog APK at: " + mWifiDialogApkPkgName); + return mWifiDialogApkPkgName; + } + /** Gets the package name of the apk responding to the given intent action */ + private String getApkPkgNameForAction(@NonNull String action) { List<ResolveInfo> resolveInfos = getPackageManager().queryIntentActivities( - new Intent(ACTION_RESOURCES_APK), + new Intent(action), PackageManager.MATCH_SYSTEM_ONLY); - Log.i(TAG, "Got resolveInfos: " + resolveInfos); + Log.i(TAG, "Got resolveInfos for " + action + ": " + resolveInfos); // remove apps that don't live in the Wifi apex resolveInfos.removeIf(info -> !Environment.isAppInWifiApex(info.activityInfo.applicationInfo)); if (resolveInfos.isEmpty()) { - // Resource APK not loaded yet, print a stack trace to see where this is called from - Log.e(TAG, "Attempted to fetch resources before Wifi Resources APK is loaded!", - new IllegalStateException()); return null; } if (resolveInfos.size() > 1) { // multiple apps found, log a warning, but continue - Log.w(TAG, "Found > 1 APK that can resolve Wifi Resources APK intent: " + Log.w(TAG, "Found > 1 APK that can resolve " + action + ": " + resolveInfos.stream() - .map(info -> info.activityInfo.applicationInfo.packageName) - .collect(Collectors.joining(", "))); + .map(info -> info.activityInfo.applicationInfo.packageName) + .collect(Collectors.joining(", "))); } // Assume the first ResolveInfo is the one we're looking for ResolveInfo info = resolveInfos.get(0); - mWifiOverlayApkPkgName = info.activityInfo.applicationInfo.packageName; - Log.i(TAG, "Found Wifi Resources APK at: " + mWifiOverlayApkPkgName); - return mWifiOverlayApkPkgName; + return info.activityInfo.applicationInfo.packageName; } private Context getResourcesApkContext() { diff --git a/framework/java/android/net/wifi/WifiManager.java b/framework/java/android/net/wifi/WifiManager.java index d357d78e5b..8c8c4e127d 100644 --- a/framework/java/android/net/wifi/WifiManager.java +++ b/framework/java/android/net/wifi/WifiManager.java @@ -8834,4 +8834,54 @@ public class WifiManager { throw e.rethrowFromSystemServer(); } } + + /** + * Unknown DialogType. + * @hide + */ + public static final int DIALOG_TYPE_UNKNOWN = 0; + + /** + * DialogType for a P2P Invitation Received dialog. + * @hide + */ + public static final int DIALOG_TYPE_P2P_INVITATION_RECEIVED = 1; + + /** @hide */ + @IntDef(prefix = { "DIALOG_TYPE_" }, value = { + DIALOG_TYPE_UNKNOWN, + DIALOG_TYPE_P2P_INVITATION_RECEIVED, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DialogType {} + + /** + * Extra int indicating the type of dialog to display. + * @hide + */ + public static final String EXTRA_DIALOG_TYPE = "android.net.wifi.extra.DIALOG_TYPE"; + + /** + * Extra int indicating the ID of a dialog. The value must be non-negative. + * @hide + */ + public static final String EXTRA_DIALOG_ID = "android.net.wifi.extra.DIALOG_ID"; + + /** + * Extra String indicating a P2P device name for a P2P Invitation Sent/Received dialog. + * @hide + */ + public static final String EXTRA_P2P_DEVICE_NAME = "android.net.wifi.extra.P2P_DEVICE_NAME"; + + /** + * Extra boolean indicating that a PIN is requested for a P2P Invitation Received dialog. + * @hide + */ + public static final String EXTRA_P2P_PIN_REQUESTED = "android.net.wifi.extra.P2P_PIN_REQUESTED"; + + /** + * Extra String indicating the PIN to be displayed for a P2P Invitation Sent/Received dialog. + * @hide + */ + public static final String EXTRA_P2P_DISPLAY_PIN = "android.net.wifi.extra.P2P_DISPLAY_PIN"; } diff --git a/service/java/com/android/server/wifi/WifiDialogManager.java b/service/java/com/android/server/wifi/WifiDialogManager.java new file mode 100644 index 0000000000..5adf96709a --- /dev/null +++ b/service/java/com/android/server/wifi/WifiDialogManager.java @@ -0,0 +1,127 @@ +/* + * 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.content.Intent; +import android.content.pm.PackageManager; +import android.net.wifi.WifiContext; +import android.net.wifi.WifiManager; +import android.os.UserHandle; +import android.util.Log; +import android.util.SparseArray; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Class to manage launching dialogs via WifiDialog and returning the user reply. + */ +public class WifiDialogManager { + private static final String TAG = "WifiDialogManager"; + public static final String WIFI_DIALOG_ACTIVITY_CLASSNAME = + "com.android.wifi.dialog.WifiDialogActivity"; + + private boolean mVerboseLoggingEnabled; + + private int mNextDialogId = 0; + private @NonNull SparseArray<P2pInvitationReceivedDialogCallback> + mP2pInvitationReceivedDialogCallbacks = new SparseArray<>(); + + @NonNull WifiContext mContext; + @NonNull PackageManager mPackageManager; + + public WifiDialogManager(WifiContext context) { + mContext = context; + mPackageManager = context.getPackageManager(); + } + + /** + * Enables verbose logging. + */ + public void enableVerboseLogging(boolean enabled) { + mVerboseLoggingEnabled = enabled; + } + + private int getNextDialogId() { + if (mNextDialogId < 0) { + mNextDialogId = 0; + } + return mNextDialogId++; + } + + /** + * 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. + */ + void onDeclined(); + } + + /** + * Launches 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 callback Callback to receive the dialog response. Runs on the main Wi-Fi thread. + * @return id of the launched dialog, or {@code -1} if the dialog could not be created. + */ + public int launchP2pInvitationReceivedDialog( + String deviceName, + boolean isPinRequested, + @Nullable String displayPin, + @NonNull P2pInvitationReceivedDialogCallback callback) { + if (callback == null) { + Log.e(TAG, "Cannot launch a P2P Invitation Received dialog with null callback!"); + return -1; + } + int dialogId = getNextDialogId(); + Intent intent = new Intent(); + String wifiDialogApkPkgName = mContext.getWifiDialogApkPkgName(); + if (wifiDialogApkPkgName == null) { + Log.e(TAG, "Tried to launch P2P Invitation Received dialog but could not find a" + + " WifiDialog apk package name!"); + return -1; + } + intent.setClassName(wifiDialogApkPkgName, WIFI_DIALOG_ACTIVITY_CLASSNAME); + intent.putExtra(WifiManager.EXTRA_DIALOG_TYPE, + WifiManager.DIALOG_TYPE_P2P_INVITATION_RECEIVED); + intent.putExtra(WifiManager.EXTRA_DIALOG_ID, dialogId); + intent.putExtra(WifiManager.EXTRA_P2P_DEVICE_NAME, deviceName); + intent.putExtra(WifiManager.EXTRA_P2P_PIN_REQUESTED, isPinRequested); + intent.putExtra(WifiManager.EXTRA_P2P_DISPLAY_PIN, displayPin); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mP2pInvitationReceivedDialogCallbacks.put(dialogId, callback); + mContext.startActivityAsUser(intent, UserHandle.CURRENT); + if (mVerboseLoggingEnabled) { + Log.v(TAG, "Launching P2P Invitation Received dialog." + + " id=" + dialogId + + " deviceName=" + deviceName + + " isPinRequested=" + isPinRequested + + " displayPin=" + displayPin + + " callback=" + callback); + } + return dialogId; + } +} diff --git a/service/java/com/android/server/wifi/WifiInjector.java b/service/java/com/android/server/wifi/WifiInjector.java index d3ea13cefc..eefca2833b 100644 --- a/service/java/com/android/server/wifi/WifiInjector.java +++ b/service/java/com/android/server/wifi/WifiInjector.java @@ -240,6 +240,7 @@ public class WifiInjector { private final ExternalScoreUpdateObserverProxy mExternalScoreUpdateObserverProxy; private final WifiNotificationManager mWifiNotificationManager; private final LastCallerInfoManager mLastCallerInfoManager; + @NonNull private final WifiDialogManager mWifiDialogManager; public WifiInjector(WifiContext context) { if (context == null) { @@ -527,6 +528,7 @@ public class WifiInjector { mSimRequiredNotifier = new SimRequiredNotifier(mContext, mFrameworkFacade, mWifiNotificationManager); mLastCallerInfoManager = new LastCallerInfoManager(); + mWifiDialogManager = new WifiDialogManager(mContext); } /** @@ -581,6 +583,7 @@ public class WifiInjector { } mWifiPermissionsWrapper.enableVerboseLogging(verboseEnabled); mWifiPermissionsUtil.enableVerboseLogging(verboseEnabled); + mWifiDialogManager.enableVerboseLogging(verboseEnabled); } public UserManager getUserManager() { @@ -1090,6 +1093,11 @@ public class WifiInjector { return mLastCallerInfoManager; } + @NonNull + public WifiDialogManager getWifiDialogManager() { + return mWifiDialogManager; + } + public BuildProperties getBuildProperties() { return mBuildProperties; } diff --git a/service/java/com/android/server/wifi/WifiShellCommand.java b/service/java/com/android/server/wifi/WifiShellCommand.java index fdef4b7cdd..8b286dd887 100644 --- a/service/java/com/android/server/wifi/WifiShellCommand.java +++ b/service/java/com/android/server/wifi/WifiShellCommand.java @@ -75,6 +75,7 @@ import android.text.TextUtils; import android.util.Pair; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.android.internal.annotations.VisibleForTesting; @@ -172,6 +173,7 @@ public class WifiShellCommand extends BasicShellCommandHandler { private final WifiApConfigStore mWifiApConfigStore; private int mSapState = WifiManager.WIFI_STATE_UNKNOWN; private final ScanRequestProxy mScanRequestProxy; + private final @NonNull WifiDialogManager mWifiDialogManager; private class SoftApCallbackProxy extends ISoftApCallback.Stub { private final PrintWriter mPrintWriter; @@ -280,6 +282,7 @@ public class WifiShellCommand extends BasicShellCommandHandler { mSelfRecovery = wifiInjector.getSelfRecovery(); mWifiApConfigStore = wifiInjector.getWifiApConfigStore(); mScanRequestProxy = wifiInjector.getScanRequestProxy(); + mWifiDialogManager = wifiInjector.getWifiDialogManager(); } @Override @@ -1090,6 +1093,48 @@ public class WifiShellCommand extends BasicShellCommandHandler { } mScanRequestProxy.enableScanning(enabled, hiddenEnabled); return 0; + case "launch-dialog-p2p-invitation-received": + String deviceName = getNextArgRequired(); + boolean isPinRequested = false; + String displayPin = null; + String pinOption = getNextOption(); + while (pinOption != null) { + if (pinOption.equals("-p")) { + isPinRequested = true; + } else if (pinOption.equals("-d")) { + displayPin = getNextArgRequired(); + } else { + pw.println("Ignoring unknown option " + pinOption); + } + pinOption = getNextOption(); + } + ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1); + WifiDialogManager.P2pInvitationReceivedDialogCallback callback = + new WifiDialogManager.P2pInvitationReceivedDialogCallback() { + @Override + public void onAccepted(@Nullable String optionalPin) { + queue.offer("Invitation accepted with optionalPin=" + optionalPin); + } + + @Override + public void onDeclined() { + queue.offer("Invitation declined"); + } + }; + mWifiDialogManager.launchP2pInvitationReceivedDialog( + deviceName, + isPinRequested, + displayPin, + callback); + pw.println("Launched dialog. Waiting up to 15 seconds for user response."); + pw.flush(); + String msg = queue.poll(15, TimeUnit.SECONDS); + if (msg == null) { + pw.println("No response received."); + } else { + pw.println(msg); + } + return 0; default: return handleDefaultCommands(cmd); } @@ -1776,6 +1821,12 @@ public class WifiShellCommand extends BasicShellCommandHandler { pw.println( " Reset the WiFi resources cache which will cause them to be reloaded next " + "time they are accessed. Necessary if overlays are manually modified."); + pw.println(" launch-dialog-p2p-invitation-received <device_name> [-p] [-d <pin>]"); + pw.println(" Launches a P2P Invitation Received dialog and waits up to 30 seconds to" + + " print the response."); + pw.println(" <device_name> - Name of the device sending the invitation"); + pw.println(" -p - Show PIN input"); + pw.println(" -d - Display PIN <pin>"); } private void onHelpPrivileged(PrintWriter pw) { |