blob: 4fbbfbbd8ecb5fc668a59013be0e9baa4faa4b3c [file] [log] [blame]
/*
* 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.profileinstaller;
import static androidx.profileinstaller.ProfileTranscoder.MAGIC_PROF;
import static androidx.profileinstaller.ProfileTranscoder.MAGIC_PROFM;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.Executor;
/**
* Orchestrate device-level profiler decisions.
*
* This class is structured such that it is fast at execution time and avoids allocating extra
* memory, or reading files multiple times, above api simplicity.
*
* Usage:
*
* <pre>
* if (!deviceProfileWriter.deviceAllowsProfileInstallerAotWrites()) {
* return; // nothing else to do here
* }
* deviceProfileWriter.copyProfileOrRead(skipStrategy)
* .transcodeIfNeeded()
* .writeIfNeeded(skipStrategy);
* </pre>
*
*/
@RequiresApi(19)
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class DeviceProfileWriter {
@NonNull
private final AssetManager mAssetManager;
@NonNull
private final Executor mExecutor;
@NonNull
private final ProfileInstaller.DiagnosticsCallback mDiagnostics;
@Nullable
private final byte[] mDesiredVersion;
@NonNull
private final File mCurProfile;
@NonNull
private final String mApkName;
@NonNull
private final String mProfileSourceLocation;
@NonNull
private final String mProfileMetaSourceLocation;
private boolean mDeviceSupportsAotProfile = false;
@Nullable
private DexProfileData[] mProfile;
@Nullable
private byte[] mTranscodedProfile;
private void result(@ProfileInstaller.ResultCode int code, @Nullable Object data) {
mExecutor.execute(() -> mDiagnostics.onResultReceived(code, data));
}
/**
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public DeviceProfileWriter(
@NonNull AssetManager assetManager,
@NonNull Executor executor,
@NonNull ProfileInstaller.DiagnosticsCallback diagnosticsCallback,
@NonNull String apkName,
@NonNull String profileSourceLocation,
@NonNull String profileMetaSourceLocation,
@NonNull File curProfile
) {
mAssetManager = assetManager;
mExecutor = executor;
mDiagnostics = diagnosticsCallback;
mApkName = apkName;
mProfileSourceLocation = profileSourceLocation;
mProfileMetaSourceLocation = profileMetaSourceLocation;
mCurProfile = curProfile;
mDesiredVersion = desiredVersion();
}
/**
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public boolean deviceAllowsProfileInstallerAotWrites() {
if (mDesiredVersion == null) {
result(ProfileInstaller.RESULT_UNSUPPORTED_ART_VERSION, Build.VERSION.SDK_INT);
return false;
}
// Check if the current profile file can be written. In Android U the current profile is
// no more created empty at app startup, so we need to deal with both file already existing
// and not existing. When the file exists, we just want to make sure that it's writeable.
// When the file does not exist, we want to make sure that it can be created.
// If this is not possible on the device, there is nothing we can do. This behavior might
// also be customized by OEM, that could prevent writing this file.
if (mCurProfile.exists()) {
if (!mCurProfile.canWrite()) {
result(ProfileInstaller.RESULT_NOT_WRITABLE, null);
return false;
}
} else {
try {
mCurProfile.createNewFile();
} catch (IOException e) {
// If the file cannot be created it's the same of the profile file not being
// writeable
result(ProfileInstaller.RESULT_NOT_WRITABLE, null);
return false;
}
}
mDeviceSupportsAotProfile = true;
return true;
}
private void assertDeviceAllowsProfileInstallerAotWritesCalled() {
if (!mDeviceSupportsAotProfile) {
throw new IllegalStateException("This device doesn't support aot. Did you call "
+ "deviceSupportsAotProfile()?");
}
}
/**
* Attempt to copy the profile, or if it needs transcode it read it.
*
* Always call this with transcodeIfNeeded and writeIfNeeded()
*
* <pre>
* deviceProfileInstaller.read()
* .transcodeIfNeeded()
* .write()
* </pre>
*
* @return this to chain call to transcodeIfNeeded
*/
@NonNull
@RestrictTo(RestrictTo.Scope.LIBRARY)
public DeviceProfileWriter read() {
assertDeviceAllowsProfileInstallerAotWritesCalled();
if (mDesiredVersion == null) {
return this;
}
InputStream profileStream = getProfileInputStream(mAssetManager);
if (profileStream != null) {
mProfile = readProfileInternal(profileStream);
}
if (mProfile != null) {
DexProfileData[] profile = mProfile;
if (requiresMetadata()) {
DeviceProfileWriter profileWriter = addMetadata(profile, mDesiredVersion);
if (profileWriter != null) return profileWriter;
}
}
return this;
}
/**
* Loads an {@link InputStream} from assets whether the underlying file is compressed or not.
*
* @param assetManager The {@link AssetManager} to use.
* @param location The source file's location.
* @return An InputStream in case the profile was successfully read.
* @throws IOException If anything goes wrong while opening or reading the file.
*/
private @Nullable InputStream openStreamFromAssets(AssetManager assetManager, String location)
throws IOException {
InputStream profileStream = null;
try {
AssetFileDescriptor descriptor = assetManager.openFd(location);
profileStream = descriptor.createInputStream();
} catch (FileNotFoundException e) {
String message = e.getMessage();
if (message != null && message.contains("compressed")) {
mDiagnostics.onDiagnosticReceived(
ProfileInstaller.DIAGNOSTIC_PROFILE_IS_COMPRESSED, null);
}
}
return profileStream;
}
/**
* Load the baseline profile file from assets.
* @param assetManager The {@link AssetManager} to use.
* @return The opened stream or null if the stream was unable to be opened.
*/
private @Nullable InputStream getProfileInputStream(AssetManager assetManager) {
InputStream profileStream = null;
try {
profileStream = openStreamFromAssets(assetManager, mProfileSourceLocation);
} catch (FileNotFoundException e) {
mDiagnostics.onResultReceived(ProfileInstaller.RESULT_BASELINE_PROFILE_NOT_FOUND, e);
} catch (IOException e) {
mDiagnostics.onResultReceived(ProfileInstaller.RESULT_IO_EXCEPTION, e);
}
return profileStream;
}
/**
* Reads a baseline profile from a given {@link InputStream} and transcodes it along the way
* if needed.
*
* @param profileStream The {@link InputStream} containing the baseline profile data.
*/
private @Nullable DexProfileData[] readProfileInternal(InputStream profileStream) {
DexProfileData[] profile = null;
try {
byte[] baselineVersion = ProfileTranscoder.readHeader(profileStream, MAGIC_PROF);
profile = ProfileTranscoder.readProfile(profileStream, baselineVersion, mApkName);
} catch (IOException e) {
mDiagnostics.onResultReceived(ProfileInstaller.RESULT_IO_EXCEPTION, e);
} catch (IllegalStateException e) {
mDiagnostics.onResultReceived(ProfileInstaller.RESULT_PARSE_EXCEPTION, e);
} finally {
try {
profileStream.close();
} catch (IOException e) {
mDiagnostics.onResultReceived(ProfileInstaller.RESULT_IO_EXCEPTION, e);
}
}
return profile;
}
/**
* Add Metadata from an existing baseline profile metadata file.
* @param profile The profile which needs adding of metadata.
*
* @return Baseline profile with metaadata.
*/
@Nullable
private DeviceProfileWriter addMetadata(DexProfileData[] profile, byte[] desiredVersion) {
try (InputStream is = openStreamFromAssets(mAssetManager, mProfileMetaSourceLocation)) {
if (is != null) {
byte[] metaVersion = ProfileTranscoder.readHeader(is, MAGIC_PROFM);
mProfile = ProfileTranscoder.readMeta(
is,
metaVersion,
desiredVersion,
profile
);
return this;
}
} catch (FileNotFoundException e) {
mDiagnostics.onResultReceived(
ProfileInstaller.RESULT_META_FILE_REQUIRED_BUT_NOT_FOUND, e);
} catch (IOException e) {
mDiagnostics.onResultReceived(ProfileInstaller.RESULT_IO_EXCEPTION, e);
} catch (IllegalStateException e) {
mProfile = null;
mDiagnostics.onResultReceived(ProfileInstaller.RESULT_PARSE_EXCEPTION, e);
}
return null;
}
/**
* Attempt to transcode profile, or if it needs transcode it read it.
*
* Always call this after read
*
* <pre>
* deviceProfileInstaller.read()
* .transcodeIfNeeded()
* .write()
* </pre>
*
* This method will always clear the profile read by copyProfileOrRead and may only be called
* once.
*
* @return this to chain call call writeIfNeeded()
*/
@NonNull
@RestrictTo(RestrictTo.Scope.LIBRARY)
public DeviceProfileWriter transcodeIfNeeded() {
DexProfileData[] profile = mProfile;
byte[] desiredVersion = mDesiredVersion;
if (profile == null || desiredVersion == null) {
return this;
}
assertDeviceAllowsProfileInstallerAotWritesCalled();
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
ProfileTranscoder.writeHeader(os, desiredVersion);
boolean success = ProfileTranscoder.transcodeAndWriteBody(
os,
desiredVersion,
profile
);
if (!success) {
mDiagnostics.onResultReceived(
ProfileInstaller.RESULT_DESIRED_FORMAT_UNSUPPORTED,
null
);
mProfile = null;
return this;
}
mTranscodedProfile = os.toByteArray();
} catch (IOException e) {
mDiagnostics.onResultReceived(ProfileInstaller.RESULT_IO_EXCEPTION, e);
} catch (IllegalStateException e) {
mDiagnostics.onResultReceived(ProfileInstaller.RESULT_PARSE_EXCEPTION, e);
}
mProfile = null;
return this;
}
/**
* Write the transcoded profile generated by transcodeIfNeeded()
*
* This method will always clear the profile, and may only be called once.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public boolean write() {
byte[] transcodedProfile = mTranscodedProfile;
if (transcodedProfile == null) {
return false;
}
assertDeviceAllowsProfileInstallerAotWritesCalled();
try (
InputStream bis = new ByteArrayInputStream(transcodedProfile);
OutputStream os = new FileOutputStream(mCurProfile)
) {
Encoding.writeAll(bis, os);
result(ProfileInstaller.RESULT_INSTALL_SUCCESS, null);
return true;
} catch (FileNotFoundException e) {
result(ProfileInstaller.RESULT_BASELINE_PROFILE_NOT_FOUND, e);
} catch (IOException e) {
result(ProfileInstaller.RESULT_IO_EXCEPTION, e);
} finally {
mTranscodedProfile = null;
mProfile = null;
}
return false;
}
private static @Nullable byte[] desiredVersion() {
// If SDK is pre or post supported version, we don't want to do anything, so return null.
if (Build.VERSION.SDK_INT < ProfileVersion.MIN_SUPPORTED_SDK
|| Build.VERSION.SDK_INT > ProfileVersion.MAX_SUPPORTED_SDK) {
return null;
}
switch (Build.VERSION.SDK_INT) {
case Build.VERSION_CODES.N:
case Build.VERSION_CODES.N_MR1:
return ProfileVersion.V001_N;
case Build.VERSION_CODES.O:
return ProfileVersion.V005_O;
case Build.VERSION_CODES.O_MR1:
return ProfileVersion.V009_O_MR1;
case Build.VERSION_CODES.P:
case Build.VERSION_CODES.Q:
case Build.VERSION_CODES.R:
return ProfileVersion.V010_P;
case Build.VERSION_CODES.S:
case Build.VERSION_CODES.S_V2:
case Build.VERSION_CODES.TIRAMISU:
return ProfileVersion.V015_S;
default:
return null;
}
}
private static boolean requiresMetadata() {
// If SDK is pre-N, we don't want to do anything, so return null.
if (Build.VERSION.SDK_INT < ProfileVersion.MIN_SUPPORTED_SDK
|| Build.VERSION.SDK_INT > ProfileVersion.MAX_SUPPORTED_SDK) {
return false;
}
switch (Build.VERSION.SDK_INT) {
// The profiles for N and N_MR1 used class ids to identify classes instead of type
// ids, which is what the V0.1.0 profile encodes, so a metadata file is required in
// order to transcode to this profile.
case Build.VERSION_CODES.N:
case Build.VERSION_CODES.N_MR1:
return true;
// for all of these versions, the data encoded in the V0.1.0 profile is enough to
// losslessly transcode into these other formats.
case Build.VERSION_CODES.O:
case Build.VERSION_CODES.O_MR1:
case Build.VERSION_CODES.P:
case Build.VERSION_CODES.Q:
case Build.VERSION_CODES.R:
return false;
// The profiles for S require a typeIdCount. Therefore metadata is required.
case Build.VERSION_CODES.S:
case Build.VERSION_CODES.S_V2:
case Build.VERSION_CODES.TIRAMISU:
return true;
default:
return false;
}
}
}