• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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