blob: 60e8651a7bef3880f9265056aee39e2b6301f65e [file] [log] [blame]
/*
* 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 com.android.car.internal;
import static android.system.OsConstants.PROT_READ;
import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_CODE;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SharedMemory;
import android.system.ErrnoException;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import java.io.Closeable;
import java.nio.ByteBuffer;
/**
* Base class to allow passing {@code Parcelable} over binder directly or through shared memory if
* payload size is too big.
*
* <p>Child class should inherit this to use this or use {@link LargeParcelable} class.
*
* <p>Parcelized data will have following elements
* <ul>
* <li>@Nullable Parcelable
* <li>@Nullable SharedMemory which include serialized Parcelable if non-null. This will be set
* only when the previous Parcelable is null or this also can be null for no data case.
* </ul>
*
* @hide
*/
public abstract class LargeParcelableBase implements Parcelable, Closeable {
/** Payload size bigger than this value will be passed over shared memory. */
public static final int MAX_DIRECT_PAYLOAD_SIZE = 4096;
private static final String TAG = LargeParcelable.class.getSimpleName();
private static final boolean DBG_PAYLOAD = Log.isLoggable(TAG, Log.DEBUG);
private static final int DBG_DUMP_LENGTH = 16;
private static final int NULL_PAYLOAD = 0;
private static final int NONNULL_PAYLOAD = 1;
private static final int FD_HEADER = 0;
private final Object mLock = new Object();
@GuardedBy("mLock")
private @Nullable SharedMemory mSharedMemory;
/**
* Serialize (=write Parcelable into given Parcel) a {@code Parcelable} child class wants to
* pass over binder call.
*/
protected abstract void serialize(@NonNull Parcel dest, int flags);
/**
* Serialize null payload to the given {@code Parcel}. For {@code Parcelable}, this can be
* simply {@code dest.writeParcelable(null)} but non-Parcelable should have other way to
* mark that there is no payload.
*/
protected abstract void serializeNullPayload(@NonNull Parcel dest);
/**
* Read a {@code Parcelable} from the given {@code Parcel}.
*/
protected abstract void deserialize(@NonNull Parcel src);
public LargeParcelableBase() {
}
public LargeParcelableBase(Parcel in) {
// Make this compatible with stable AIDL
// payload size + Parcelable / payload + 1:has shared memory + 0 + file
// 0:no shared memory
// 0 + file makes it compatible with ParcelFileDescrpitor
// file contains:
// file size + Parcelable / payload + 0
int startPosition = in.dataPosition();
int totalPayloadSize = in.readInt();
deserialize(in);
int sharedMemoryPosition = in.dataPosition();
boolean hasSharedMemory = (in.readInt() != NULL_PAYLOAD);
if (hasSharedMemory) {
int fdHeader = in.readInt();
if (fdHeader != FD_HEADER) {
throw new IllegalArgumentException(
"Invalid data, wrong fdHeader, expected 0 while got " + fdHeader);
}
SharedMemory memory = SharedMemory.CREATOR.createFromParcel(in);
deserializeSharedMemoryAndClose(memory);
}
in.setDataPosition(startPosition + totalPayloadSize);
if (DBG_PAYLOAD) {
Log.d(TAG, "Read, start:" + startPosition + " totalPayloadSize:" + totalPayloadSize
+ " sharedMemoryPosition:" + sharedMemoryPosition
+ " hasSharedMemory:" + hasSharedMemory + " dataAvail:" + in.dataAvail());
}
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
int startPosition = dest.dataPosition();
SharedMemory sharedMemory;
synchronized (mLock) {
sharedMemory = mSharedMemory;
}
int totalPayloadSize = 0;
if (sharedMemory != null) {
// optimized path for resending the same Parcelable multiple times with already
// created shared memory
totalPayloadSize = serializeMemoryFdOrPayloadToParcel(dest, flags, sharedMemory);
if (DBG_PAYLOAD) {
Log.d(TAG, "Write, reusing shared memory, start:" + startPosition
+ " totalPayloadSize:" + totalPayloadSize);
}
return;
}
// dataParcel is the parcel that would be serialized to the shared memory file.
Parcel dataParcel = Parcel.obtain();
totalPayloadSize = serializeMemoryFdOrPayloadToParcel(dataParcel, flags, null);
boolean noSharedMemory = totalPayloadSize <= MAX_DIRECT_PAYLOAD_SIZE;
boolean hasNonNullPayload = true;
if (noSharedMemory) {
if (DBG_PAYLOAD) {
Log.d(TAG, "not using shared memory");
}
dest.appendFrom(dataParcel, 0, totalPayloadSize);
dataParcel.recycle();
} else {
if (DBG_PAYLOAD) {
Log.d(TAG, "using shared memory");
}
sharedMemory = serializeParcelToSharedMemory(dataParcel);
dataParcel.recycle();
synchronized (mLock) {
// If it is already set, let sharedMemory go and GV will close it later.
// This is ok as this kind of race should not happen often.
if (mSharedMemory != null) {
mSharedMemory = sharedMemory;
}
}
totalPayloadSize = serializeMemoryFdOrPayloadToParcel(dest, flags, sharedMemory);
}
if (DBG_PAYLOAD) {
Log.d(TAG, "Write, start:" + startPosition + " totalPayloadSize:" + totalPayloadSize
+ " hasNonNullPayload:" + hasNonNullPayload
+ " hasSharedMemory:" + !noSharedMemory + " dataSize:" + dest.dataSize());
}
}
private int updatePayloadSize(Parcel dest, int startPosition) {
int lastPosition = dest.dataPosition();
int totalPayloadSize = lastPosition - startPosition;
dest.setDataPosition(startPosition);
dest.writeInt(totalPayloadSize);
dest.setDataPosition(lastPosition);
dest.setDataSize(lastPosition);
return totalPayloadSize;
}
// Write shared memory in compatible way with ParcelFileDescriptor
private void writeSharedMemoryCompatibleToParcel(Parcel dest, SharedMemory memory, int flags) {
// dest.writeParcelable() adds class type which makes it incompatible with C++.
if (memory == null) {
dest.writeInt(NULL_PAYLOAD);
return;
}
// non-null case
dest.writeInt(NONNULL_PAYLOAD);
dest.writeInt(FD_HEADER); // additional header for ParcelFileDescriptor
memory.writeToParcel(dest, flags);
}
@Override
@ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE)
public int describeContents() {
return 0;
}
/**
* {@inheritDoc}
*
* <p>Close the underlying shared memory for this. This can be called multiple times safely.
* When this is not called explicitly, it will be closed when this instance is GCed.
* Calling this can be useful when many instances are created frequently.
*
* <p>If underlying payload is changed, the client should call this before sending it over
* binder as sending it over binder can keep shared memory generated from the previous binder
* call.
*/
@Override
public void close() {
SharedMemory sharedMemory = null;
synchronized (mLock) {
sharedMemory = mSharedMemory;
mSharedMemory = null;
}
if (sharedMemory != null) {
sharedMemory.close();
}
}
protected static SharedMemory serializeParcelToSharedMemory(Parcel p) {
SharedMemory memory = null;
ByteBuffer buffer = null;
int size = p.dataSize();
try {
memory = SharedMemory.create(LargeParcelableBase.class.getSimpleName(), size);
buffer = memory.mapReadWrite();
byte[] data = p.marshall();
buffer.put(data, 0, size);
if (DBG_PAYLOAD) {
int dumpSize = Math.min(DBG_DUMP_LENGTH, data.length);
StringBuilder bd = new StringBuilder();
bd.append("marshalled:");
for (int i = 0; i < dumpSize; i++) {
bd.append(data[i]);
if (i != dumpSize - 1) {
bd.append(',');
}
}
bd.append("=memory:");
for (int i = 0; i < dumpSize; i++) {
bd.append(buffer.get(i));
if (i != dumpSize - 1) {
bd.append(',');
}
}
Log.d(TAG, bd.toString());
}
if (!memory.setProtect(PROT_READ)) {
memory.close();
throw new SecurityException("Failed to set read-only protection on shared memory");
}
} catch (ErrnoException e) {
throw new IllegalArgumentException("Failed to use shared memory", e);
} catch (Exception e) {
throw new IllegalArgumentException("failed to serialize", e);
} finally {
if (buffer != null) {
SharedMemory.unmap(buffer);
}
}
return memory;
}
protected static Parcel copyFromSharedMemory(SharedMemory memory) {
ByteBuffer buffer = null;
Parcel in = Parcel.obtain();
try {
buffer = memory.mapReadOnly();
// TODO(b/188781089) find way to avoid this additional copy
byte[] payload = new byte[buffer.limit()];
buffer.get(payload);
in.unmarshall(payload, 0, payload.length);
in.setDataPosition(0);
if (DBG_PAYLOAD) {
int dumpSize = Math.min(DBG_DUMP_LENGTH, payload.length);
StringBuilder bd = new StringBuilder();
bd.append("unmarshalled:");
int parcelStartPosition = in.dataPosition();
byte[] fromParcel = in.marshall();
for (int i = 0; i < dumpSize; i++) {
bd.append(fromParcel[i]);
if (i != dumpSize - 1) bd.append(',');
}
bd.append("=startPosition:");
bd.append(parcelStartPosition);
bd.append("=memory:");
for (int i = 0; i < dumpSize; i++) {
bd.append(buffer.get(i));
if (i != dumpSize - 1) bd.append(',');
}
bd.append("=interim_payload:");
for (int i = 0; i < dumpSize; i++) {
bd.append(payload[i]);
if (i != dumpSize - 1) bd.append(',');
}
Log.d(TAG, bd.toString());
in.setDataPosition(parcelStartPosition);
}
} catch (ErrnoException e) {
throw new IllegalArgumentException("cannot create Parcelable from SharedMemory", e);
} catch (Exception e) {
throw new IllegalArgumentException("failed to deserialize", e);
} finally {
if (buffer != null) {
SharedMemory.unmap(buffer);
}
}
return in;
}
private void deserializeSharedMemoryAndClose(SharedMemory memory) {
// The shared memory file contains a serialized largeParcelable.
// size + payload + 0 (no shared memory).
Parcel in = null;
try {
in = copyFromSharedMemory(memory);
// Even if we don't need the file size, we have to read it from the parcel to advance
// the data position.
int fileSize = in.readInt();
if (DBG_PAYLOAD) {
Log.d(TAG, "file size in shared memory file: " + fileSize);
}
deserialize(in);
// There is an additional 0 in the parcel, but we ignore that.
} finally {
memory.close();
if (in != null) {
in.recycle();
}
}
}
// If sharedMemory is not null, serialize null payload and shared memory to parcel.
// Otherwise, serialize the actual payload to parcel.
private int serializeMemoryFdOrPayloadToParcel(
Parcel dest, int flags, @Nullable SharedMemory sharedMemory) {
int startPosition = dest.dataPosition();
dest.writeInt(0); // payload size
if (sharedMemory != null) {
serializeNullPayload(dest);
writeSharedMemoryCompatibleToParcel(dest, sharedMemory, flags);
} else {
serialize(dest, flags);
writeSharedMemoryCompatibleToParcel(dest, null, flags);
}
return updatePayloadSize(dest, startPosition);
}
}