PerfettoSdkSideloader.kt
/*
* Copyright 2023 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.tracing.perfetto.handshake
import java.io.File
import java.util.zip.ZipFile
/**
* Sideloads the `libtracing_perfetto.so` file to a location available to the traced app
*
* The class solves the following sub-problems:
* - knowing the right location to place the binaries
* - knowing how to extract the binaries from an AAR or APK, including choosing the right build
* variant for the device (e.g. arm64-v8a) from the archive
* - knowing how to handle device IO permissions, e.g. to allow a Benchmark app place
* the Perfetto binaries in a location accessible by the benchmarked app (we use `shell` for this)
*/
internal class PerfettoSdkSideloader(private val packageName: String) {
/**
* Sideloads `libtracing_perfetto.so` from a ZIP source to a location available to the traced
* app
*
* @param sourceZipFile either an AAR or an APK containing `libtracing_perfetto.so`
* @param shellCommandExecutor function capable of executing adb shell commands (used to
* determine the device ABI)
* @param tempDirectory a directory directly accessible to the process (used for extraction
* of the binaries from the zip)
* @param moveLibFileFromTmpDirToAppDir a function capable of moving the binary file from
* the [tempDirectory] and an app accessible folder
*
* @return location where the library file was sideloaded to
*/
fun sideloadFromZipFile(
sourceZipFile: File,
tempDirectory: File,
shellCommandExecutor: ShellCommandExecutor,
moveLibFileFromTmpDirToAppDir: FileMover
): File {
val abi = getDeviceAbi(shellCommandExecutor)
val tmpFile = extractPerfettoBinaryFromZip(sourceZipFile, tempDirectory, abi)
return sideloadSoFile(tmpFile, moveLibFileFromTmpDirToAppDir)
}
/**
* Sideloads `libtracing_perfetto.so` to a location available to the traced app
*
* @param libFile `libtracing_perfetto.so` file
* @param moveLibFileToAppDir a function moving the [libFile] to an app accessible folder
*
* @return location where the library file was sideloaded to
*/
private fun sideloadSoFile(libFile: File, moveLibFileToAppDir: FileMover): File {
val dstFile = libFileForPackageName(packageName)
moveLibFileToAppDir(libFile, dstFile)
return dstFile
}
private fun extractPerfettoBinaryFromZip(
sourceZip: File,
outputDir: File,
abi: String
): File {
val outputFile = outputDir.resolve(libFileName)
val rxLibPathInsideZip = Regex(".*(lib|jni)/[^/]*$abi[^/]*/$libFileName")
val zipFile = ZipFile(sourceZip)
val entry = zipFile
.entries()
.asSequence()
.firstOrNull { it.name.matches(rxLibPathInsideZip) }
?: throw IllegalStateException(
"Unable to locate $libFileName required to enable Perfetto SDK. " +
"Tried inside ${sourceZip.absolutePath}."
)
zipFile.getInputStream(entry).use { inputStream ->
outputFile.outputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
}
return outputFile
}
private fun getDeviceAbi(executeShellCommand: ShellCommandExecutor): String =
executeShellCommand("getprop ro.product.cpu.abilist").split(",")
.plus(executeShellCommand("getprop ro.product.cpu.abi"))
.first()
.trim()
private companion object {
private const val libFileName = "libtracing_perfetto.so"
fun libFileForPackageName(packageName: String) =
File("/sdcard/Android/media/$packageName/$libFileName")
}
}
internal typealias FileMover = (srcFile: File, dstFile: File) -> Unit
internal typealias ShellCommandExecutor = (command: String) -> String