/* * Copyright (C) 2015 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 android.car.extendedapitest.testbase; import static com.android.compatibility.common.util.ShellUtils.runShellCommand; import static com.android.compatibility.common.util.TestUtils.BooleanSupplierWithThrow; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assume.assumeFalse; import android.annotation.NonNull; import android.app.ActivityManager; import android.app.UiAutomation; import android.car.Car; import android.car.test.AbstractExpectableTestCase; import android.content.ComponentName; import android.content.Context; import android.content.ServiceConnection; import android.hardware.automotive.vehicle.InitialUserInfoRequestType; import android.os.Build; import android.os.IBinder; import android.os.Looper; import android.os.ParcelFileDescriptor; import android.os.PowerManager; import android.os.SystemClock; import android.util.Log; import androidx.test.platform.app.InstrumentationRegistry; import org.junit.After; import org.junit.Before; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Collection; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; /** * Base class for tests that don't need to connect to a {@link Car} object. * *

For tests that don't need a {@link Car} object, use * {@link CarLessApiTestBase} instead. */ public abstract class CarApiTestBase extends AbstractExpectableTestCase { private static final String TAG = CarApiTestBase.class.getSimpleName(); protected static final long DEFAULT_WAIT_TIMEOUT_MS = 120_000; /** * Constant used to wait blindly, when there is no condition that can be checked. */ private static final int SUSPEND_TIMEOUT_MS = 5_000; /** * How long to sleep (multiple times) while waiting for a condition. */ private static final int SMALL_NAP_MS = 100; private final ReceiverTrackingContext mContext = new ReceiverTrackingContext( InstrumentationRegistry.getInstrumentation().getTargetContext()); private Car mCar; protected final DefaultServiceConnectionListener mConnectionListener = new DefaultServiceConnectionListener(); // TODO(b/242350638): temporary hack to allow subclasses to disable checks - should be removed // when not needed anymore @Before public final void setFixturesAndConnectToCar() throws Exception { Log.d(TAG, "setFixturesAndConnectToCar() for " + getTestName()); mCar = Car.createCar(getContext(), mConnectionListener); mCar.connect(); mConnectionListener.waitForConnection(DEFAULT_WAIT_TIMEOUT_MS); } @Before public final void dontStopUserOnSwitch() throws Exception { Log.d(TAG, "Calling am.setStopUserOnSwitch(false) for " + getTestName()); getContext().getSystemService(ActivityManager.class) .setStopUserOnSwitch(ActivityManager.STOP_USER_ON_SWITCH_FALSE); } @After public final void disconnectCar() throws Exception { if (mCar == null) { Log.wtf(TAG, "no mCar on " + getTestName() + ".tearDown()"); return; } mCar.disconnect(); } @After public final void resetStopUserOnSwitch() throws Exception { Log.d(TAG, "Calling am.setStopUserOnSwitch(default) for " + getTestName()); getContext().getSystemService(ActivityManager.class) .setStopUserOnSwitch(ActivityManager.STOP_USER_ON_SWITCH_DEFAULT); } @After public final void checkReceiversUnregisters() { Log.d(TAG, "Checking if all receivers were unregistered."); Collection receivers = mContext.getReceiversInfo(); // Remove all registered receivers to prevent affecting future test cases. mContext.clearReceivers(); assertWithMessage("Broadcast receivers that are not unregistered: %s", receivers) .that(receivers).isEmpty(); } protected Car getCar() { return mCar; } protected final Context getContext() { return mContext; } @SuppressWarnings("TypeParameterUnusedInFormals") // error prone complains about returning protected final T getCarService(@NonNull String serviceName) { assertThat(serviceName).isNotNull(); Object service = mCar.getCarManager(serviceName); assertWithMessage("Could not get service %s", serviceName).that(service).isNotNull(); @SuppressWarnings("unchecked") T castService = (T) service; return castService; } /** * Asserts that the current thread is the main thread. */ public static void assertMainThread() { assertThat(Looper.getMainLooper().isCurrentThread()).isTrue(); } public static final class DefaultServiceConnectionListener implements ServiceConnection { private final Semaphore mConnectionWait = new Semaphore(0); /** * Waits for a connection to become available. * *

This method blocks until a connection is available or the timeout expires. * * @param timeoutMs the maximum time to wait in milliseconds * @throws InterruptedException if the current thread is interrupted while waiting */ public void waitForConnection(long timeoutMs) throws InterruptedException { mConnectionWait.tryAcquire(timeoutMs, TimeUnit.MILLISECONDS); } @Override public void onServiceConnected(ComponentName name, IBinder service) { assertMainThread(); mConnectionWait.release(); } @Override public void onServiceDisconnected(ComponentName name) { assertMainThread(); fail("Car service crashed"); } } protected void suspendToRamAndResume() throws Exception { Log.d(TAG, "Emulate suspend to RAM and resume"); try { Log.d(TAG, "Disabling background users starting on garage mode"); runShellCommand("cmd car_service set-start-bg-users-on-garage-mode false"); PowerManager powerManager = mContext.getSystemService(PowerManager.class); // clear log runShellCommand("logcat -b all -c"); // We use a simulated suspend because physically suspended devices cannot be woken up by // a shell command. runShellCommand("cmd car_service suspend --simulate --skip-garagemode " + "--wakeup-after 3"); // Check for suspend success waitUntil("screen is still on after suspend", SUSPEND_TIMEOUT_MS, () -> !powerManager.isScreenOn()); // The device will resume after 3 seconds. waitForLogcatMessage("logcat -b events", "car_user_svc_initial_user_info_req_complete: " + InitialUserInfoRequestType.RESUME, 60_000); } catch (Exception e) { runShellCommand("cmd car_service set-start-bg-users-on-garage-mode true"); } } /** * Wait for a particular logcat message. * * @param cmd is logcat command with buffer or filters * @param match is the string to be found in the log * @param timeoutMs for which call should wait for the match */ protected static void waitForLogcatMessage(String cmd, String match, int timeoutMs) { Log.d(TAG, "waiting for logcat match: " + match); long startTime = SystemClock.elapsedRealtime(); UiAutomation automation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); ParcelFileDescriptor output = automation.executeShellCommand(cmd); FileDescriptor fd = output.getFileDescriptor(); FileInputStream fileInputStream = new FileInputStream(fd); try (BufferedReader bufferedReader = new BufferedReader( new InputStreamReader(fileInputStream))) { String line; while ((line = bufferedReader.readLine()) != null) { if (line.contains(match)) { Log.d(TAG, "match found in " + (SystemClock.elapsedRealtime() - startTime) + " ms"); break; } if ((SystemClock.elapsedRealtime() - startTime) > timeoutMs) { fail("logcat message(%s) not found in %d ms", match, timeoutMs); } } } catch (IOException e) { fail("match (%s) was not found, IO exception: %s", match, e); } } protected static boolean waitUntil(String msg, long timeoutMs, BooleanSupplierWithThrow condition) { long deadline = SystemClock.elapsedRealtime() + timeoutMs; do { try { if (condition.getAsBoolean()) { return true; } } catch (Exception e) { Log.e(TAG, "Exception in waitUntil: " + msg); throw new RuntimeException(e); } SystemClock.sleep(SMALL_NAP_MS); } while (SystemClock.elapsedRealtime() < deadline); fail("%s after %d ms", msg, timeoutMs); return false; } // TODO(b/250914846): Clean this up once the investigation is done. // Same as waitUntil except for not failing the test. protected static boolean waitUntilNoFail(long timeoutMs, BooleanSupplierWithThrow condition) { long deadline = SystemClock.elapsedRealtime() + timeoutMs; do { try { if (condition.getAsBoolean()) { return true; } } catch (Exception e) { Log.e(TAG, "Exception in waitUntilNoFail"); throw new RuntimeException(e); } SystemClock.sleep(SMALL_NAP_MS); } while (SystemClock.elapsedRealtime() < deadline); return false; } protected void requireNonUserBuild() { assumeFalse("Requires Shell commands that are not available on user builds", Build.IS_USER); } protected String getTestName() { // TODO(b/300964422): Add a way to get testName if possible. return ""; } protected static void fail(String format, Object...args) { String message = String.format(format, args); Log.e(TAG, "test failed: " + message); org.junit.Assert.fail(message); } protected static String executeShellCommand(String commandFormat, Object... args) throws IOException { UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); return executeShellCommand(uiAutomation, commandFormat, args); } private static String executeShellCommand(UiAutomation uiAutomation, String commandFormat, Object... args) throws IOException { ParcelFileDescriptor stdout = uiAutomation.executeShellCommand( String.format(commandFormat, args)); try (InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(stdout)) { ByteArrayOutputStream result = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int length; while ((length = inputStream.read(buffer)) != -1) { result.write(buffer, 0, length); } return result.toString("UTF-8"); } } }