aboutsummaryrefslogtreecommitdiff
path: root/shadows/framework/src/main/java/org/robolectric/shadows/ShadowParcelFileDescriptor.java
blob: 08cdae467a53c16c736f3e274348fcb0aa1cec51 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
package org.robolectric.shadows;

import static android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE;
import static org.robolectric.shadow.api.Shadow.invokeConstructor;
import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
import static org.robolectric.util.reflector.Reflector.reflector;

import android.annotation.SuppressLint;
import android.os.Handler;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
import android.system.Os;
import com.google.common.io.ByteStreams;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;

@Implements(ParcelFileDescriptor.class)
@SuppressLint("NewApi")
public class ShadowParcelFileDescriptor {
  // TODO: consider removing this shadow in favor of shadowing file operations at the libcore.os
  // level
  private static final String PIPE_TMP_DIR = "ShadowParcelFileDescriptor";
  private static final String PIPE_FILE_NAME = "pipe";
  private static final Map<Integer, RandomAccessFile> filesInTransitById =
      Collections.synchronizedMap(new HashMap<>());
  private static final AtomicInteger NEXT_FILE_ID = new AtomicInteger();

  private RandomAccessFile file;
  private int fileIdPledgedOnClose; // != 0 if 'file' was written to a Parcel.
  private int lazyFileId; // != 0 if we were created from a Parcel but don't own a 'file' yet.
  private boolean closed;
  private Handler handler;
  private ParcelFileDescriptor.OnCloseListener onCloseListener;

  @RealObject private ParcelFileDescriptor realParcelFd;
  @RealObject private ParcelFileDescriptor realObject;

  @Implementation
  protected static void __staticInitializer__() {
    Shadow.directInitialize(ParcelFileDescriptor.class);
    ReflectionHelpers.setStaticField(
        ParcelFileDescriptor.class, "CREATOR", ShadowParcelFileDescriptor.CREATOR);
  }

  @Resetter
  public static void reset() {
    filesInTransitById.clear();
  }

  @Implementation
  protected void __constructor__(ParcelFileDescriptor wrapped) {
    invokeConstructor(
        ParcelFileDescriptor.class, realObject, from(ParcelFileDescriptor.class, wrapped));
    if (wrapped != null) {
      ShadowParcelFileDescriptor shadowParcelFileDescriptor = Shadow.extract(wrapped);
      this.file = shadowParcelFileDescriptor.file;
    }
  }

  static final Parcelable.Creator<ParcelFileDescriptor> CREATOR =
      new Parcelable.Creator<ParcelFileDescriptor>() {
        @Override
        public ParcelFileDescriptor createFromParcel(Parcel source) {
          int fileId = source.readInt();
          ParcelFileDescriptor result = newParcelFileDescriptor();
          ShadowParcelFileDescriptor shadowResult = Shadow.extract(result);
          shadowResult.lazyFileId = fileId;
          return result;
        }

        @Override
        public ParcelFileDescriptor[] newArray(int size) {
          return new ParcelFileDescriptor[size];
        }
      };

  @Implementation
  protected void writeToParcel(Parcel out, int flags) {
    if (fileIdPledgedOnClose == 0) {
      fileIdPledgedOnClose = (lazyFileId != 0) ? lazyFileId : NEXT_FILE_ID.incrementAndGet();
    }
    out.writeInt(fileIdPledgedOnClose);

    if ((flags & PARCELABLE_WRITE_RETURN_VALUE) != 0) {
      try {
        close();
      } catch (IOException e) {
        // Close "quietly", just like Android does.
      }
    }
  }

  private static ParcelFileDescriptor newParcelFileDescriptor() {
    return new ParcelFileDescriptor(new FileDescriptor());
  }

  @Implementation
  protected static ParcelFileDescriptor open(File file, int mode) throws FileNotFoundException {
    ParcelFileDescriptor pfd = newParcelFileDescriptor();
    ShadowParcelFileDescriptor shadowParcelFileDescriptor = Shadow.extract(pfd);
    shadowParcelFileDescriptor.file = new RandomAccessFile(file, getFileMode(mode));
    if ((mode & ParcelFileDescriptor.MODE_TRUNCATE) != 0) {
      try {
        shadowParcelFileDescriptor.file.setLength(0);
      } catch (IOException ioe) {
        FileNotFoundException fnfe = new FileNotFoundException("Unable to truncate");
        fnfe.initCause(ioe);
        throw fnfe;
      }
    }
    if ((mode & ParcelFileDescriptor.MODE_APPEND) != 0) {
      try {
        shadowParcelFileDescriptor.file.seek(shadowParcelFileDescriptor.file.length());
      } catch (IOException ioe) {
        FileNotFoundException fnfe = new FileNotFoundException("Unable to append");
        fnfe.initCause(ioe);
        throw fnfe;
      }
    }
    return pfd;
  }

  @Implementation
  protected static ParcelFileDescriptor open(
      File file, int mode, Handler handler, ParcelFileDescriptor.OnCloseListener listener)
      throws IOException {
    if (handler == null) {
      throw new IllegalArgumentException("Handler must not be null");
    }
    if (listener == null) {
      throw new IllegalArgumentException("Listener must not be null");
    }
    ParcelFileDescriptor pfd = open(file, mode);
    ShadowParcelFileDescriptor shadowParcelFileDescriptor = Shadow.extract(pfd);
    shadowParcelFileDescriptor.handler = handler;
    shadowParcelFileDescriptor.onCloseListener = listener;
    return pfd;
  }

