1 package org.robolectric.shadows; 2 3 import static android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE; 4 import static org.robolectric.shadow.api.Shadow.invokeConstructor; 5 import static org.robolectric.util.ReflectionHelpers.ClassParameter.from; 6 import static org.robolectric.util.reflector.Reflector.reflector; 7 8 import android.annotation.SuppressLint; 9 import android.os.Handler; 10 import android.os.Parcel; 11 import android.os.ParcelFileDescriptor; 12 import android.os.Parcelable; 13 import android.system.Os; 14 import com.google.common.io.ByteStreams; 15 import java.io.File; 16 import java.io.FileDescriptor; 17 import java.io.FileInputStream; 18 import java.io.FileNotFoundException; 19 import java.io.FileOutputStream; 20 import java.io.IOException; 21 import java.io.RandomAccessFile; 22 import java.nio.channels.FileChannel; 23 import java.nio.file.Files; 24 import java.util.Collections; 25 import java.util.HashMap; 26 import java.util.Map; 27 import java.util.UUID; 28 import java.util.concurrent.atomic.AtomicInteger; 29 import org.robolectric.RuntimeEnvironment; 30 import org.robolectric.annotation.Implementation; 31 import org.robolectric.annotation.Implements; 32 import org.robolectric.annotation.RealObject; 33 import org.robolectric.annotation.Resetter; 34 import org.robolectric.shadow.api.Shadow; 35 import org.robolectric.util.ReflectionHelpers; 36 import org.robolectric.util.reflector.Direct; 37 import org.robolectric.util.reflector.ForType; 38 39 @Implements(ParcelFileDescriptor.class) 40 @SuppressLint("NewApi") 41 public class ShadowParcelFileDescriptor { 42 // TODO: consider removing this shadow in favor of shadowing file operations at the libcore.os 43 // level 44 private static final String PIPE_TMP_DIR = "ShadowParcelFileDescriptor"; 45 private static final String PIPE_FILE_NAME = "pipe"; 46 private static final Map<Integer, RandomAccessFile> filesInTransitById = 47 Collections.synchronizedMap(new HashMap<>()); 48 private static final AtomicInteger NEXT_FILE_ID = new AtomicInteger(); 49 50 private RandomAccessFile file; 51 private int fileIdPledgedOnClose; // != 0 if 'file' was written to a Parcel. 52 private int lazyFileId; // != 0 if we were created from a Parcel but don't own a 'file' yet. 53 private boolean closed; 54 private Handler handler; 55 private ParcelFileDescriptor.OnCloseListener onCloseListener; 56 57 @RealObject private ParcelFileDescriptor realParcelFd; 58 @RealObject private ParcelFileDescriptor realObject; 59 60 @Implementation __staticInitializer__()61 protected static void __staticInitializer__() { 62 Shadow.directInitialize(ParcelFileDescriptor.class); 63 ReflectionHelpers.setStaticField( 64 ParcelFileDescriptor.class, "CREATOR", ShadowParcelFileDescriptor.CREATOR); 65 } 66 67 @Resetter reset()68 public static void reset() { 69 filesInTransitById.clear(); 70 } 71 72 @Implementation __constructor__(ParcelFileDescriptor wrapped)73 protected void __constructor__(ParcelFileDescriptor wrapped) { 74 invokeConstructor( 75 ParcelFileDescriptor.class, realObject, from(ParcelFileDescriptor.class, wrapped)); 76 if (wrapped != null) { 77 ShadowParcelFileDescriptor shadowParcelFileDescriptor = Shadow.extract(wrapped); 78 this.file = shadowParcelFileDescriptor.file; 79 } 80 } 81 82 static final Parcelable.Creator<ParcelFileDescriptor> CREATOR = 83 new Parcelable.Creator<ParcelFileDescriptor>() { 84 @Override 85 public ParcelFileDescriptor createFromParcel(Parcel source) { 86 int fileId = source.readInt(); 87 ParcelFileDescriptor result = newParcelFileDescriptor(); 88 ShadowParcelFileDescriptor shadowResult = Shadow.extract(result); 89 shadowResult.lazyFileId = fileId; 90 return result; 91 } 92 93 @Override 94 public ParcelFileDescriptor[] newArray(int size) { 95 return new ParcelFileDescriptor[size]; 96 } 97 }; 98 99 @Implementation writeToParcel(Parcel out, int flags)100 protected void writeToParcel(Parcel out, int flags) { 101 if (fileIdPledgedOnClose == 0) { 102 fileIdPledgedOnClose = (lazyFileId != 0) ? lazyFileId : NEXT_FILE_ID.incrementAndGet(); 103 } 104 out.writeInt(fileIdPledgedOnClose); 105 106 if ((flags & PARCELABLE_WRITE_RETURN_VALUE) != 0) { 107 try { 108 close(); 109 } catch (IOException e) { 110 // Close "quietly", just like Android does. 111 } 112 } 113 } 114 newParcelFileDescriptor()115 private static ParcelFileDescriptor newParcelFileDescriptor() { 116 return new ParcelFileDescriptor(new FileDescriptor()); 117 } 118 119 @Implementation open(File file, int mode)120 protected static ParcelFileDescriptor open(File file, int mode) throws FileNotFoundException { 121 ParcelFileDescriptor pfd = newParcelFileDescriptor(); 122 ShadowParcelFileDescriptor shadowParcelFileDescriptor = Shadow.extract(pfd); 123 shadowParcelFileDescriptor.file = new RandomAccessFile(file, getFileMode(mode)); 124 if ((mode & ParcelFileDescriptor.MODE_TRUNCATE) != 0) { 125 try { 126 shadowParcelFileDescriptor.file.setLength(0); 127 } catch (IOException ioe) { 128 FileNotFoundException fnfe = new FileNotFoundException("Unable to truncate"); 129 fnfe.initCause(ioe); 130 throw fnfe; 131 } 132 } 133 if ((mode & ParcelFileDescriptor.MODE_APPEND) != 0) { 134 try { 135 shadowParcelFileDescriptor.file.seek(shadowParcelFileDescriptor.file.length()); 136 } catch (IOException ioe) { 137 FileNotFoundException fnfe = new FileNotFoundException("Unable to append"); 138 fnfe.initCause(ioe); 139 throw fnfe; 140 } 141 } 142 return pfd; 143 } 144 145 @Implementation open( File file, int mode, Handler handler, ParcelFileDescriptor.OnCloseListener listener)146 protected static ParcelFileDescriptor open( 147 File file, int mode, Handler handler, ParcelFileDescriptor.OnCloseListener listener) 148 throws IOException { 149 if (handler == null) { 150 throw new IllegalArgumentException("Handler must not be null"); 151 } 152 if (listener == null) { 153 throw new IllegalArgumentException("Listener must not be null"); 154 } 155 ParcelFileDescriptor pfd = open(file, mode); 156 ShadowParcelFileDescriptor shadowParcelFileDescriptor = Shadow.extract(pfd); 157 shadowParcelFileDescriptor.handler = handler; 158 shadowParcelFileDescriptor.onCloseListener = listener; 159 return pfd; 160 } 161 getFileMode(int mode)162 private static String getFileMode(int mode) { 163 if ((mode & ParcelFileDescriptor.MODE_CREATE) != 0) { 164 return "rw"; 165 } 166 switch (mode & ParcelFileDescriptor.MODE_READ_WRITE) { 167 case ParcelFileDescriptor.MODE_READ_ONLY: 168 return "r"; 169 case ParcelFileDescriptor.MODE_WRITE_ONLY: 170 case ParcelFileDescriptor.MODE_READ_WRITE: 171 return "rw"; 172 } 173 return "rw"; 174 } 175 176 @Implementation createPipe()177 protected static ParcelFileDescriptor[] createPipe() throws IOException { 178 File file = 179 new File( 180 RuntimeEnvironment.getTempDirectory().createIfNotExists(PIPE_TMP_DIR).toFile(), 181 PIPE_FILE_NAME + "-" + UUID.randomUUID()); 182 if (!file.createNewFile()) { 183 throw new IOException("Cannot create pipe file: " + file.getAbsolutePath()); 184 } 185 ParcelFileDescriptor readSide = open(file, ParcelFileDescriptor.MODE_READ_ONLY); 186 ParcelFileDescriptor writeSide = open(file, ParcelFileDescriptor.MODE_READ_WRITE); 187 file.deleteOnExit(); 188 return new ParcelFileDescriptor[] {readSide, writeSide}; 189 } 190 191 @Implementation createReliablePipe()192 protected static ParcelFileDescriptor[] createReliablePipe() throws IOException { 193 return createPipe(); 194 } 195 getFile()196 private RandomAccessFile getFile() { 197 if (file == null && lazyFileId != 0) { 198 file = filesInTransitById.remove(lazyFileId); 199 lazyFileId = 0; 200 if (file == null) { 201 throw new FileDescriptorFromParcelUnavailableException(); 202 } 203 } 204 return file; 205 } 206 207 @Implementation getFileDescriptor()208 protected FileDescriptor getFileDescriptor() { 209 try { 210 RandomAccessFile file = getFile(); 211 if (file != null) { 212 return file.getFD(); 213 } 214 return reflector(ParcelFileDescriptorReflector.class, realParcelFd).getFileDescriptor(); 215 } catch (IOException e) { 216 throw new RuntimeException(e); 217 } 218 } 219 220 @Implementation getStatSize()221 protected long getStatSize() { 222 try { 223 return getFile().length(); 224 } catch (IOException e) { 225 // This might occur when the file object has been closed. 226 return -1; 227 } 228 } 229 230 @Implementation getFd()231 protected int getFd() { 232 if (closed) { 233 throw new IllegalStateException("Already closed"); 234 } 235 236 try { 237 return ReflectionHelpers.getField(getFile().getFD(), "fd"); 238 } catch (IOException e) { 239 throw new RuntimeException(e); 240 } 241 } 242 243 @Implementation close()244 protected void close() throws IOException { 245 // Act this status check the same as real close operation in AOSP. 246 if (closed) { 247 return; 248 } 249 250 if (file != null) { 251 if (fileIdPledgedOnClose != 0) { 252 // Don't actually close 'file'! Instead stash it where our Parcel reader(s) can find it. 253 filesInTransitById.put(fileIdPledgedOnClose, file); 254 fileIdPledgedOnClose = 0; 255 256 // Replace this.file with a dummy instance to be close()d below. This leaves instances that 257 // have been written to Parcels and never-parceled ones in exactly the same state. 258 File tempFile = Files.createTempFile(null, null).toFile(); 259 file = new RandomAccessFile(tempFile, "rw"); 260 tempFile.delete(); 261 } 262 file.close(); 263 } 264 265 reflector(ParcelFileDescriptorReflector.class, realParcelFd).close(); 266 closed = true; 267 if (handler != null && onCloseListener != null) { 268 handler.post(() -> onCloseListener.onClose(null)); 269 } 270 } 271 272 @Implementation dup()273 protected ParcelFileDescriptor dup() throws IOException { 274 return new ParcelFileDescriptor(realParcelFd); 275 } 276 277 /** 278 * Support shadowing of the static method {@link ParcelFileDescriptor#dup}. 279 * 280 * <p>The real implementation calls {@link Os#fcntlInt} in order to duplicate the FileDescriptor 281 * in native code. This cannot be simulated on the JVM without the use of native code. 282 */ 283 @Implementation dup(FileDescriptor fileDescriptor)284 protected static ParcelFileDescriptor dup(FileDescriptor fileDescriptor) throws IOException { 285 File dupFile = 286 new File( 287 RuntimeEnvironment.getTempDirectory().createIfNotExists(PIPE_TMP_DIR).toFile(), 288 "dupfd-" + UUID.randomUUID()); 289 290 // Duplicate the file represented by the file descriptor. Note that neither file streams should 291 // be closed because doing so will invalidate the corresponding file descriptor. 292 FileInputStream fileInputStream = new FileInputStream(fileDescriptor); 293 FileOutputStream fileOutputStream = new FileOutputStream(dupFile); 294 FileChannel sourceChannel = fileInputStream.getChannel(); 295 296 long originalPosition = sourceChannel.position(); 297 298 sourceChannel.position(0); 299 ByteStreams.copy(fileInputStream, fileOutputStream); 300 sourceChannel.position(originalPosition); 301 RandomAccessFile randomAccessFile = new RandomAccessFile(dupFile, "rw"); 302 return new ParcelFileDescriptor(randomAccessFile.getFD()); 303 } 304 305 static class FileDescriptorFromParcelUnavailableException extends RuntimeException { FileDescriptorFromParcelUnavailableException()306 FileDescriptorFromParcelUnavailableException() { 307 super( 308 "ParcelFileDescriptors created from a Parcel refer to the same content as the" 309 + " ParcelFileDescriptor that originally wrote it. Robolectric has the unfortunate" 310 + " limitation that only one of these instances can be functional at a time. Try" 311 + " closing the original ParcelFileDescriptor before using any duplicates created via" 312 + " the Parcelable API."); 313 } 314 } 315 316 @ForType(ParcelFileDescriptor.class) 317 interface ParcelFileDescriptorReflector { 318 319 @Direct close()320 void close(); 321 322 @Direct getFileDescriptor()323 FileDescriptor getFileDescriptor(); 324 } 325 } 326