DisplaySizeAction.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.action

import android.app.Activity
import android.content.res.Configuration
import android.util.Log
import android.view.View
import android.view.ViewGroup
import androidx.test.espresso.device.context.ActionContext
import androidx.test.espresso.device.controller.DeviceControllerOperationException
import androidx.test.espresso.device.sizeclass.HeightSizeClass
import androidx.test.espresso.device.sizeclass.WidthSizeClass
import androidx.test.espresso.device.util.executeShellCommand
import androidx.test.espresso.device.util.getDeviceApiLevel
import androidx.test.espresso.device.util.getResumedActivityOrNull
import androidx.test.platform.device.DeviceController
import androidx.test.platform.device.UnsupportedDeviceOperationException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt

/** Action to set the test device to the provided display size. */
internal class DisplaySizeAction(
  val widthDisplaySize: WidthSizeClass,
  val heightDisplaySize: HeightSizeClass
) : DeviceAction {
  override fun perform(context: ActionContext, deviceController: DeviceController) {
    if (getDeviceApiLevel() < 24) {
      throw UnsupportedDeviceOperationException(
        "Setting display size is not supported on devices with APIs below 24."
      )
    }

    val currentActivity = getResumedActivityOrNull()
    if (currentActivity != null) {
      val displaySize = calculateCurrentDisplay(currentActivity)
      val startingWidth = displaySize.first
      val startingHeight = displaySize.second
      if (
        widthDisplaySize == WidthSizeClass.compute(startingWidth) &&
          heightDisplaySize == HeightSizeClass.compute(startingHeight)
      ) {
        Log.d(TAG, "Device display is already the requested size, no changes needed.")
        return
      }

      val latch: CountDownLatch = CountDownLatch(1)

      val currentActivityView: View = object : View(currentActivity) {
        override fun onConfigurationChanged(newConfig: Configuration?) {
          super.onConfigurationChanged(newConfig)
          val currentDisplaySize = calculateCurrentDisplay(currentActivity)
          if (
            WidthSizeClass.compute(currentDisplaySize.first) == widthDisplaySize &&
              HeightSizeClass.compute(currentDisplaySize.second) == heightDisplaySize
          ) {
            latch.countDown()
          }
        }
      }
      val container: ViewGroup =
          currentActivity.getWindow().findViewById(android.R.id.content) as ViewGroup
      currentActivity.runOnUiThread {
        container.addView(currentActivityView)
      }

      val widthDp = WidthSizeClass.getWidthDpInSizeClass(widthDisplaySize)
      val heightDp = HeightSizeClass.getHeightDpInSizeClass(heightDisplaySize)

      executeShellCommand("wm size ${widthDp}dpx${heightDp}dp")

      latch.await(5, TimeUnit.SECONDS)
      currentActivity.runOnUiThread {
        container.removeView(currentActivityView)
      }

      val finalSize = calculateCurrentDisplay(currentActivity)
      if (
        WidthSizeClass.compute(finalSize.first) != widthDisplaySize ||
          HeightSizeClass.compute(finalSize.second) != heightDisplaySize
      ) {
        // Display could not be set to the requested size, reset to starting size
        executeShellCommand("wm size ${startingWidth}dpx${startingHeight}dp")
        throw UnsupportedDeviceOperationException(
          "Device could not be set to the requested display size."
        )
      }
    } else {
      throw DeviceControllerOperationException(
        "Device could not be set to the requested display size because there are no activities in" +
          " the resumed stage."
      )
    }
  }

  private fun calculateCurrentDisplay(activity: Activity): Pair<Int, Int> {
    // "wm size" will output a string with the format
    // "Physical size: WxH
    //  Override size: WxH"
    val output = executeShellCommand("wm size")

    val subStringToFind = "Override size: "
    val displaySizes =
      output.substring(output.indexOf(subStringToFind) + subStringToFind.length).trim().split("x")
    val widthPx = displaySizes.get(0).toInt()
    val heightPx = displaySizes.get(1).toInt()

    val widthDp = (widthPx / activity.getResources().displayMetrics.density).roundToInt()
    val heightDp = (heightPx / activity.getResources().displayMetrics.density).roundToInt()
    return Pair(widthDp, heightDp)
  }

  companion object {
    private val TAG = DisplaySizeAction::class.java.simpleName
  }
}