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