Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b7e6decf authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Automerger Merge Worker
Browse files

Merge changes from topics "face_auth_ripple", "udfps_falsing" into sc-dev am: 9a5967ff

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/14104139

Change-Id: I006992e71844deec789889ae705d16f7fe860923
parents 85fda9d6 9a5967ff
Loading
Loading
Loading
Loading
+8 −0
Original line number Original line Diff line number Diff line
@@ -100,4 +100,12 @@
            android:ellipsize="marquee"
            android:ellipsize="marquee"
            android:focusable="true" />
            android:focusable="true" />
    </LinearLayout>
    </LinearLayout>

    <com.android.systemui.biometrics.AuthRippleView
        android:id="@+id/auth_ripple"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:importantForAccessibility="no"
        sysui:ignoreRightInset="true"
    />
</com.android.systemui.statusbar.phone.NotificationShadeWindowView>
</com.android.systemui.statusbar.phone.NotificationShadeWindowView>
+7 −0
Original line number Original line Diff line number Diff line
@@ -642,4 +642,11 @@


    <!-- Whether to use window background blur for the volume dialog. -->
    <!-- Whether to use window background blur for the volume dialog. -->
    <bool name="config_volumeDialogUseBackgroundBlur">false</bool>
    <bool name="config_volumeDialogUseBackgroundBlur">false</bool>

    <!-- The properties of the face auth camera in pixels -->
    <integer-array name="config_face_auth_props">
        <!-- sensorLocationX -->
        <!-- sensorLocationY -->
        <!--sensorRadius -->
    </integer-array>
</resources>
</resources>
+42 −27
Original line number Original line Diff line number Diff line
@@ -18,11 +18,12 @@ package com.android.keyguard;


import static android.hardware.biometrics.BiometricSourceType.FINGERPRINT;
import static android.hardware.biometrics.BiometricSourceType.FINGERPRINT;


import static com.android.systemui.classifier.Classifier.DISABLED_UDFPS_AFFORDANCE;

import android.content.Context;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.graphics.drawable.InsetDrawable;
import android.hardware.biometrics.BiometricSourceType;
import android.hardware.biometrics.BiometricSourceType;
import android.view.MotionEvent;
import android.view.View;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup;


@@ -33,7 +34,9 @@ import com.android.systemui.Dumpable;
import com.android.systemui.R;
import com.android.systemui.R;
import com.android.systemui.biometrics.AuthController;
import com.android.systemui.biometrics.AuthController;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.ViewController;
import com.android.systemui.util.ViewController;


@@ -53,14 +56,16 @@ public class DisabledUdfpsController extends ViewController<DisabledUdfpsView> i
    @NonNull private final KeyguardViewController mKeyguardViewController;
    @NonNull private final KeyguardViewController mKeyguardViewController;
    @NonNull private final StatusBarStateController mStatusBarStateController;
    @NonNull private final StatusBarStateController mStatusBarStateController;
    @NonNull private final KeyguardStateController mKeyguardStateController;
    @NonNull private final KeyguardStateController mKeyguardStateController;
    @NonNull private final FalsingManager mFalsingManager;
    @NonNull private final Drawable mButton;
    @NonNull private final Drawable mButton;
    @NonNull private final Drawable mUnlockIcon;
    @NonNull private final Drawable mUnlockIcon;


    private boolean mIsDozing;
    private boolean mIsDozing;
    private boolean mIsBouncerShowing;
    private boolean mIsBouncerShowing;
    private boolean mIsKeyguardShowing;
    private boolean mRunningFPS;
    private boolean mRunningFPS;
    private boolean mCanDismissLockScreen;
    private boolean mCanDismissLockScreen;
    private boolean mQsExpanded;
    private int mStatusBarState;


    private boolean mShowButton;
    private boolean mShowButton;
    private boolean mShowUnlockIcon;
    private boolean mShowUnlockIcon;
