RequiresDeviceModeFilter.kt

/*
 * 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 androidx.test.espresso.device.filter

import android.os.Build
import android.util.Log
import androidx.test.espresso.device.controller.DeviceMode
import androidx.test.espresso.device.util.executeShellCommand
import androidx.test.filters.AbstractFilter
import org.junit.runner.Description

/**
 * Class that filters out tests annotated with {@link RequiresDeviceMode} when running on a device
 * that doesn't support the provided device mode.
 */
internal class RequiresDeviceModeFilter() : AbstractFilter() {
  override fun evaluateTest(description: Description): Boolean {
    val annotations = getMethodAnnotations(description)
    annotations.addAll(getClassAnnotations(description))
    if (!annotations.isEmpty()) {
      val supportedDeviceModes = getSupportedDeviceModes()
      val deviceModesAnnotations = mutableListOf<RequiresDeviceMode>()
      for (annotation in annotations) {
        if (annotation is RequiresDeviceModes) {
          for (v: RequiresDeviceMode in annotation.value) {
            deviceModesAnnotations.add(v)
          }
        } else if (annotation is RequiresDeviceMode) {
          deviceModesAnnotations.add(annotation)
        }
      }
      for (annotation in deviceModesAnnotations) {
        if (!supportedDeviceModes.contains(annotation.mode)) {
          Log.d(TAG, "Required device mode is not supported, skip the test")
          return false
        }
      }
    }
    Log.d(
      TAG,
      "No requires device mode annotation or all required modes are supported, run the test"
    )
    return true
  }

  private fun getSupportedDeviceModes(): List<DeviceMode> {
    if (Build.VERSION.SDK_INT < 29) {
      // Foldable postures are not available on device running on API 29 and below.
      return emptyList()
    }

    val supportedModes = mutableListOf<DeviceMode>()
    if (Build.VERSION.SDK_INT == 30 || Build.VERSION.SDK_INT == 31) {
      // The "device_state print-states" shell command does not work on APIs 30-31. For these
      // devices, check if any folding feaures are present and assume that devices that have
      // folding features can be half-open and open.
      val displayFeatures = executeShellCommand("cmd settings get global display_features")
      if (displayFeatures.contains("fold") || displayFeatures.contains("hinge")) {
        supportedModes.add(DeviceMode.TABLETOP)
        supportedModes.add(DeviceMode.BOOK)
        supportedModes.add(DeviceMode.FLAT)
      }
    } else { // API 32+
      // Example output on a foldable device:
      // "Supported states: [
      //  DeviceState{identifier=1, name='CLOSED'},
      //  DeviceState{identifier=2, name='HALF_OPENED'},
      //  DeviceState{identifier=3, name='OPENED'},
      // ]"
      val modes = executeShellCommand("cmd device_state print-states")
      if (modes.contains("HALF_OPENED")) {
        supportedModes.add(DeviceMode.TABLETOP)
        supportedModes.add(DeviceMode.BOOK)
      }
      if (modes.contains("OPENED")) {
        supportedModes.add(DeviceMode.FLAT)
      }
    }
    return supportedModes
  }

  override fun describe(): String {
    return "skip tests annotated with RequiresDeviceMode if necessary"
  }

  companion object {
    private val TAG = RequiresDeviceModeFilter::class.java.getSimpleName()
  }
}