blob: dc7183cc1c6039708b85bfb2e445f092bdce35ad [file] [log] [blame]
/*
* Copyright (C) 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 android.app.appsearch;
import android.annotation.NonNull;
import android.annotation.SuppressLint;
import android.os.Build;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
/** Wrapper class to provide implementation for readBlob/writeBlob for all API levels.
*
* @hide
*/
public class ParcelableUtil {
private static final String TAG = "AppSearchParcel";
private static final String TEMP_FILE_PREFIX = "AppSearchSerializedBytes";
private static final String TEMP_FILE_SUFFIX = ".tmp";
// Same as IBinder.MAX_IPC_LIMIT. Limit that should be placed on IPC sizes to keep them safely
// under the transaction buffer limit.
private static final int DOCUMENT_SIZE_LIMIT_IN_BYTES = 64 * 1024;
// TODO(b/232805516): Update SDK_INT in Android.bp to safeguard from unexpected compiler issues.
@SuppressLint("ObsoleteSdkInt")
public static void writeBlob(@NonNull Parcel parcel, @NonNull byte[] bytes) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// Since parcel.writeBlob was added in API level 33, it is not available
// on lower API levels.
parcel.writeBlob(bytes);
} else {
writeToParcelForSAndBelow(parcel, bytes);
}
}
private static void writeToParcelForSAndBelow(Parcel parcel, byte[] bytes) {
try {
parcel.writeInt(bytes.length);
if (bytes.length <= DOCUMENT_SIZE_LIMIT_IN_BYTES) {
parcel.writeByteArray(bytes);
} else {
ParcelFileDescriptor parcelFileDescriptor =
writeDataToTempFileAndUnlinkFile(bytes);
parcel.writeFileDescriptor(parcelFileDescriptor.getFileDescriptor());
}
} catch (IOException e) {
// TODO(b/232805516): Add abstraction to handle the exception based on environment.
Log.w(TAG, "Couldn't write to unlinked file.", e);
}
}
@NonNull
// TODO(b/232805516): Update SDK_INT in Android.bp to safeguard from unexpected compiler issues.
@SuppressLint("ObsoleteSdkInt")
public static byte[] readBlob(Parcel parcel) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// Since parcel.readBlob was added in API level 33, it is not available
// on lower API levels.
return parcel.readBlob();
} else {
return readFromParcelForSAndBelow(parcel);
}
}
private static byte[] readFromParcelForSAndBelow(Parcel parcel) {
try {
int length = parcel.readInt();
if (length <= DOCUMENT_SIZE_LIMIT_IN_BYTES) {
byte[] documentByteArray = new byte[length];
parcel.readByteArray(documentByteArray);
return documentByteArray;
} else {
ParcelFileDescriptor pfd = parcel.readFileDescriptor();
return getDataFromFd(pfd, length);
}
} catch (IOException e) {
// TODO(b/232805516): Add abstraction to handle the exception based on environment.
Log.w(TAG, "Couldn't read from unlinked file.", e);
return null;
}
}
/**
* Reads data bytes from file using provided FileDescriptor. It also closes the PFD so that
* will delete the underlying file if it's the only reference left.
*
* @param pfd ParcelFileDescriptor for the file to read.
* @param length Number of bytes to read from the file.
*/
private static byte[] getDataFromFd(ParcelFileDescriptor pfd,
int length) throws IOException {
try(DataInputStream in =
new DataInputStream(new ParcelFileDescriptor.AutoCloseInputStream(pfd))){
byte[] data = new byte[length];
in.read(data);
return data;
}
}
/**
* Writes to a temp file owned by the caller, then unlinks/deletes it, and returns an FD which
* is the only remaining reference to the tmp file.
*
* @param data Data in the form of byte array to write to the file.
*/
private static ParcelFileDescriptor writeDataToTempFileAndUnlinkFile(byte[] data)
throws IOException {
// TODO(b/232959004): Update directory to app-specific cache dir instead of null.
File unlinkedFile =
File.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX, /* directory= */ null);
try(DataOutputStream out = new DataOutputStream(new FileOutputStream(unlinkedFile))) {
out.write(data);
out.flush();
}
ParcelFileDescriptor parcelFileDescriptor =
ParcelFileDescriptor.open(unlinkedFile,
ParcelFileDescriptor.MODE_CREATE
| ParcelFileDescriptor.MODE_READ_WRITE);
unlinkedFile.delete();
return parcelFileDescriptor;
}
private ParcelableUtil() {}
}