• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2022 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  *      http://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.libraries.mobiledatadownload.file.openers;
17 
18 import android.content.Context;
19 import android.util.Log;
20 import androidx.annotation.VisibleForTesting;
21 import com.google.android.libraries.mobiledatadownload.file.OpenContext;
22 import com.google.android.libraries.mobiledatadownload.file.Opener;
23 import com.google.android.libraries.mobiledatadownload.file.common.FileConvertible;
24 import com.google.android.libraries.mobiledatadownload.file.common.ReleasableResource;
25 import com.google.android.libraries.mobiledatadownload.file.common.UnsupportedFileStorageOperation;
26 import com.google.errorprone.annotations.CanIgnoreReturnValue;
27 import java.io.File;
28 import java.io.FileOutputStream;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.util.concurrent.ExecutorService;
32 import java.util.concurrent.Future;
33 import java.util.concurrent.atomic.AtomicInteger;
34 import javax.annotation.Nullable;
35 
36 /**
37  * Opener for reading data from a {@link java.io.File} object. Depending on the backend, this may
38  * return...
39  *
40  * <ol>
41  *   <li>The simple posix path.
42  *   <li>A path to a FIFO (named pipe) from which data can be streamed.
43  * </ol>
44  *
45  * Note that the second option is disabled by default, and must be turned on with {@link
46  * #withFallbackToPipeUsingExecutor}.
47  *
48  * <p>Usage: <code>
49  * File file = storage.open(uri,
50  *    ReadFileOpener.create().withFallbackToPipeUsingExecutor(executor, context)));
51  * try (FileInputStream in = new FileInputStream(file)) {
52  *   // Read file
53  * }
54  * </code>
55  */
56 public final class ReadFileOpener implements Opener<File> {
57 
58   private static final String TAG = "ReadFileOpener";
59   private static final int STREAM_BUFFER_SIZE = 4096;
60   private static final AtomicInteger FIFO_COUNTER = new AtomicInteger();
61 
62   @Nullable private ExecutorService executor;
63   @Nullable private Context context;
64   @Nullable private Future<Throwable> pumpFuture;
65   private boolean shortCircuit = false;
66 
ReadFileOpener()67   private ReadFileOpener() {}
68 
create()69   public static ReadFileOpener create() {
70     return new ReadFileOpener();
71   }
72 
73   /**
74    * If enabled, still try to return a raw file path but, if that fails, return a FIFO (aka named
75    * pipe) from which the data can be consumed as a stream. Raw file paths are not available if
76    * there are any transforms installed; if there are any monitors installed; or if the backend
77    * lacks such support.
78    *
79    * <p>The caller MUST open the returned file in order to avoid a thread leak. It may only open it
80    * once.
81    *
82    * <p>The caller may block on {@link #waitForPump} and handle any exceptions in order to monitor
83    * failures.
84    *
85    * <p>WARNING: FIFOs require SDK level 21+ (Lollipop). If the raw file path is unavailable and the
86    * current SDK level is insufficient for FIFOs, the fallback will fail (throw IOException).
87    *
88    * @param executor Executor for pump threads.
89    * @param context Android context for the root directory where fifos are stored.
90    * @return This opener.
91    */
92   @CanIgnoreReturnValue
withFallbackToPipeUsingExecutor(ExecutorService executor, Context context)93   public ReadFileOpener withFallbackToPipeUsingExecutor(ExecutorService executor, Context context) {
94     this.executor = executor;
95     this.context = context;
96     return this;
97   }
98 
99   /**
100    * If enabled, will ONLY attempt to convert the URI to a path using string processing. Fails if
101    * there are any transforms enabled. This is like the {@link UriAdapter} interface, but with more
102    * guard rails to make it safe to expose publicly.
103    */
104   @CanIgnoreReturnValue
withShortCircuit()105   public ReadFileOpener withShortCircuit() {
106     this.shortCircuit = true;
107     return this;
108   }
109 
110   @Override
open(OpenContext openContext)111   public File open(OpenContext openContext) throws IOException {
112     if (shortCircuit) {
113       if (openContext.hasTransforms()) {
114         throw new UnsupportedFileStorageOperation("Short circuit would skip transforms.");
115       }
116       return openContext.backend().toFile(openContext.encodedUri());
117     }
118 
119     try (ReleasableResource<InputStream> in =
120         ReleasableResource.create(ReadStreamOpener.create().open(openContext))) {
121       // TODO(b/118888044): FileConvertible probably can be deprecated.
122       if (in.get() instanceof FileConvertible) {
123         return ((FileConvertible) in.get()).toFile();
124       }
125       if (executor != null) {
126         return pipeToFile(in.release());
127       }
128       throw new IOException("Not convertible and fallback to pipe is disabled.");
129     }
130   }
131 
132   /** Wait for pump and propagate any exceptions it may have encountered. */
133   @VisibleForTesting
waitForPump()134   void waitForPump() throws IOException {
135     Pipes.getAndPropagateAsIOException(pumpFuture);
136   }
137 
pipeToFile(InputStream in)138   private File pipeToFile(InputStream in) throws IOException {
139     File fifo = Pipes.makeFifo(context.getCacheDir(), TAG, FIFO_COUNTER);
140     pumpFuture =
141         executor.submit(
142             () -> {
143               try (FileOutputStream out = new FileOutputStream(fifo)) {
144                 // In order to reach this point, reader must have opened the FIFO, so it's ok
145                 // to delete it.
146                 fifo.delete();
147                 byte[] tmp = new byte[STREAM_BUFFER_SIZE];
148                 try {
149                   int len;
150                   while ((len = in.read(tmp)) != -1) {
151                     out.write(tmp, 0, len);
152                   }
153                   out.flush();
154                 } finally {
155                   in.close();
156                 }
157               } catch (IOException e) {
158                 Log.w(TAG, "pump", e);
159                 return e;
160               } catch (Throwable t) {
161                 Log.e(TAG, "pump", t);
162                 return t;
163               }
164               return null;
165             });
166     return fifo;
167   }
168 }
169