  private static String getFileMode(int mode) {
    if ((mode & ParcelFileDescriptor.MODE_CREATE) != 0) {
      return "rw";
    }
    switch (mode & ParcelFileDescriptor.MODE_READ_WRITE) {
      case ParcelFileDescriptor.MODE_READ_ONLY:
        return "r";
      case ParcelFileDescriptor.MODE_WRITE_ONLY:
      case ParcelFileDescriptor.MODE_READ_WRITE:
        return "rw";
    }
    return "rw";
  }

  @Implementation
  protected static ParcelFileDescriptor[] createPipe() throws IOException {
    File file =
        new File(
            RuntimeEnvironment.getTempDirectory().createIfNotExists(PIPE_TMP_DIR).toFile(),
            PIPE_FILE_NAME + "-" + UUID.randomUUID());
    if (!file.createNewFile()) {
      throw new IOException("Cannot create pipe file: " + file.getAbsolutePath());
    }
    ParcelFileDescriptor readSide = open(file, ParcelFileDescriptor.MODE_READ_ONLY);
    ParcelFileDescriptor writeSide = open(file, ParcelFileDescriptor.MODE_READ_WRITE);
    file.deleteOnExit();
    return new ParcelFileDescriptor[] {readSide, writeSide};
  }

  @Implementation
  protected static ParcelFileDescriptor[] createReliablePipe() throws IOException {
    return createPipe();
  }

  private RandomAccessFile getFile() {
    if (file == null && lazyFileId != 0) {
      file = filesInTransitById.remove(lazyFileId);
      lazyFileId = 0;
      if (file == null) {
        throw new FileDescriptorFromParcelUnavailableException();
      }
    }
    return file;
  }

  @Implementation
  protected FileDescriptor getFileDescriptor() {
    try {
      RandomAccessFile file = getFile();
      if (file != null) {
        return file.getFD();
      }
      return reflector(ParcelFileDescriptorReflector.class, realParcelFd).getFileDescriptor();
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  @Implementation
  protected long getStatSize() {
    try {
      return getFile().length();
    } catch (IOException e) {
      // This might occur when the file object has been closed.
      return -1;
    }
  }

  @Implementation
  protected int getFd() {
    if (closed) {
      throw new IllegalStateException("Already closed");
    }

    try {
      return ReflectionHelpers.getField(getFile().getFD(), "fd");
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  @Implementation
  protected void close() throws IOException {
    // Act this status check the same as real close operation in AOSP.
    if (closed) {
      return;
    }

    if (file != null) {
      if (fileIdPledgedOnClose != 0) {
        // Don't actually close 'file'! Instead stash it where our Parcel reader(s) can find it.
        filesInTransitById.put(fileIdPledgedOnClose, file);
        fileIdPledgedOnClose = 0;

        // Replace this.file with a dummy instance to be close()d below. This leaves instances that
        // have been written to Parcels and never-parceled ones in exactly the same state.
        File tempFile = Files.createTempFile(null, null).toFile();
        file = new RandomAccessFile(tempFile, "rw");
        tempFile.delete();
      }
      file.close();
    }

    reflector(ParcelFileDescriptorReflector.class, realParcelFd).close();
    closed = true;
    if (handler != null && onCloseListener != null) {
      handler.post(() -> onCloseListener.onClose(null));
    }
  }

  @Implementation
  protected ParcelFileDescriptor dup() throws IOException {
    return new ParcelFileDescriptor(realParcelFd);
  }

  /**
   * Support shadowing of the static method {@link ParcelFileDescriptor#dup}.
   *
   * <p>The real implementation calls {@link Os#fcntlInt} in order to duplicate the FileDescriptor
   * in native code. This cannot be simulated on the JVM without the use of native code.
   */
  @Implementation
  protected static ParcelFileDescriptor dup(FileDescriptor fileDescriptor) throws IOException {
    File dupFile =
        new File(
            RuntimeEnvironment.getTempDirectory().createIfNotExists(PIPE_TMP_DIR).toFile(),
            "dupfd-" + UUID.randomUUID());

    // Duplicate the file represented by the file descriptor. Note that neither file streams should
    // be closed because doing so will invalidate the corresponding file descriptor.
    FileInputStream fileInputStream = new FileInputStream(fileDescriptor);
    FileOutputStream fileOutputStream = new FileOutputStream(dupFile);
    FileChannel sourceChannel = fileInputStream.getChannel();

    long originalPosition = sourceChannel.position();

    sourceChannel.position(0);
    ByteStreams.copy(fileInputStream, fileOutputStream);
    sourceChannel.position(originalPosition);
    RandomAccessFile randomAccessFile = new RandomAccessFile(dupFile, "rw");
    return new ParcelFileDescriptor(randomAccessFile.getFD());
  }

  static class FileDescriptorFromParcelUnavailableException extends RuntimeException {
    FileDescriptorFromParcelUnavailableException() {
      super(
          "ParcelFileDescriptors created from a Parcel refer to the same content as the"
              + " ParcelFileDescriptor that originally wrote it. Robolectric has the unfortunate"
              + " limitation that only one of these instances can be functional at a time. Try"
              + " closing the original ParcelFileDescriptor before using any duplicates created via"
              + " the Parcelable API.");
    }
  }

  @ForType(ParcelFileDescriptor.class)
  interface ParcelFileDescriptorReflector {

    @Direct
    void close();

    @Direct
    FileDescriptor getFileDescriptor();
  }
}