/* * 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 android.annotation.NonNull; import android.annotation.Nullable; import android.car.builtin.os.SharedMemoryHelper; import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.Parcelable; import android.os.SharedMemory; import android.util.Slog; import java.io.IOException; import java.lang.ref.WeakReference; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.concurrent.Callable; /** * Utility to pass any {@code Parcelable} through binder with automatic conversion into shared * memory when data size is too big. * *

This class can work by itself but child class can be useful to use a custom class to * interface with C++ world. For such usage, child class will only add its own {@code CREATOR} impl * and a constructor taking {@code Parcel in}. *

For stable AIDL, this class also provides two methods for serialization {@link * #toLargeParcelable(Parcelable)} and deserialization * {@link #reconstructStableAIDLParcelable(Parcelable, boolean)}. Plz check included test for the * usage. * * @hide */ public class LargeParcelable extends LargeParcelableBase { /** * Stable AIDL Parcelable should have this member with {@code ParcelFileDescriptor} to support * bigger payload passing over shared memory. */ public static final String STABLE_AIDL_SHARED_MEMORY_MEMBER = "sharedMemoryFd"; /** * Stable AIDL Parcelable has {@code readFromParcel(Parcel)} public method. */ public static final String STABLE_AIDL_PARCELABLE_READ_FROM_PARCEL = "readFromParcel"; private static final String TAG = LargeParcelable.class.getSimpleName(); private static final boolean DBG_PAYLOAD = false; private static final boolean DBG_STABLE_AIDL_CLASS = false; // cannot set this final even if it is set only once in constructor as set is done in // deserialize call. private @Nullable Parcelable mParcelable; // This is shared across thread. As this is per class, use volatile to avoid adding // separate lock. If additional static volatile is added, a lock should be added. private static volatile WeakReference sClassLoader = null; /** * Sets {@code ClassLoader} for loading the {@Code Parcelable}. This should be done before * getting binder call. Default classloader may not recognize the Parcelable passed and relevant * classloader like package classloader should be set before getting any binder data. */ public static void setClassLoader(ClassLoader loader) { sClassLoader = new WeakReference<>(loader); } public LargeParcelable(Parcel in) { super(in); } public LargeParcelable(Parcelable parcelable) { mParcelable = parcelable; } /** * Returns {@code Parcelable} carried by this instance. */ public Parcelable getParcelable() { return mParcelable; } @Override protected void serialize(@NonNull Parcel dest, int flags) { int startPosition; if (DBG_PAYLOAD) { startPosition = dest.dataPosition(); } dest.writeParcelable(mParcelable, flags); if (DBG_PAYLOAD) { Slog.d(TAG, "serialize-payload, start:" + startPosition + " size:" + (dest.dataPosition() - startPosition)); } } @Override protected void serializeNullPayload(@NonNull Parcel dest) { int startPosition; if (DBG_PAYLOAD) { startPosition = dest.dataPosition(); } dest.writeParcelable(null, 0); if (DBG_PAYLOAD) { Slog.d(TAG, "serializeNullPayload-payload, start:" + startPosition + " size:" + (dest.dataPosition() - startPosition)); } } @Override protected void deserialize(@NonNull Parcel src) { // default class loader does not work as it may not be in boot class path. ClassLoader loader = (sClassLoader == null) ? null : sClassLoader.get(); int startPosition; if (DBG_PAYLOAD) { startPosition = src.dataPosition(); } mParcelable = src.readParcelable(loader); if (DBG_PAYLOAD) { Slog.d(TAG, "deserialize-payload, start:" + startPosition + " size:" + (src.dataPosition() - startPosition) + " mParcelable:" + mParcelable); } } public static final @NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public LargeParcelable[] newArray(int size) { return new LargeParcelable[size]; } @Override public LargeParcelable createFromParcel(@NonNull Parcel in) { return new LargeParcelable(in); } }; /** * @see #toLargeParcelable(Parcelable, Callable) */ @Nullable public static Parcelable toLargeParcelable(@Nullable Parcelable p) { return toLargeParcelable(p, null); } /** * Prepare a {@code Parcelable} defined from Stable AIDL to be able to sent through binder. * *

The {@code Parcelable} should have a public member having name of * {@link #STABLE_AIDL_SHARED_MEMORY_MEMBER} with {@code ParcelFileDescriptor} type. * *

If payload size is big, the input would be serialized to a shared memory file and a * an empty {@code Parcelable} with only the file descriptor set would be returned. If the * payload size is small enough to be sent across binder or the input already contains a shared * memory file, the original input would be returned. * * @param p {@code Parcelable} the input to convert that might contain large data. * @param constructEmptyParcelable a callable to create an empty Parcelable with the same type * as input. If this is null, the default initializer for the input type would be used. * @return a {@code Parcelable} that could be sent through binder despite memory limitation. */ @Nullable public static Parcelable toLargeParcelable( @Nullable Parcelable p, @Nullable Callable constructEmptyParcelable) { if (p == null) { return null; } Class parcelableClass = p.getClass(); if (DBG_STABLE_AIDL_CLASS) { Slog.d(TAG, "toLargeParcelable stable AIDL Parcelable:" + parcelableClass.getSimpleName()); } Field field; ParcelFileDescriptor sharedMemoryFd; try { field = parcelableClass.getField(STABLE_AIDL_SHARED_MEMORY_MEMBER); sharedMemoryFd = (ParcelFileDescriptor) field.get(p); } catch (Exception e) { throw new IllegalArgumentException("Cannot access " + STABLE_AIDL_SHARED_MEMORY_MEMBER, e); } if (sharedMemoryFd != null) { return p; } Parcel dataParcel = Parcel.obtain(); p.writeToParcel(dataParcel, 0); int payloadSize = dataParcel.dataSize(); if (payloadSize <= LargeParcelableBase.MAX_DIRECT_PAYLOAD_SIZE) { // direct path, no re-write to shared memory. if (DBG_PAYLOAD) { Slog.d(TAG, "toLargeParcelable send directly, payload size:" + payloadSize); } return p; } SharedMemory memory = LargeParcelableBase.serializeParcelToSharedMemory(dataParcel); dataParcel.recycle(); try { sharedMemoryFd = SharedMemoryHelper.createParcelFileDescriptor(memory); } catch (IOException e) { throw new IllegalArgumentException("unable to duplicate shared memory fd", e); } Parcelable emptyPayload; if (constructEmptyParcelable != null) { try { emptyPayload = constructEmptyParcelable.call(); } catch (Exception e) { throw new IllegalArgumentException("Cannot use Parcelable constructor", e); } } else { try { emptyPayload = (Parcelable) parcelableClass.newInstance(); } catch (Exception e) { throw new IllegalArgumentException("Cannot access Parcelable constructor", e); } } try { field.set(emptyPayload, sharedMemoryFd); } catch (Exception e) { throw new IllegalArgumentException("Cannot access Parcelable member for FD", e); } return emptyPayload; } /** * Reconstructs {@code Parcelable} defined from Stable AIDL. It should have a {@link * ParcelFileDescriptor} member named {@link #STABLE_AIDL_SHARED_MEMORY_MEMBER} and will create * a new {@code Parcelable} if the shared memory portion is not null. If there is no shared * memory, it will return the original {@code Parcelable p} as it is. * * @param p Original {@code Parcelable} containing the payload. * @param keepSharedMemory Whether to keep created shared memory in the returned {@code * Parcelable}. Set to {@code true} if this {@code Parcelable} is sent * across binder repeatedly. * @return a new {@code Parcelable} if payload read from shared memory or old one if payload * is small enough. */ public static @Nullable Parcelable reconstructStableAIDLParcelable(@Nullable Parcelable p, boolean keepSharedMemory) { if (p == null) { return null; } Class parcelableClass = p.getClass(); if (DBG_STABLE_AIDL_CLASS) { Slog.d(TAG, "reconstructStableAIDLParcelable stable AIDL Parcelable:" + parcelableClass.getSimpleName()); } ParcelFileDescriptor sharedMemoryFd = null; Field fieldSharedMemory; try { fieldSharedMemory = parcelableClass.getField(STABLE_AIDL_SHARED_MEMORY_MEMBER); sharedMemoryFd = (ParcelFileDescriptor) fieldSharedMemory.get(p); } catch (Exception e) { throw new IllegalArgumentException("Cannot access " + STABLE_AIDL_SHARED_MEMORY_MEMBER, e); } if (sharedMemoryFd == null) { if (DBG_PAYLOAD) { Slog.d(TAG, "reconstructStableAIDLParcelable null shared memory"); } return p; } Parcel in = null; Parcelable retParcelable; try { // SharedMemory.fromFileDescriptor take ownership, so we need to dupe to keep // sharedMemoryFd the Parcelable valid. SharedMemory memory = SharedMemory.fromFileDescriptor(sharedMemoryFd.dup()); in = LargeParcelableBase.copyFromSharedMemory(memory); retParcelable = (Parcelable) parcelableClass.newInstance(); // runs retParcelable.readFromParcel(in) Method readMethod = parcelableClass.getMethod(STABLE_AIDL_PARCELABLE_READ_FROM_PARCEL, new Class[]{Parcel.class}); readMethod.invoke(retParcelable, in); if (keepSharedMemory) { fieldSharedMemory.set(retParcelable, sharedMemoryFd); } if (DBG_PAYLOAD) { Slog.d(TAG, "reconstructStableAIDLParcelable read shared memory, data size:" + in.dataPosition()); } } catch (Exception e) { throw new IllegalArgumentException("Cannot access Parcelable constructor/method", e); } finally { if (in != null) { in.recycle(); } } return retParcelable; } }