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.common.calculateCurrentDisplayWidthAndHeightPx
import androidx.test.espresso.device.common.executeShellCommand
import androidx.test.espresso.device.common.getDeviceApiLevel
import androidx.test.espresso.device.common.getResumedActivityOrNull
import androidx.test.espresso.device.controller.DeviceControllerOperationException
import androidx.test.espresso.device.sizeclass.HeightSizeClass
import androidx.test.espresso.device.sizeclass.WidthSizeClass
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(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 = calculateCurrentDisplayWidthAndHeightDp(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 = calculateCurrentDisplayWidthAndHeightDp(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 = calculateCurrentDisplayWidthAndHeightDp(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 calculateCurrentDisplayWidthAndHeightDp(activity: Activity): Pair<Int, Int> {
    val displayPx = calculateCurrentDisplayWidthAndHeightPx()
    val widthDp = (displayPx.first / activity.getResources().displayMetrics.density).roundToInt()
    val heightDp = (displayPx.second / activity.getResources().displayMetrics.density).roundToInt()
    return Pair(widthDp, heightDp)
  }

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