@@ -71,16 +76,19 @@ public class DisabledUdfpsController extends ViewController<DisabledUdfpsView> i
            @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor,
            @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor,
            @NonNull AuthController authController,
            @NonNull AuthController authController,
            @NonNull KeyguardViewController keyguardViewController,
            @NonNull KeyguardViewController keyguardViewController,
            @NonNull KeyguardStateController keyguardStateController
            @NonNull KeyguardStateController keyguardStateController,
            @NonNull FalsingManager falsingManager
    ) {
    ) {
        super(view);
        super(view);
        mView.setOnTouchListener(mOnTouchListener);
        mView.setOnClickListener(v -> onAffordanceClick());
        mView.setOnLongClickListener(v -> onAffordanceClick());
        mView.setSensorProperties(authController.getUdfpsProps().get(0));
        mView.setSensorProperties(authController.getUdfpsProps().get(0));


        mStatusBarStateController = statusBarStateController;
        mStatusBarStateController = statusBarStateController;
        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
        mKeyguardViewController = keyguardViewController;
        mKeyguardViewController = keyguardViewController;
        mKeyguardStateController = keyguardStateController;
        mKeyguardStateController = keyguardStateController;
        mFalsingManager = falsingManager;


        final Context context = view.getContext();
        final Context context = view.getContext();
        mButton = context.getResources().getDrawable(
        mButton = context.getResources().getDrawable(
@@ -94,10 +102,10 @@ public class DisabledUdfpsController extends ViewController<DisabledUdfpsView> i
    @Override
    @Override
    protected void onViewAttached() {
    protected void onViewAttached() {
        mIsBouncerShowing = mKeyguardViewController.isBouncerShowing();
        mIsBouncerShowing = mKeyguardViewController.isBouncerShowing();
        mIsKeyguardShowing = mKeyguardStateController.isShowing();
        mIsDozing = mStatusBarStateController.isDozing();
        mIsDozing = mStatusBarStateController.isDozing();
        mRunningFPS = mKeyguardUpdateMonitor.isFingerprintDetectionRunning();
        mRunningFPS = mKeyguardUpdateMonitor.isFingerprintDetectionRunning();
        mCanDismissLockScreen = mKeyguardStateController.canDismissLockScreen();
        mCanDismissLockScreen = mKeyguardStateController.canDismissLockScreen();
        mStatusBarState = mStatusBarStateController.getState();
        mUnlockIcon.setTint(Utils.getColorAttrDefaultColor(mView.getContext(),
        mUnlockIcon.setTint(Utils.getColorAttrDefaultColor(mView.getContext(),
                R.attr.wallpaperTextColorAccent));
                R.attr.wallpaperTextColorAccent));
        updateVisibility();
        updateVisibility();
@@ -114,6 +122,15 @@ public class DisabledUdfpsController extends ViewController<DisabledUdfpsView> i
        mKeyguardStateController.removeCallback(mKeyguardStateCallback);
        mKeyguardStateController.removeCallback(mKeyguardStateCallback);
    }
    }


    private boolean onAffordanceClick() {
        if (mFalsingManager.isFalseTouch(DISABLED_UDFPS_AFFORDANCE)) {
            return false;
        }
        mView.setVisibility(View.INVISIBLE);
        mKeyguardViewController.showBouncer(/* scrim */ true);
        return true;
    }

    /**
    /**
     * Call when this controller is no longer needed. This will remove the view from its parent.
     * Call when this controller is no longer needed. This will remove the view from its parent.
     */
     */
@@ -123,6 +140,14 @@ public class DisabledUdfpsController extends ViewController<DisabledUdfpsView> i
        }
        }
    }
    }


    /**
     * Set whether qs is expanded. When QS is expanded, don't show a DisabledUdfps affordance.
     */
    public void setQsExpanded(boolean expanded) {
        mQsExpanded = expanded;
        updateVisibility();
    }

    private void updateVisibility() {
    private void updateVisibility() {
        mShowButton = !mCanDismissLockScreen && !mRunningFPS && isLockScreen();
        mShowButton = !mCanDismissLockScreen && !mRunningFPS && isLockScreen();
        mShowUnlockIcon = mCanDismissLockScreen && isLockScreen();
        mShowUnlockIcon = mCanDismissLockScreen && isLockScreen();
@@ -139,7 +164,10 @@ public class DisabledUdfpsController extends ViewController<DisabledUdfpsView> i
    }
    }


    private boolean isLockScreen() {
    private boolean isLockScreen() {
        return mIsKeyguardShowing && !mIsDozing && !mIsBouncerShowing;
        return !mIsDozing
                && !mIsBouncerShowing
                && !mQsExpanded
                && mStatusBarState == StatusBarState.KEYGUARD;
    }
    }


    @Override
    @Override
