Outputs.kt
/*
* Copyright 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 androidx.benchmark
import android.annotation.SuppressLint
import android.os.Build
import android.util.Log
import androidx.annotation.RestrictTo
import androidx.test.platform.app.InstrumentationRegistry
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.TimeZone
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public object Outputs {
private val formatter: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss")
/**
* The intended output directory that respects the `additionalTestOutputDir`.
*/
public val outputDirectory: File
/**
* The usable output directory, given permission issues with `adb shell` on Android R.
* Both the app and the shell have access to this output folder.
*
* This dir can be read/written by app
* This dir can be read by shell (see [forceFilesForShellAccessible] for API 21/22!)
*/
public val dirUsableByAppAndShell: File
/**
* Any file created by this process for the shell to use must be explicitly made filesystem
* globally readable, as prior to API 23 the shell didn't have access by default.
*/
val forceFilesForShellAccessible: Boolean = Build.VERSION.SDK_INT in 21..22
init {
// Be explicit about the TimeZone for stable formatting
formatter.timeZone = TimeZone.getTimeZone("UTC")
val context = InstrumentationRegistry.getInstrumentation().targetContext
@SuppressLint("NewApi")
dirUsableByAppAndShell = when {
Build.VERSION.SDK_INT >= 29 -> {
// On Android Q+ we are using the media directory because that is
// the directory that the shell has access to. Context: b/181601156
// Additionally, Benchmarks append user space traces to the ones produced
// by the Macro Benchmark run; and that is a lot simpler to do if we use the
// Media directory. (b/216588251)
context.getFirstMountedMediaDir()
}
Build.VERSION.SDK_INT <= 22 -> {
// prior to API 23, shell didn't have access to externalCacheDir
context.cacheDir
}
else -> context.externalCacheDir
} ?: throw IllegalStateException(
"Unable to select a directory for writing files, " +
"additionalTestOutputDir argument required to declare output dir."
)
if (forceFilesForShellAccessible) {
// By default, shell doesn't have access to app dirs on 21/22 so we need to modify
// this so that the shell can output here too
dirUsableByAppAndShell.setReadable(true, false)
dirUsableByAppAndShell.setWritable(true, false)
dirUsableByAppAndShell.setExecutable(true, false)
}
Log.d(BenchmarkState.TAG, "Usable output directory: $dirUsableByAppAndShell")
outputDirectory = Arguments.additionalTestOutputDir?.let { File(it) }
?: dirUsableByAppAndShell
Log.d(BenchmarkState.TAG, "Output Directory: $outputDirectory")
outputDirectory.mkdirs()
}
/**
* Create a benchmark output [File] to write to.
*
* This method handles reporting files to `InstrumentationStatus` to request copy,
* writing them in the desired output directory, and handling shell access issues on Android R.
*
* @return The absolute path of the output [File].
*/
public fun writeFile(
fileName: String,
reportKey: String,
reportOnRunEndOnly: Boolean = false,
block: (file: File) -> Unit,
): String {
val sanitizedName = sanitizeFilename(fileName)
val destination = File(outputDirectory, sanitizedName)
// We override the `additionalTestOutputDir` argument.
// Context: b/181601156
val file = File(dirUsableByAppAndShell, sanitizedName)
block.invoke(file)
check(file.exists()) { "File doesn't exist!" }
if (dirUsableByAppAndShell != outputDirectory) {
// We need to copy files over anytime `dirUsableByAppAndShell` is different from
// `outputDirectory`.
Log.d(BenchmarkState.TAG, "Copying $file to $destination")
file.copyTo(destination, overwrite = true)
}
InstrumentationResults.reportAdditionalFileToCopy(
key = reportKey,
absoluteFilePath = destination.absolutePath,
reportOnRunEndOnly = reportOnRunEndOnly
)
return destination.absolutePath
}
public fun sanitizeFilename(filename: String): String {
return filename
.replace(" ", "")
.replace("(", "[")
.replace(")", "]")
}
public fun testOutputFile(filename: String): File {
return File(outputDirectory, filename)
}
public fun dateToFileName(date: Date = Date()): String {
return formatter.format(date)
}
public fun relativePathFor(path: String): String {
val hasOutputDirectoryPrefix = path.startsWith(outputDirectory.absolutePath)
val relativePath = when {
hasOutputDirectoryPrefix -> path.removePrefix("${outputDirectory.absolutePath}/")
else -> path.removePrefix("${dirUsableByAppAndShell.absolutePath}/")
}
check(relativePath != path) {
"$relativePath == $path"
}
return relativePath
}
}