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

Commit d7c679ae authored by Peter Kalauskas's avatar Peter Kalauskas Committed by Android (Google) Code Review
Browse files

Merge "Add tests for guest user metrics"

parents 790cc2b6 83deb23a
Loading
Loading
Loading
Loading
+36 −31
Original line number Diff line number Diff line
@@ -17,9 +17,8 @@
package com.android.systemui;

import android.app.ActivityManager;
import android.app.Dialog;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
@@ -28,14 +27,16 @@ import android.content.pm.UserInfo;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.util.Log;
import android.view.WindowManagerGlobal;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.qs.QSUserSwitcherEvent;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.util.settings.SecureSettings;

/**
 * Manages notification when a guest session is resumed.
@@ -44,13 +45,20 @@ public class GuestResumeSessionReceiver extends BroadcastReceiver {

    private static final String TAG = "GuestResumeSessionReceiver";

    private static final String SETTING_GUEST_HAS_LOGGED_IN = "systemui.guest_has_logged_in";
    @VisibleForTesting
    public static final String SETTING_GUEST_HAS_LOGGED_IN = "systemui.guest_has_logged_in";

    private Dialog mNewSessionDialog;
    @VisibleForTesting
    public AlertDialog mNewSessionDialog;
    private final UserTracker mUserTracker;
    private final UiEventLogger mUiEventLogger;
    private final SecureSettings mSecureSettings;

    public GuestResumeSessionReceiver(UiEventLogger uiEventLogger) {
    public GuestResumeSessionReceiver(UserTracker userTracker, UiEventLogger uiEventLogger,
            SecureSettings secureSettings) {
        mUserTracker = userTracker;
        mUiEventLogger = uiEventLogger;
        mSecureSettings = secureSettings;
    }

    /**
@@ -76,25 +84,19 @@ public class GuestResumeSessionReceiver extends BroadcastReceiver {
                return;
            }

            UserInfo currentUser;
            try {
                currentUser = ActivityManager.getService().getCurrentUser();
            } catch (RemoteException e) {
                return;
            }
            UserInfo currentUser = mUserTracker.getUserInfo();
            if (!currentUser.isGuest()) {
                return;
            }

            ContentResolver cr = context.getContentResolver();
            int notFirstLogin = Settings.System.getIntForUser(
                    cr, SETTING_GUEST_HAS_LOGGED_IN, 0, userId);
            int notFirstLogin = mSecureSettings.getIntForUser(
                    SETTING_GUEST_HAS_LOGGED_IN, 0, userId);
            if (notFirstLogin != 0) {
                mNewSessionDialog = new ResetSessionDialog(context, mUiEventLogger, userId);
                mNewSessionDialog = new ResetSessionDialog(context, mUserTracker, mUiEventLogger,
                        userId);
                mNewSessionDialog.show();
            } else {
                Settings.System.putIntForUser(
                        cr, SETTING_GUEST_HAS_LOGGED_IN, 1, userId);
                mSecureSettings.putIntForUser(SETTING_GUEST_HAS_LOGGED_IN, 1, userId);
            }
        }
    }
@@ -104,15 +106,9 @@ public class GuestResumeSessionReceiver extends BroadcastReceiver {
     *
     * The guest must be the current user and its id must be {@param userId}.
     */
    private static void wipeGuestSession(Context context, int userId) {
    private static void wipeGuestSession(Context context, UserTracker userTracker, int userId) {
        UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
        UserInfo currentUser;
        try {
            currentUser = ActivityManager.getService().getCurrentUser();
        } catch (RemoteException e) {
            Log.e(TAG, "Couldn't wipe session because ActivityManager is dead");
            return;
        }
        UserInfo currentUser = userTracker.getUserInfo();
        if (currentUser.id != userId) {
            Log.w(TAG, "User requesting to start a new session (" + userId + ")"
                    + " is not current user (" + currentUser.id + ")");
@@ -154,16 +150,24 @@ public class GuestResumeSessionReceiver extends BroadcastReceiver {
        }
    }

    private static class ResetSessionDialog extends SystemUIDialog implements
    /**
     * Dialog shown when user when asking for confirmation before deleting guest user.
     */
    @VisibleForTesting
    public static class ResetSessionDialog extends SystemUIDialog implements
            DialogInterface.OnClickListener {

        private static final int BUTTON_WIPE = BUTTON_NEGATIVE;
        private static final int BUTTON_DONTWIPE = BUTTON_POSITIVE;
        @VisibleForTesting
        public static final int BUTTON_WIPE = BUTTON_NEGATIVE;
        @VisibleForTesting
        public static final int BUTTON_DONTWIPE = BUTTON_POSITIVE;

        private final UserTracker mUserTracker;
        private final UiEventLogger mUiEventLogger;
        private final int mUserId;

        ResetSessionDialog(Context context, UiEventLogger uiEventLogger, int userId) {
        ResetSessionDialog(Context context, UserTracker userTracker, UiEventLogger uiEventLogger,
                int userId) {
            super(context);

            setTitle(context.getString(R.string.guest_wipe_session_title));
@@ -175,6 +179,7 @@ public class GuestResumeSessionReceiver extends BroadcastReceiver {
            setButton(BUTTON_DONTWIPE,
                    context.getString(R.string.guest_wipe_session_dontwipe), this);

            mUserTracker = userTracker;
            mUiEventLogger = uiEventLogger;
            mUserId = userId;
        }
@@ -183,7 +188,7 @@ public class GuestResumeSessionReceiver extends BroadcastReceiver {
        public void onClick(DialogInterface dialog, int which) {
            if (which == BUTTON_WIPE) {
                mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_WIPE);
                wipeGuestSession(getContext(), mUserId);
                wipeGuestSession(getContext(), mUserTracker, mUserId);
                dismiss();
            } else if (which == BUTTON_DONTWIPE) {
                mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_CONTINUE);
+38 −25
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import static com.android.systemui.DejankUtils.whitelistIpcs;

import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.IActivityTaskManager;
import android.content.BroadcastReceiver;
@@ -67,9 +68,11 @@ import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.qs.DetailAdapter;
import com.android.systemui.qs.QSUserSwitcherEvent;
import com.android.systemui.qs.tiles.UserDetailView;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.telephony.TelephonyListenerManager;
import com.android.systemui.user.CreateUserActivity;
import com.android.systemui.util.settings.SecureSettings;

import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -98,9 +101,12 @@ public class UserSwitcherController implements Dumpable {
    private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";

    protected final Context mContext;
    protected final UserTracker mUserTracker;
    protected final UserManager mUserManager;
    private final ContentObserver mSettingsObserver;
    private final ArrayList<WeakReference<BaseUserAdapter>> mAdapters = new ArrayList<>();
    private final GuestResumeSessionReceiver mGuestResumeSessionReceiver;
    @VisibleForTesting
    final GuestResumeSessionReceiver mGuestResumeSessionReceiver;
    private final KeyguardStateController mKeyguardStateController;
    protected final Handler mHandler;
    private final ActivityStarter mActivityStarter;
@@ -109,8 +115,10 @@ public class UserSwitcherController implements Dumpable {
    private final IActivityTaskManager mActivityTaskManager;

    private ArrayList<UserRecord> mUsers = new ArrayList<>();
    private Dialog mExitGuestDialog;
    private Dialog mAddUserDialog;
    @VisibleForTesting
    AlertDialog mExitGuestDialog;
    @VisibleForTesting
    Dialog mAddUserDialog;
    private int mLastNonGuestUser = UserHandle.USER_SYSTEM;
    private boolean mResumeUserOnGuestLogout = true;
    private boolean mSimpleUserSwitcher;
@@ -125,17 +133,21 @@ public class UserSwitcherController implements Dumpable {
    public final DetailAdapter mUserDetailAdapter;

    @Inject
    public UserSwitcherController(Context context, KeyguardStateController keyguardStateController,
    public UserSwitcherController(Context context, UserManager userManager, UserTracker userTracker,
            KeyguardStateController keyguardStateController,
            @Main Handler handler, ActivityStarter activityStarter,
            BroadcastDispatcher broadcastDispatcher, UiEventLogger uiEventLogger,
            TelephonyListenerManager telephonyListenerManager,
            IActivityTaskManager activityTaskManager, UserDetailAdapter userDetailAdapter) {
            IActivityTaskManager activityTaskManager, UserDetailAdapter userDetailAdapter,
            SecureSettings secureSettings) {
        mContext = context;
        mUserTracker = userTracker;
        mBroadcastDispatcher = broadcastDispatcher;
        mTelephonyListenerManager = telephonyListenerManager;
        mActivityTaskManager = activityTaskManager;
        mUiEventLogger = uiEventLogger;
        mGuestResumeSessionReceiver = new GuestResumeSessionReceiver(mUiEventLogger);
        mGuestResumeSessionReceiver = new GuestResumeSessionReceiver(
                mUserTracker, mUiEventLogger, secureSettings);
        mUserDetailAdapter = userDetailAdapter;
        if (!UserManager.isGuestUserEphemeral()) {
            mGuestResumeSessionReceiver.register(mBroadcastDispatcher);
@@ -143,7 +155,7 @@ public class UserSwitcherController implements Dumpable {
        mKeyguardStateController = keyguardStateController;
        mHandler = handler;
        mActivityStarter = activityStarter;
        mUserManager = UserManager.get(context);
        mUserManager = userManager;
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_USER_ADDED);
        filter.addAction(Intent.ACTION_USER_REMOVED);
@@ -162,6 +174,14 @@ public class UserSwitcherController implements Dumpable {
        mContext.registerReceiverAsUser(mReceiver, UserHandle.SYSTEM, filter,
                PERMISSION_SELF, null /* scheduler */);

        mSettingsObserver = new ContentObserver(mHandler) {
            public void onChange(boolean selfChange) {
                mSimpleUserSwitcher = shouldUseSimpleUserSwitcher();
                mAddUsersFromLockScreen = Settings.Global.getInt(mContext.getContentResolver(),
                        Settings.Global.ADD_USERS_WHEN_LOCKED, 0) != 0;
                refreshUsers(UserHandle.USER_NULL);
            };
        };
        mContext.getContentResolver().registerContentObserver(
                Settings.Global.getUriFor(SIMPLE_USER_SWITCHER_GLOBAL_SETTING), true,
                mSettingsObserver);
@@ -223,11 +243,11 @@ public class UserSwitcherController implements Dumpable {
                    return null;
                }
                ArrayList<UserRecord> records = new ArrayList<>(infos.size());
                int currentId = ActivityManager.getCurrentUser();
                int currentId = mUserTracker.getUserId();
                // Check user switchability of the foreground user since SystemUI is running in
                // User 0
                boolean canSwitchUsers = mUserManager.getUserSwitchability(
                        UserHandle.of(ActivityManager.getCurrentUser())) == SWITCHABILITY_STATUS_OK;
                        UserHandle.of(mUserTracker.getUserId())) == SWITCHABILITY_STATUS_OK;
                UserInfo currentUserInfo = null;
                UserRecord guestRecord = null;

@@ -355,7 +375,7 @@ public class UserSwitcherController implements Dumpable {
    }

    public void logoutCurrentUser() {
        int currentUser = ActivityManager.getCurrentUser();
        int currentUser = mUserTracker.getUserId();
        if (currentUser != UserHandle.USER_SYSTEM) {
            pauseRefreshUsers();
            ActivityManager.logoutCurrentUser();
@@ -367,7 +387,7 @@ public class UserSwitcherController implements Dumpable {
            Log.w(TAG, "User " + userId + " could not removed.");
            return;
        }
        if (ActivityManager.getCurrentUser() == userId) {
        if (mUserTracker.getUserId() == userId) {
            switchToUserId(UserHandle.USER_SYSTEM);
        }
        if (mUserManager.removeUser(userId)) {
@@ -375,7 +395,8 @@ public class UserSwitcherController implements Dumpable {
        }
    }

    private void onUserListItemClicked(UserRecord record) {
    @VisibleForTesting
    void onUserListItemClicked(UserRecord record) {
        int id;
        if (record.isGuest && record.info == null) {
            // No guest user. Create one.
@@ -401,7 +422,7 @@ public class UserSwitcherController implements Dumpable {
            id = record.info.id;
        }

        int currUserId = ActivityManager.getCurrentUser();
        int currUserId = mUserTracker.getUserId();
        if (currUserId == id) {
            if (record.isGuest) {
                showExitGuestDialog(id);
@@ -557,15 +578,6 @@ public class UserSwitcherController implements Dumpable {
        }
    };

    private final ContentObserver mSettingsObserver = new ContentObserver(new Handler()) {
        public void onChange(boolean selfChange) {
            mSimpleUserSwitcher = shouldUseSimpleUserSwitcher();
            mAddUsersFromLockScreen = Settings.Global.getInt(mContext.getContentResolver(),
                    Settings.Global.ADD_USERS_WHEN_LOCKED, 0) != 0;
            refreshUsers(UserHandle.USER_NULL);
        };
    };

    @Override
    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        pw.println("UserSwitcherController state:");
@@ -701,9 +713,9 @@ public class UserSwitcherController implements Dumpable {

    private void checkIfAddUserDisallowedByAdminOnly(UserRecord record) {
        EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext,
                UserManager.DISALLOW_ADD_USER, ActivityManager.getCurrentUser());
                UserManager.DISALLOW_ADD_USER, mUserTracker.getUserId());
        if (admin != null && !RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext,
                UserManager.DISALLOW_ADD_USER, ActivityManager.getCurrentUser())) {
                UserManager.DISALLOW_ADD_USER, mUserTracker.getUserId())) {
            record.isDisabledByAdmin = true;
            record.enforcedAdmin = admin;
        } else {
@@ -906,7 +918,8 @@ public class UserSwitcherController implements Dumpable {
        }
    }

    private final class AddUserDialog extends SystemUIDialog implements
    @VisibleForTesting
    final class AddUserDialog extends SystemUIDialog implements
            DialogInterface.OnClickListener {

        public AddUserDialog(Context context) {
+220 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.systemui.statusbar.policy

import android.app.IActivityTaskManager
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.UserInfo
import android.graphics.Bitmap
import android.os.Handler
import android.os.UserManager
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.internal.util.UserIcons
import com.android.systemui.GuestResumeSessionReceiver
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.qs.QSUserSwitcherEvent
import com.android.systemui.settings.UserTracker
import com.android.systemui.telephony.TelephonyListenerManager
import com.android.systemui.util.settings.SecureSettings
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito.any
import org.mockito.Mockito.anyString
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations

@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@SmallTest
class UserSwitcherControllerTest : SysuiTestCase() {
    @Mock private lateinit var keyguardStateController: KeyguardStateController
    @Mock private lateinit var handler: Handler
    @Mock private lateinit var userTracker: UserTracker
    @Mock private lateinit var userManager: UserManager
    @Mock private lateinit var activityStarter: ActivityStarter
    @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
    @Mock private lateinit var activityTaskManager: IActivityTaskManager
    @Mock private lateinit var userDetailAdapter: UserSwitcherController.UserDetailAdapter
    @Mock private lateinit var telephonyListenerManager: TelephonyListenerManager
    @Mock private lateinit var userInfo: UserInfo
    @Mock private lateinit var secureSettings: SecureSettings
    private lateinit var testableLooper: TestableLooper
    private lateinit var userSwitcherController: UserSwitcherController
    private lateinit var uiEventLogger: UiEventLoggerFake
    private lateinit var picture: Bitmap
    private val guestId = 1234

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        testableLooper = TestableLooper.get(this)
        uiEventLogger = UiEventLoggerFake()

        userSwitcherController = UserSwitcherController(context,
                userManager,
                userTracker,
                keyguardStateController,
                handler,
                activityStarter,
                broadcastDispatcher,
                uiEventLogger,
                telephonyListenerManager,
                activityTaskManager,
                userDetailAdapter,
                secureSettings)
        picture = UserIcons.convertToBitmap(context.getDrawable(R.drawable.ic_avatar_user))
    }

    @Test
    fun testAddGuest_okButtonPressed_isLogged() {
        val emptyGuestUserRecord = UserSwitcherController.UserRecord(
                null,
                null,
                true /* guest */,
                false /* current */,
                false /* isAddUser */,
                false /* isRestricted */,
                true /* isSwitchToEnabled */)

        `when`(userManager.createGuest(any(), anyString())).thenReturn(userInfo)

        userSwitcherController.onUserListItemClicked(emptyGuestUserRecord)
        assertEquals(1, uiEventLogger.numLogs())
        assertEquals(QSUserSwitcherEvent.QS_USER_GUEST_ADD.id, uiEventLogger.eventId(0))
    }

    @Test
    fun testRemoveGuest_removeButtonPressed_isLogged() {
        val currentGuestUserRecord = UserSwitcherController.UserRecord(
                userInfo,
                picture,
                true /* guest */,
                true /* current */,
                false /* isAddUser */,
                false /* isRestricted */,
                true /* isSwitchToEnabled */)

        userSwitcherController.onUserListItemClicked(currentGuestUserRecord)
        assertNotNull(userSwitcherController.mExitGuestDialog)
        userSwitcherController.mExitGuestDialog
                .getButton(DialogInterface.BUTTON_POSITIVE).performClick()
        testableLooper.processAllMessages()
        assertEquals(1, uiEventLogger.numLogs())
        assertEquals(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE.id, uiEventLogger.eventId(0))
    }

    @Test
    fun testRemoveGuest_cancelButtonPressed_isNotLogged() {
        val currentGuestUserRecord = UserSwitcherController.UserRecord(
                userInfo,
                picture,
                true /* guest */,
                true /* current */,
                false /* isAddUser */,
                false /* isRestricted */,
                true /* isSwitchToEnabled */)

        userSwitcherController.onUserListItemClicked(currentGuestUserRecord)
        assertNotNull(userSwitcherController.mExitGuestDialog)
        userSwitcherController.mExitGuestDialog
                .getButton(DialogInterface.BUTTON_NEGATIVE).performClick()
        testableLooper.processAllMessages()
        assertEquals(0, uiEventLogger.numLogs())
    }

    @Test
    fun testWipeGuest_startOverButtonPressed_isLogged() {
        val guestInfo = UserInfo(guestId, null, null, 0, UserManager.USER_TYPE_FULL_GUEST)
        val currentGuestUserRecord = UserSwitcherController.UserRecord(
                guestInfo,
                picture,
                true /* guest */,
                false /* current */,
                false /* isAddUser */,
                false /* isRestricted */,
                true /* isSwitchToEnabled */)

        // Simulate that guest user has already logged in
        `when`(secureSettings.getIntForUser(
                eq(GuestResumeSessionReceiver.SETTING_GUEST_HAS_LOGGED_IN), anyInt(), anyInt()))
                .thenReturn(1)

        userSwitcherController.onUserListItemClicked(currentGuestUserRecord)

        // Simulate a user switch event
        val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, guestId)
        `when`(userTracker.userInfo).thenReturn(guestInfo)

        assertNotNull(userSwitcherController.mGuestResumeSessionReceiver)
        userSwitcherController.mGuestResumeSessionReceiver.onReceive(context, intent)

        assertNotNull(userSwitcherController.mGuestResumeSessionReceiver.mNewSessionDialog)
        userSwitcherController.mGuestResumeSessionReceiver.mNewSessionDialog
                .getButton(GuestResumeSessionReceiver.ResetSessionDialog.BUTTON_WIPE).performClick()
        testableLooper.processAllMessages()
        assertEquals(1, uiEventLogger.numLogs())
        assertEquals(QSUserSwitcherEvent.QS_USER_GUEST_WIPE.id, uiEventLogger.eventId(0))
    }

    @Test
    fun testWipeGuest_continueButtonPressed_isLogged() {
        val guestInfo = UserInfo(guestId, null, null, 0, UserManager.USER_TYPE_FULL_GUEST)
        val currentGuestUserRecord = UserSwitcherController.UserRecord(
                guestInfo,
                picture,
                true /* guest */,
                false /* current */,
                false /* isAddUser */,
                false /* isRestricted */,
                true /* isSwitchToEnabled */)

        // Simulate that guest user has already logged in
        `when`(secureSettings.getIntForUser(
                eq(GuestResumeSessionReceiver.SETTING_GUEST_HAS_LOGGED_IN), anyInt(), anyInt()))
                .thenReturn(1)

        userSwitcherController.onUserListItemClicked(currentGuestUserRecord)

        // Simulate a user switch event
        val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, guestId)
        `when`(userTracker.userInfo).thenReturn(guestInfo)

        assertNotNull(userSwitcherController.mGuestResumeSessionReceiver)
        userSwitcherController.mGuestResumeSessionReceiver.onReceive(context, intent)

        assertNotNull(userSwitcherController.mGuestResumeSessionReceiver.mNewSessionDialog)
        userSwitcherController.mGuestResumeSessionReceiver.mNewSessionDialog
                .getButton(GuestResumeSessionReceiver.ResetSessionDialog.BUTTON_DONTWIPE)
                .performClick()
        testableLooper.processAllMessages()
        assertEquals(1, uiEventLogger.numLogs())
        assertEquals(QSUserSwitcherEvent.QS_USER_GUEST_CONTINUE.id, uiEventLogger.eventId(0))
    }
}