@@ -148,20 +176,13 @@ public class DisabledUdfpsController extends ViewController<DisabledUdfpsView> i
        pw.println("  mShowBouncerButton: " + mShowButton);
        pw.println("  mShowBouncerButton: " + mShowButton);
        pw.println("  mShowUnlockIcon: " + mShowUnlockIcon);
        pw.println("  mShowUnlockIcon: " + mShowUnlockIcon);
        pw.println("  mIsDozing: " + mIsDozing);
        pw.println("  mIsDozing: " + mIsDozing);
        pw.println("  mIsKeyguardShowing: " + mIsKeyguardShowing);
        pw.println("  mIsBouncerShowing: " + mIsBouncerShowing);
        pw.println("  mIsBouncerShowing: " + mIsBouncerShowing);
        pw.println("  mRunningFPS: " + mRunningFPS);
        pw.println("  mRunningFPS: " + mRunningFPS);
        pw.println("  mCanDismissLockScreen: " + mCanDismissLockScreen);
        pw.println("  mCanDismissLockScreen: " + mCanDismissLockScreen);
        pw.println("  mStatusBarState: " + StatusBarState.toShortString(mStatusBarState));
        pw.println("  mQsExpanded: " + mQsExpanded);
    }
    }


    private final View.OnTouchListener mOnTouchListener = new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            mKeyguardViewController.showBouncer(/* scrim */ true);
            return true;
        }
    };

    private StatusBarStateController.StateListener mStatusBarStateListener =
    private StatusBarStateController.StateListener mStatusBarStateListener =
            new StatusBarStateController.StateListener() {
            new StatusBarStateController.StateListener() {
                @Override
                @Override
@@ -169,6 +190,12 @@ public class DisabledUdfpsController extends ViewController<DisabledUdfpsView> i
                    mIsDozing = isDozing;
                    mIsDozing = isDozing;
                    updateVisibility();
                    updateVisibility();
                }
                }

                @Override
                public void onStateChanged(int statusBarState) {
                    mStatusBarState = statusBarState;
                    updateVisibility();
                }
            };
            };


    private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback =
    private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback =
@@ -192,22 +219,10 @@ public class DisabledUdfpsController extends ViewController<DisabledUdfpsView> i


    private final KeyguardStateController.Callback mKeyguardStateCallback =
    private final KeyguardStateController.Callback mKeyguardStateCallback =
            new KeyguardStateController.Callback() {
            new KeyguardStateController.Callback() {
        @Override
        public void onKeyguardShowingChanged() {
            updateIsKeyguardShowing();
            updateVisibility();
        }

        @Override
        @Override
        public void onUnlockedChanged() {
        public void onUnlockedChanged() {
            updateIsKeyguardShowing();
            mCanDismissLockScreen = mKeyguardStateController.canDismissLockScreen();
            mCanDismissLockScreen = mKeyguardStateController.canDismissLockScreen();
            updateVisibility();
            updateVisibility();
        }
        }

        private void updateIsKeyguardShowing() {
            mIsKeyguardShowing = mKeyguardStateController.isShowing()
                    && !mKeyguardStateController.isKeyguardGoingAway();
        }
    };
    };
}
}
+37 −2
Original line number Original line Diff line number Diff line
@@ -29,6 +29,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.content.res.Configuration;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.RectF;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricPrompt;
import android.hardware.biometrics.BiometricPrompt;
@@ -82,6 +83,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
    @Nullable private final List<FingerprintSensorPropertiesInternal> mFpProps;
    @Nullable private final List<FingerprintSensorPropertiesInternal> mFpProps;
    @Nullable private final List<FaceSensorPropertiesInternal> mFaceProps;
    @Nullable private final List<FaceSensorPropertiesInternal> mFaceProps;
    @Nullable private final List<FingerprintSensorPropertiesInternal> mUdfpsProps;
    @Nullable private final List<FingerprintSensorPropertiesInternal> mUdfpsProps;
    @Nullable private final PointF mFaceAuthSensorLocation;


    // TODO: These should just be saved from onSaveState
    // TODO: These should just be saved from onSaveState
    private SomeArgs mCurrentDialogArgs;
    private SomeArgs mCurrentDialogArgs;
