| /* |
| * Copyright (C) 2021 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.os.cts |
| |
| import android.app.ActivityManager |
| import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE |
| import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING |
| import android.app.Instrumentation |
| import android.content.Context |
| import android.content.Intent |
| import android.content.pm.ApplicationInfo |
| import android.content.pm.PackageManager |
| import android.net.Uri |
| import android.os.Build |
| import android.platform.test.annotations.AppModeFull |
| import android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION |
| import android.provider.Settings |
| import android.support.test.uiautomator.By |
| import android.support.test.uiautomator.BySelector |
| import android.support.test.uiautomator.UiDevice |
| import android.support.test.uiautomator.UiObject2 |
| import android.support.test.uiautomator.UiScrollable |
| import android.support.test.uiautomator.UiSelector |
| import androidx.test.InstrumentationRegistry |
| import androidx.test.filters.SdkSuppress |
| import androidx.test.runner.AndroidJUnit4 |
| import com.android.compatibility.common.util.DisableAnimationRule |
| import com.android.compatibility.common.util.FreezeRotationRule |
| import com.android.compatibility.common.util.SystemUtil |
| import com.android.compatibility.common.util.SystemUtil.eventually |
| import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow |
| import com.android.compatibility.common.util.UiAutomatorUtils |
| import org.hamcrest.CoreMatchers |
| import org.hamcrest.Matchers |
| import org.junit.After |
| import org.junit.Assert.assertFalse |
| import org.junit.Assert.assertThat |
| import org.junit.Assert.assertTrue |
| import org.junit.Assume.assumeFalse |
| import org.junit.Before |
| import org.junit.Rule |
| import org.junit.Test |
| import org.junit.runner.RunWith |
| |
| /** |
| * Integration test for app hibernation. |
| */ |
| @RunWith(AndroidJUnit4::class) |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S") |
| class AppHibernationIntegrationTest { |
| companion object { |
| const val LOG_TAG = "AppHibernationIntegrationTest" |
| const val WAIT_TIME_MS = 1000L |
| const val MAX_SCROLL_ATTEMPTS = 3 |
| const val TEST_UNUSED_THRESHOLD = 1L |
| |
| const val CMD_KILL = "am kill %s" |
| } |
| private val context: Context = InstrumentationRegistry.getTargetContext() |
| private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() |
| |
| private lateinit var packageManager: PackageManager |
| |
| @get:Rule |
| val disableAnimationRule = DisableAnimationRule() |
| |
| @get:Rule |
| val freezeRotationRule = FreezeRotationRule() |
| |
| @Before |
| fun setup() { |
| packageManager = context.packageManager |
| |
| // Collapse notifications |
| assertThat( |
| runShellCommandOrThrow("cmd statusbar collapse"), |
| CoreMatchers.equalTo("")) |
| |
| // Wake up the device |
| runShellCommandOrThrow("input keyevent KEYCODE_WAKEUP") |
| runShellCommandOrThrow("input keyevent 82") |
| } |
| |
| @After |
| fun cleanUp() { |
| goHome() |
| } |
| |
| @Test |
| fun testUnusedApp_getsForceStopped() { |
| withDeviceConfig(NAMESPACE_APP_HIBERNATION, "app_hibernation_enabled", "true") { |
| withUnusedThresholdMs(TEST_UNUSED_THRESHOLD) { |
| withApp(APK_PATH_S_APP, APK_PACKAGE_NAME_S_APP) { |
| // Use app |
| startApp(APK_PACKAGE_NAME_S_APP) |
| leaveApp(APK_PACKAGE_NAME_S_APP) |
| killApp(APK_PACKAGE_NAME_S_APP) |
| |
| // Wait for the unused threshold time to pass |
| Thread.sleep(TEST_UNUSED_THRESHOLD) |
| |
| // Run job |
| runAppHibernationJob(context, LOG_TAG) |
| |
| // Verify |
| val ai = |
| packageManager.getApplicationInfo(APK_PACKAGE_NAME_S_APP, 0 /* flags */) |
| val stopped = ((ai.flags and ApplicationInfo.FLAG_STOPPED) != 0) |
| assertTrue(stopped) |
| |
| if (hasFeatureTV()) { |
| // Skip checking unused apps screen because it may be unavailable on TV |
| return |
| } |
| openUnusedAppsNotification() |
| waitFindObject(By.text(APK_PACKAGE_NAME_S_APP)) |
| } |
| } |
| } |
| } |
| |
| @Test |
| fun testPreSVersionUnusedApp_doesntGetForceStopped() { |
| assumeFalse( |
| "TV may have different behaviour for Pre-S version apps", |
| hasFeatureTV()) |
| withUnusedThresholdMs(TEST_UNUSED_THRESHOLD) { |
| withApp(APK_PATH_R_APP, APK_PACKAGE_NAME_R_APP) { |
| // Use app |
| startApp(APK_PACKAGE_NAME_R_APP) |
| leaveApp(APK_PACKAGE_NAME_R_APP) |
| killApp(APK_PACKAGE_NAME_R_APP) |
| |
| // Wait for the unused threshold time to pass |
| Thread.sleep(TEST_UNUSED_THRESHOLD) |
| |
| // Run job |
| runAppHibernationJob(context, LOG_TAG) |
| |
| // Verify |
| val ai = |
| packageManager.getApplicationInfo(APK_PACKAGE_NAME_R_APP, 0 /* flags */) |
| val stopped = ((ai.flags and ApplicationInfo.FLAG_STOPPED) != 0) |
| assertFalse(stopped) |
| } |
| } |
| } |
| |
| @AppModeFull(reason = "Uses application details settings") |
| @Test |
| fun testAppInfo_RemovePermissionsAndFreeUpSpaceToggleExists() { |
| assumeFalse( |
| "Remove permissions and free up space toggle may be unavailable on TV", |
| hasFeatureTV()) |
| withDeviceConfig(NAMESPACE_APP_HIBERNATION, "app_hibernation_enabled", "true") { |
| withApp(APK_PATH_S_APP, APK_PACKAGE_NAME_S_APP) { |
| // Open app info |
| val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) |
| val uri = Uri.fromParts("package", APK_PACKAGE_NAME_S_APP, null /* fragment */) |
| intent.data = uri |
| intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) |
| context.startActivity(intent) |
| |
| waitForIdle() |
| UiAutomatorUtils.getUiDevice() |
| |
| val packageManager = context.packageManager |
| val settingsPackage = intent.resolveActivity(packageManager).packageName |
| val res = packageManager.getResourcesForApplication(settingsPackage) |
| val title = res.getString( |
| res.getIdentifier("unused_apps_switch", "string", settingsPackage)) |
| |
| // Settings can have multiple scrollable containers so all of them should be |
| // searched. |
| var toggleFound = UiDevice.getInstance(instrumentation) |
| .findObject(UiSelector().text(title)) |
| .waitForExists(WAIT_TIME_MS) |
| var i = 0 |
| var scrollableObject = UiScrollable(UiSelector().scrollable(true).instance(i)) |
| while (!toggleFound && scrollableObject.waitForExists(WAIT_TIME_MS)) { |
| toggleFound = scrollableObject.scrollTextIntoView(title) |
| scrollableObject = UiScrollable(UiSelector().scrollable(true).instance(++i)) |
| } |
| |
| assertTrue("Remove permissions and free up space toggle not found", toggleFound) |
| } |
| } |
| } |
| |
| private fun leaveApp(packageName: String) { |
| eventually { |
| goHome() |
| SystemUtil.runWithShellPermissionIdentity { |
| val packageImportance = context |
| .getSystemService(ActivityManager::class.java)!! |
| .getPackageImportance(packageName) |
| assertThat(packageImportance, Matchers.greaterThan(IMPORTANCE_TOP_SLEEPING)) |
| } |
| } |
| } |
| |
| private fun killApp(packageName: String) { |
| eventually { |
| SystemUtil.runWithShellPermissionIdentity { |
| runShellCommandOrThrow(String.format(CMD_KILL, packageName)) |
| val packageImportance = context |
| .getSystemService(ActivityManager::class.java)!! |
| .getPackageImportance(packageName) |
| assertThat(packageImportance, Matchers.equalTo(IMPORTANCE_GONE)) |
| } |
| } |
| } |
| |
| private fun waitFindObject(selector: BySelector): UiObject2 { |
| return waitFindObject(instrumentation.uiAutomation, selector) |
| } |
| } |