/* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.libraries.mobiledatadownload.file.openers; import android.content.Context; import android.util.Log; import androidx.annotation.VisibleForTesting; import com.google.android.libraries.mobiledatadownload.file.OpenContext; import com.google.android.libraries.mobiledatadownload.file.Opener; import com.google.android.libraries.mobiledatadownload.file.common.FileConvertible; import com.google.android.libraries.mobiledatadownload.file.common.ReleasableResource; import com.google.android.libraries.mobiledatadownload.file.common.UnsupportedFileStorageOperation; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.Nullable; /** * Opener for reading data from a {@link java.io.File} object. Depending on the backend, this may * return... * *
    *
  1. The simple posix path. *
  2. A path to a FIFO (named pipe) from which data can be streamed. *
* * Note that the second option is disabled by default, and must be turned on with {@link * #withFallbackToPipeUsingExecutor}. * *

Usage: * File file = storage.open(uri, * ReadFileOpener.create().withFallbackToPipeUsingExecutor(executor, context))); * try (FileInputStream in = new FileInputStream(file)) { * // Read file * } * */ public final class ReadFileOpener implements Opener { private static final String TAG = "ReadFileOpener"; private static final int STREAM_BUFFER_SIZE = 4096; private static final AtomicInteger FIFO_COUNTER = new AtomicInteger(); @Nullable private ExecutorService executor; @Nullable private Context context; @Nullable private Future pumpFuture; private boolean shortCircuit = false; private ReadFileOpener() {} public static ReadFileOpener create() { return new ReadFileOpener(); } /** * If enabled, still try to return a raw file path but, if that fails, return a FIFO (aka named * pipe) from which the data can be consumed as a stream. Raw file paths are not available if * there are any transforms installed; if there are any monitors installed; or if the backend * lacks such support. * *

The caller MUST open the returned file in order to avoid a thread leak. It may only open it * once. * *

The caller may block on {@link #waitForPump} and handle any exceptions in order to monitor * failures. * *

WARNING: FIFOs require SDK level 21+ (Lollipop). If the raw file path is unavailable and the * current SDK level is insufficient for FIFOs, the fallback will fail (throw IOException). * * @param executor Executor for pump threads. * @param context Android context for the root directory where fifos are stored. * @return This opener. */ @CanIgnoreReturnValue public ReadFileOpener withFallbackToPipeUsingExecutor(ExecutorService executor, Context context) { this.executor = executor; this.context = context; return this; } /** * If enabled, will ONLY attempt to convert the URI to a path using string processing. Fails if * there are any transforms enabled. This is like the {@link UriAdapter} interface, but with more * guard rails to make it safe to expose publicly. */ @CanIgnoreReturnValue public ReadFileOpener withShortCircuit() { this.shortCircuit = true; return this; } @Override public File open(OpenContext openContext) throws IOException { if (shortCircuit) { if (openContext.hasTransforms()) { throw new UnsupportedFileStorageOperation("Short circuit would skip transforms."); } return openContext.backend().toFile(openContext.encodedUri()); } try (ReleasableResource in = ReleasableResource.create(ReadStreamOpener.create().open(openContext))) { // TODO(b/118888044): FileConvertible probably can be deprecated. if (in.get() instanceof FileConvertible) { return ((FileConvertible) in.get()).toFile(); } if (executor != null) { return pipeToFile(in.release()); } throw new IOException("Not convertible and fallback to pipe is disabled."); } } /** Wait for pump and propagate any exceptions it may have encountered. */ @VisibleForTesting void waitForPump() throws IOException { Pipes.getAndPropagateAsIOException(pumpFuture); } private File pipeToFile(InputStream in) throws IOException { File fifo = Pipes.makeFifo(context.getCacheDir(), TAG, FIFO_COUNTER); pumpFuture = executor.submit( () -> { try (FileOutputStream out = new FileOutputStream(fifo)) { // In order to reach this point, reader must have opened the FIFO, so it's ok // to delete it. fifo.delete(); byte[] tmp = new byte[STREAM_BUFFER_SIZE]; try { int len; while ((len = in.read(tmp)) != -1) { out.write(tmp, 0, len); } out.flush(); } finally { in.close(); } } catch (IOException e) { Log.w(TAG, "pump", e); return e; } catch (Throwable t) { Log.e(TAG, "pump", t); return t; } return null; }); return fifo; } }