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

Commit d9902e3c authored by Felipe Leme's avatar Felipe Leme
Browse files

Added unit tests for MasterClearReceiver.

Test: atest FrameworksMockingServicesTests:MasterClearReceiverTest
Bug: 171603586

Change-Id: Ib05af552d8a848be93fe4ba7198b90f4800f861b
parent 2923b4eb
Loading
Loading
Loading
Loading
+7 −2
Original line number Original line Diff line number Diff line
@@ -25,7 +25,6 @@ import android.os.RecoverySystem;
import android.os.UserHandle;
import android.os.UserHandle;
import android.os.storage.StorageManager;
import android.os.storage.StorageManager;
import android.text.TextUtils;
import android.text.TextUtils;
import android.util.Log;
import android.util.Slog;
import android.util.Slog;
import android.view.WindowManager;
import android.view.WindowManager;


@@ -59,6 +58,7 @@ public class MasterClearReceiver extends BroadcastReceiver {
                .getString(com.android.internal.R.string.config_factoryResetPackage);
                .getString(com.android.internal.R.string.config_factoryResetPackage);
        if (Intent.ACTION_FACTORY_RESET.equals(intent.getAction())
        if (Intent.ACTION_FACTORY_RESET.equals(intent.getAction())
                && !TextUtils.isEmpty(factoryResetPackage)) {
                && !TextUtils.isEmpty(factoryResetPackage)) {
            Slog.i(TAG, "Re-directing intent to " + factoryResetPackage);
            intent.setPackage(factoryResetPackage).setComponent(null);
            intent.setPackage(factoryResetPackage).setComponent(null);
            context.sendBroadcastAsUser(intent, UserHandle.SYSTEM);
            context.sendBroadcastAsUser(intent, UserHandle.SYSTEM);
            return;
            return;
@@ -77,9 +77,12 @@ public class MasterClearReceiver extends BroadcastReceiver {
            @Override
            @Override
            public void run() {
            public void run() {
                try {
                try {
                    Slog.i(TAG, "Calling RecoverySystem.rebootWipeUserData(context, "
                            + "shutdown=" + shutdown + ", reason=" + reason
                            + ", forceWipe=" + forceWipe + ", wipeEsims=" + mWipeEsims + ")");
                    RecoverySystem
                    RecoverySystem
                            .rebootWipeUserData(context, shutdown, reason, forceWipe, mWipeEsims);
                            .rebootWipeUserData(context, shutdown, reason, forceWipe, mWipeEsims);
                    Log.wtf(TAG, "Still running after master clear?!");
                    Slog.wtf(TAG, "Still running after master clear?!");
                } catch (IOException e) {
                } catch (IOException e) {
                    Slog.e(TAG, "Can't perform master clear/factory reset", e);
                    Slog.e(TAG, "Can't perform master clear/factory reset", e);
                } catch (SecurityException e) {
                } catch (SecurityException e) {
@@ -90,8 +93,10 @@ public class MasterClearReceiver extends BroadcastReceiver {


        if (mWipeExternalStorage) {
        if (mWipeExternalStorage) {
            // thr will be started at the end of this task.
            // thr will be started at the end of this task.
            Slog.i(TAG, "Wiping external storage on async task");
            new WipeDataTask(context, thr).execute();
            new WipeDataTask(context, thr).execute();
        } else {
        } else {
            Slog.i(TAG, "NOT wiping external storage; starting thread " + thr.getName());
            thr.start();
            thr.start();
        }
        }
    }
    }
+3 −0
Original line number Original line Diff line number Diff line
@@ -27,6 +27,9 @@
    <uses-permission android:name="android.permission.MANAGE_APPOPS"/>
    <uses-permission android:name="android.permission.MANAGE_APPOPS"/>
    <uses-permission android:name="android.permission.MONITOR_DEVICE_CONFIG_ACCESS"/>
    <uses-permission android:name="android.permission.MONITOR_DEVICE_CONFIG_ACCESS"/>


    <!-- needed by MasterClearReceiverTest to display a system dialog -->
    <uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW"/>

    <application android:testOnly="true"
    <application android:testOnly="true"
                 android:debuggable="true">
                 android:debuggable="true">
        <uses-library android:name="android.test.runner" />
        <uses-library android:name="android.test.runner" />
+210 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2020 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;

import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;

import static com.google.common.truth.Truth.assertWithMessage;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.never;

import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.os.Looper;
import android.os.RecoverySystem;
import android.os.storage.StorageManager;
import android.platform.test.annotations.Presubmit;
import android.util.Log;
import android.view.WindowManager;

import androidx.test.InstrumentationRegistry;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoSession;
import org.mockito.quality.Strictness;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * Run it as {@code FrameworksMockingServicesTests:MasterClearReceiverTest}.
 */
@Presubmit
public final class MasterClearReceiverTest {

    private static final String TAG = MasterClearReceiverTest.class.getSimpleName();

    private MockitoSession mSession;

    // Cannot @Mock context because MasterClearReceiver shows an AlertDialog, which relies
    // on resources - we'd need to mock them as well.
    private final Context mContext = new ContextWrapper(
            InstrumentationRegistry.getInstrumentation().getTargetContext()) {

        @Override
        public Object getSystemService(String name) {
            Log.v(TAG, "getSystemService(): " + name);
            return name.equals(Context.STORAGE_SERVICE) ? mSm : super.getSystemService(name);
        }
    };

    private final MasterClearReceiver mReceiver = new MasterClearReceiver();

    // Used to make sure that wipeAdoptableDisks() is called before rebootWipeUserData()
    private boolean mWipeExternalDataCalled;

    // Uset to block test until rebootWipeUserData() is called, as it might be asynchronous called
    // in a different thread
    private final CountDownLatch mRebootWipeUserDataLatch = new CountDownLatch(1);

    @Mock
    private StorageManager mSm;

    @Mock
    private WindowManager mWm;

    @Before
    public void startSession() {
        mSession = mockitoSession()
                .initMocks(this)
                .mockStatic(RecoverySystem.class)
                .strictness(Strictness.LENIENT)
                .startMocking();
    }

    @After
    public void finishSession() {
        if (mSession == null) {
            Log.w(TAG, "finishSession(): no session");
            return;
        }
        mSession.finishMocking();
    }

    @Test
    public void testNoExtras() throws Exception {
        expectNoWipeExternalData();
        expectRebootWipeUserData();

        Intent intent = new Intent(Intent.ACTION_FACTORY_RESET);
        mReceiver.onReceive(mContext, intent);

        verifyRebootWipeUserData();
        verifyNoWipeExternalData();
    }

    @Test
    public void testWipeExternalDirectory() throws Exception {
        expectWipeExternalData();
        expectRebootWipeUserData();

        Intent intent = new Intent(Intent.ACTION_FACTORY_RESET);
        intent.putExtra(Intent.EXTRA_WIPE_EXTERNAL_STORAGE, true);
        mReceiver.onReceive(mContext, intent);

        verifyRebootWipeUserData();
        verifyWipeExternalData();
    }

    @Test
    public void testAllExtras() throws Exception {
        expectWipeExternalData();
        expectRebootWipeUserData();

        Intent intent = new Intent(Intent.ACTION_FACTORY_RESET);
        intent.putExtra(Intent.EXTRA_WIPE_EXTERNAL_STORAGE, true);
        intent.putExtra("shutdown", true);
        intent.putExtra(Intent.EXTRA_REASON, "Self destruct");
        intent.putExtra(Intent.EXTRA_FORCE_FACTORY_RESET, true);
        intent.putExtra(Intent.EXTRA_WIPE_ESIMS, true);
        mReceiver.onReceive(mContext, intent);

        verifyRebootWipeUserData(/* shutdown= */ true, /* reason= */ "Self destruct",
                /* force= */ true, /* wipeEuicc= */ true);
        verifyWipeExternalData();
    }


    private void expectNoWipeExternalData() {
        // This is a trick to simplify how the order of methods are called: as wipeAdoptableDisks()
        // should be called before rebootWipeUserData(), expectRebootWipeUserData() throws an
        // exception if it's not called, so this method "emulates" a call when it's not neeeded.
        //
        // A more robust solution would be using internal counters for expected and actual mocked
        // calls, so the expectXXX() methods would increment expected counter and the Answer
        // implementations would increment the actual counter and check if they match, but that
        // would be an overkill (and make the test logic more complicated).
        mWipeExternalDataCalled = true;
    }

    private void expectRebootWipeUserData() {
        doAnswer((inv) -> {
            Log.i(TAG, inv.toString());
            if (!mWipeExternalDataCalled) {
                String error = "rebootWipeUserData() called before wipeAdoptableDisks()";
                Log.e(TAG, error);
                throw new IllegalStateException(error);
            }
            mRebootWipeUserDataLatch.countDown();
            return null;
        }).when(() -> RecoverySystem
                .rebootWipeUserData(any(), anyBoolean(), any(), anyBoolean(), anyBoolean()));
    }

    private void expectWipeExternalData() {
        Looper.prepare(); // needed by Dialog

        doAnswer((inv) -> {
            Log.i(TAG, inv.toString());
            mWipeExternalDataCalled = true;
            return null;
        }).when(mSm).wipeAdoptableDisks();
    }

    private void verifyRebootWipeUserData() throws Exception  {
        verifyRebootWipeUserData(/* shutdown= */ false, /* reason= */ null, /* force= */ false,
                /* wipeEuicc= */ false);

    }

    private void verifyRebootWipeUserData(boolean shutdown, String reason, boolean force,
            boolean wipeEuicc) throws Exception {
        boolean called = mRebootWipeUserDataLatch.await(5, TimeUnit.SECONDS);
        assertWithMessage("rebootWipeUserData not called in 5s").that(called).isTrue();

        verify(()-> RecoverySystem.rebootWipeUserData(same(mContext), eq(shutdown), eq(reason),
                eq(force), eq(wipeEuicc)));
    }

    private void verifyWipeExternalData() {
        verify(mSm).wipeAdoptableDisks();
    }

    private void verifyNoWipeExternalData() {
        verify(mSm, never()).wipeAdoptableDisks();
    }
}