@@ -261,10 +263,34 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
    }
    }


    /**
    /**
     * @return where the UDFPS exists on the screen in pixels.
     * @return where the UDFPS exists on the screen in pixels in portrait mode.
     */
     */
    public RectF getUdfpsRegion() {
    public RectF getUdfpsRegion() {
        return mUdfpsController == null ? null : mUdfpsController.getSensorLocation();
        return mUdfpsController == null
                ? null
                : mUdfpsController.getSensorLocation();
    }

    /**
     * @return where the UDFPS exists on the screen in pixels in portrait mode.
     */
    public PointF getUdfpsSensorLocation() {
        if (mUdfpsController == null) {
            return null;
        }
        return new PointF(mUdfpsController.getSensorLocation().centerX(),
                mUdfpsController.getSensorLocation().centerY());
    }

    /**
     * @return where the face authentication sensor exists relative to the screen in pixels in
     * portrait mode.
     */
    public PointF getFaceAuthSensorLocation() {
        if (mFaceProps == null || mFaceAuthSensorLocation == null) {
            return null;
        }
        return new PointF(mFaceAuthSensorLocation.x, mFaceAuthSensorLocation.y);
    }
    }


    /**
    /**
@@ -339,6 +365,15 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
            }
            }
        }
        }
        mUdfpsProps = !udfpsProps.isEmpty() ? udfpsProps : null;
        mUdfpsProps = !udfpsProps.isEmpty() ? udfpsProps : null;
        int[] faceAuthLocation = context.getResources().getIntArray(
                com.android.systemui.R.array.config_face_auth_props);
        if (faceAuthLocation == null || faceAuthLocation.length < 2) {
            mFaceAuthSensorLocation = null;
        } else {
            mFaceAuthSensorLocation = new PointF(
                    (float) faceAuthLocation[0],
                    (float) faceAuthLocation[1]);
        }


        IntentFilter filter = new IntentFilter();
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
        filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+113 −52
Original line number Original line Diff line number Diff line
@@ -17,17 +17,19 @@
package com.android.systemui.biometrics
package com.android.systemui.biometrics


import android.content.Context
import android.content.Context
import android.content.res.Configuration
import android.graphics.PointF
import android.hardware.biometrics.BiometricSourceType
import android.hardware.biometrics.BiometricSourceType
import android.view.View
import androidx.annotation.VisibleForTesting
import android.view.ViewGroup
import com.android.internal.annotations.VisibleForTesting
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.settingslib.Utils
import com.android.settingslib.Utils
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.statusbar.phone.dagger.StatusBarComponent.StatusBarScope
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.ViewController
import java.io.PrintWriter
import java.io.PrintWriter
import javax.inject.Inject
import javax.inject.Inject


@@ -35,30 +37,82 @@ import javax.inject.Inject
 * Controls the ripple effect that shows when authentication is successful.
 * Controls the ripple effect that shows when authentication is successful.
 * The ripple uses the accent color of the current theme.
 * The ripple uses the accent color of the current theme.
 */
 */
