1 /* 2 * Copyright 2021 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * https://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.enterprise.connectedapps.internal; 17 18 import android.os.Bundle; 19 import android.os.Parcel; 20 import com.google.android.enterprise.connectedapps.CrossProfileSender; 21 import java.nio.ByteBuffer; 22 import java.util.Arrays; 23 import java.util.HashMap; 24 import java.util.Map; 25 26 /** 27 * Build up parcels over multiple calls and prepare responses. 28 * 29 * <p>This is the counterpart to {@link BundleCallSender}. Calls by the {@link BundleCallSender} 30 * should be relayed to an instance of this class. 31 */ 32 public final class BundleCallReceiver { 33 34 static final byte STATUS_COMPLETE = 0; 35 static final byte STATUS_INCOMPLETE = 1; 36 static final byte STATUS_INCLUDES_BUNDLES = 2; 37 38 private final Map<Long, byte[]> preparedCalls = new HashMap<>(); 39 private final Map<Long, Integer> preparedCallParts = new HashMap<>(); 40 private final Map<Long, byte[]> preparedResponses = new HashMap<>(); 41 private final Map<Long, Bundle> preparedCallBundles = new HashMap<>(); 42 private final Map<Long, Bundle> preparedResponseBundles = new HashMap<>(); 43 44 /** 45 * Prepare a response to be returned by calls to {@link #getPreparedResponse(long, int)}. 46 * 47 * <p>The {@code byte[]} returned will begin with a 0 if all can be contained in a single call and 48 * 1 if further calls to {@link #getPreparedResponse(long, int)} are required. If the first byte 49 * is a 1, then the following 4 bytes will be an {@link Integer} representing the total number of 50 * bytes in the response. 51 * 52 * <p>The {@code byte[]} returned will consist only of a 2 if a bundle needs to be fetched using 53 * {@link #getPreparedResponseBundle(long, int)}. 54 */ prepareResponse(long callId, Bundle responseBundle)55 public byte[] prepareResponse(long callId, Bundle responseBundle) { 56 Parcel responseParcel = Parcel.obtain(); 57 responseBundle.writeToParcel(responseParcel, /* flags= */ 0); 58 59 byte[] responseBytes; 60 try { 61 responseBytes = responseParcel.marshall(); 62 } catch (Exception | AssertionError e) { 63 prepareResponseBundle(callId, /* bundleId= */ 0, responseBundle); 64 return new byte[] {STATUS_INCLUDES_BUNDLES}; 65 } finally { 66 responseParcel.recycle(); 67 } 68 69 if (responseBytes.length <= CrossProfileSender.MAX_BYTES_PER_BLOCK) { 70 return ByteUtilities.joinByteArrays(new byte[] {STATUS_COMPLETE}, responseBytes); 71 } 72 // Record the bytes to be sent and send the first block 73 preparedResponses.put(callId, responseBytes); 74 byte[] response = new byte[CrossProfileSender.MAX_BYTES_PER_BLOCK + 5]; 75 response[0] = STATUS_INCOMPLETE; 76 byte[] sizeBytes = ByteBuffer.allocate(4).putInt(responseBytes.length).array(); 77 System.arraycopy(sizeBytes, /* srcPos= */ 0, response, /* destPos= */ 1, /* length= */ 4); 78 System.arraycopy( 79 responseBytes, 80 /* srcPos= */ 0, 81 response, 82 /* destPos= */ 5, 83 /* length= */ CrossProfileSender.MAX_BYTES_PER_BLOCK); 84 return response; 85 } 86 prepareBundle(long callId, int bundleId, Bundle bundle)87 public void prepareBundle(long callId, int bundleId, Bundle bundle) { 88 if (preparedCallBundles.containsKey(callId)) { 89 throw new IllegalStateException("Already prepared bundle for call " + callId); 90 } 91 92 preparedCallBundles.put(callId, bundle); 93 } 94 prepareResponseBundle(long callId, int bundleId, Bundle bundle)95 private void prepareResponseBundle(long callId, int bundleId, Bundle bundle) { 96 if (preparedResponseBundles.containsKey(callId)) { 97 throw new IllegalStateException("Already prepared bundle for response " + callId); 98 } 99 100 preparedResponseBundles.put(callId, bundle); 101 } 102 getPreparedResponseBundle(long callId, int bundleId)103 public Bundle getPreparedResponseBundle(long callId, int bundleId) { 104 return preparedResponseBundles.remove(callId); 105 } 106 107 /** 108 * Prepare a call, storing one block of bytes for a call which will be completed with a call to 109 * {@link #getPreparedCall(long, int, byte[])}. 110 */ prepareCall(long callId, int blockId, int numBytes, byte[] paramBytes)111 public void prepareCall(long callId, int blockId, int numBytes, byte[] paramBytes) { 112 if (!preparedCalls.containsKey(callId)) { 113 preparedCalls.put(callId, new byte[numBytes]); 114 preparedCallParts.put(callId, 0); 115 } 116 System.arraycopy( 117 paramBytes, 118 /* srcPos= */ 0, 119 preparedCalls.get(callId), 120 /* destPos= */ blockId * CrossProfileSender.MAX_BYTES_PER_BLOCK, 121 /* length= */ CrossProfileSender.MAX_BYTES_PER_BLOCK); 122 preparedCallParts.put( 123 callId, 124 preparedCallParts.get(callId) 125 + 1 126 + blockId); // +1 to have a difference when preparing the 0th block 127 } 128 129 /** 130 * Fetch the full {@link Bundle} using bytes previously stored by calls to {@link 131 * #prepareCall(long, int, int, byte[])}. 132 * 133 * <p>If this is the only block, then the {@code paramBytes} will be unmarshalled directly into a 134 * {@link Bundle}. 135 * 136 * @throws IllegalStateException If this is not the only block, and any previous blocks are 137 * missing. 138 */ getPreparedCall(long callId, int blockId, byte[] paramBytes)139 public Bundle getPreparedCall(long callId, int blockId, byte[] paramBytes) { 140 if (bytesRefersToBundle(paramBytes)) { 141 return preparedCallBundles.remove(callId); 142 } 143 144 if (blockId > 0) { 145 int expectedBlocks = 0; 146 for (int i = 0; i < blockId; i++) { 147 expectedBlocks += 1 + i; 148 } 149 if (!preparedCallParts.containsKey(callId) 150 || expectedBlocks != preparedCallParts.get(callId)) { 151 throw new IllegalStateException("Call " + callId + " not prepared"); 152 } 153 byte[] fullParamBytes = preparedCalls.get(callId); 154 System.arraycopy( 155 paramBytes, 156 /* srcPos= */ 0, 157 fullParamBytes, 158 /* destPos= */ blockId * CrossProfileSender.MAX_BYTES_PER_BLOCK, 159 /* length= */ paramBytes.length); 160 paramBytes = fullParamBytes; 161 preparedCalls.remove(callId); 162 preparedCallParts.remove(callId); 163 } 164 165 Parcel parcel = Parcel.obtain(); 166 parcel.unmarshall(paramBytes, 0, paramBytes.length); 167 parcel.setDataPosition(0); 168 Bundle bundle = new Bundle(Bundler.class.getClassLoader()); 169 bundle.readFromParcel(parcel); 170 parcel.recycle(); 171 return bundle; 172 } 173 bytesRefersToBundle(byte[] bytes)174 static boolean bytesRefersToBundle(byte[] bytes) { 175 return bytes[0] == STATUS_INCLUDES_BUNDLES; 176 } 177 178 /** 179 * Get a block from a response previously prepared with {@link #prepareResponse(long, Bundle)}. 180 * 181 * <p>If this is the final block, then the prepared blocks will be dropped, and future calls to 182 * this method will fail. 183 */ getPreparedResponse(long callId, int blockId)184 public byte[] getPreparedResponse(long callId, int blockId) { 185 byte[] preparedBytes = preparedResponses.get(callId); 186 byte[] response = 187 Arrays.copyOfRange( 188 preparedBytes, 189 /* from= */ blockId * CrossProfileSender.MAX_BYTES_PER_BLOCK, 190 /* to= */ Math.min( 191 preparedBytes.length, (blockId + 1) * CrossProfileSender.MAX_BYTES_PER_BLOCK)); 192 int numberOfBlocks = 193 (int) Math.ceil(preparedBytes.length * 1.0 / CrossProfileSender.MAX_BYTES_PER_BLOCK); 194 if (blockId == numberOfBlocks - 1) { 195 preparedResponses.remove(callId); 196 } 197 return response; 198 } 199 } 200