@SysUISingleton
@StatusBarScope
class AuthRippleController @Inject constructor(
class AuthRippleController @Inject constructor(
    commandRegistry: CommandRegistry,
    private val sysuiContext: Context,
    configurationController: ConfigurationController,
    private val authController: AuthController,
    private val context: Context,
    private val configurationController: ConfigurationController,
    private val keyguardUpdateMonitor: KeyguardUpdateMonitor
    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
) {
    private val commandRegistry: CommandRegistry,
    private val notificationShadeWindowController: NotificationShadeWindowController,
    rippleView: AuthRippleView?
) : ViewController<AuthRippleView>(rippleView) {
    private var fingerprintSensorLocation: PointF? = null
    private var faceSensorLocation: PointF? = null

    @VisibleForTesting
    @VisibleForTesting
    var rippleView: AuthRippleView = AuthRippleView(context, attrs = null)
    public override fun onViewAttached() {
        updateRippleColor()
        updateSensorLocation()
        configurationController.addCallback(configurationChangedListener)
        keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
        commandRegistry.registerCommand("auth-ripple") { AuthRippleCommand() }
    }

    @VisibleForTesting
    public override fun onViewDetached() {
        keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
        configurationController.removeCallback(configurationChangedListener)
        commandRegistry.unregisterCommand("auth-ripple")

        notificationShadeWindowController.setForcePluginOpen(false, this)
    }

    private fun showRipple(biometricSourceType: BiometricSourceType?) {
        if (biometricSourceType == BiometricSourceType.FINGERPRINT &&
            fingerprintSensorLocation != null) {
            mView.setSensorLocation(fingerprintSensorLocation!!)
            showRipple()
        } else if (biometricSourceType == BiometricSourceType.FACE &&
            faceSensorLocation != null) {
            mView.setSensorLocation(faceSensorLocation!!)
            showRipple()
        }
    }

    private fun showRipple() {
        notificationShadeWindowController.setForcePluginOpen(true, this)
        mView.startRipple(Runnable {
            notificationShadeWindowController.setForcePluginOpen(false, this)
        })
    }

    private fun updateSensorLocation() {
        fingerprintSensorLocation = authController.udfpsSensorLocation
        faceSensorLocation = authController.faceAuthSensorLocation
    }

    private fun updateRippleColor() {
        mView.setColor(
            Utils.getColorAttr(sysuiContext, android.R.attr.colorAccent).defaultColor)
    }


    val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() {
    val keyguardUpdateMonitorCallback =
        object : KeyguardUpdateMonitorCallback() {
            override fun onBiometricAuthenticated(
            override fun onBiometricAuthenticated(
                userId: Int,
                userId: Int,
                biometricSourceType: BiometricSourceType?,
                biometricSourceType: BiometricSourceType?,
                isStrongBiometric: Boolean
                isStrongBiometric: Boolean
            ) {
            ) {
            if (biometricSourceType == BiometricSourceType.FINGERPRINT) {
                showRipple(biometricSourceType)
                rippleView.startRipple()
            }
            }
            }
    }
    }


    init {
    val configurationChangedListener =
        val configurationChangedListener = object : ConfigurationController.ConfigurationListener {
        object : ConfigurationController.ConfigurationListener {
            override fun onConfigChanged(newConfig: Configuration?) {
                updateSensorLocation()
            }
            override fun onUiModeChanged() {
            override fun onUiModeChanged() {
                updateRippleColor()
                updateRippleColor()
            }
            }
@@ -69,42 +123,49 @@ class AuthRippleController @Inject constructor(
                updateRippleColor()
                updateRippleColor()
            }
            }
    }
    }
        configurationController.addCallback(configurationChangedListener)


        commandRegistry.registerCommand("auth-ripple") { AuthRippleCommand() }
    inner class AuthRippleCommand : Command {
        override fun execute(pw: PrintWriter, args: List<String>) {
            if (args.isEmpty()) {
                invalidCommand(pw)
            } else {
                when (args[0]) {
                    "fingerprint" -> {
                        pw.println("fingerprint ripple sensorLocation=$fingerprintSensorLocation")
                        showRipple(BiometricSourceType.FINGERPRINT)
                    }
                    }

                    "face" -> {
    fun setSensorLocation(x: Float, y: Float) {
                        pw.println("face ripple sensorLocation=$faceSensorLocation")
        rippleView.setSensorLocation(x, y)
                        showRipple(BiometricSourceType.FACE)
                    }
                    }

                    "custom" -> {
    fun setViewHost(viewHost: View) {
                        if (args.size != 3 ||
        // Add the ripple view to its host layout
                            args[1].toFloatOrNull() == null ||
        viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
                            args[2].toFloatOrNull() == null) {
            override fun onViewDetachedFromWindow(view: View?) {}
                            invalidCommand(pw)

                            return
            override fun onViewAttachedToWindow(view: View?) {
                (viewHost as ViewGroup).addView(rippleView)
                keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
                viewHost.removeOnAttachStateChangeListener(this)
                        }
                        }
        })
                        pw.println("custom ripple sensorLocation=" + args[1].toFloat() + ", " +

                            args[2].toFloat())
        updateRippleColor()
                        mView.setSensorLocation(PointF(args[1].toFloat(), args[2].toFloat()))
                        showRipple()
                    }
                    else -> invalidCommand(pw)
                }
                }

    private fun updateRippleColor() {
        rippleView.setColor(
            Utils.getColorAttr(context, android.R.attr.colorAccent).defaultColor)
            }
            }

    inner class AuthRippleCommand : Command {
        override fun execute(pw: PrintWriter, args: List<String>) {
            rippleView.startRipple()
        }
        }


        override fun help(pw: PrintWriter) {
        override fun help(pw: PrintWriter) {
            pw.println("Usage: adb shell cmd statusbar auth-ripple")
            pw.println("Usage: adb shell cmd statusbar auth-ripple <command>")
            pw.println("Available commands:")
            pw.println("  fingerprint")
            pw.println("  face")
            pw.println("  custom <x-location: int> <y-location: int>")
        }

        fun invalidCommand(pw: PrintWriter) {
            pw.println("invalid command")
            help(pw)
        }
        }
    }
    }
}
}
Loading