Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c47d12e5 authored by Tom Chan's avatar Tom Chan
Browse files

Allow WearableSensingService to read from disk via Context#openFileInput

The open file request will be proxied via the process that called
WearableSensingManager#provideDataStream or #provideConnection. This
follows a similar pattern as VisualQueryDetectionService.

Test: atest CtsWearableSensingServiceTestCases
Bug: 330701114
Change-Id: I5f16d8a96d098eca6dfbf4e6416d72bd8ec1f996
parent 1a9530df
Loading
Loading
Loading
Loading
+35 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * 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 android.app.wearable;

import android.os.ParcelFileDescriptor;

import com.android.internal.infra.AndroidFuture;

/**
 * Interface for callbacks coming from the WearableSensingService.
 *
 * @hide
 */
oneway interface IWearableSensingCallback {

    /**
     * Opens the requested file and returns the resulting ParcelFileDescriptor to the provided
     * future.
     */
    void openFile(in String filename, in AndroidFuture<ParcelFileDescriptor> future);
}
 No newline at end of file
+3 −2
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package android.app.wearable;

import android.app.PendingIntent;
import android.app.wearable.IWearableSensingCallback;
import android.content.ComponentName;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
@@ -30,9 +31,9 @@ import android.os.SharedMemory;
 */
interface IWearableSensingManager {
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)")
     void provideConnection(in ParcelFileDescriptor parcelFileDescriptor, in RemoteCallback callback);
     void provideConnection(in ParcelFileDescriptor parcelFileDescriptor, in IWearableSensingCallback wearableSensingCallback, in RemoteCallback statusCallback);
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)")
     void provideDataStream(in ParcelFileDescriptor parcelFileDescriptor, in RemoteCallback callback);
     void provideDataStream(in ParcelFileDescriptor parcelFileDescriptor, in @nullable IWearableSensingCallback wearableSensingCallback, in RemoteCallback statusCallback);
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)")
     void provideData(in PersistableBundle data, in SharedMemory sharedMemory, in RemoteCallback callback);
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)")
+96 −10
Original line number Diff line number Diff line
@@ -27,11 +27,15 @@ import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.app.PendingIntent;
import android.app.ambientcontext.AmbientContextEvent;
import android.app.compat.CompatChanges;
import android.companion.CompanionDeviceManager;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.RemoteCallback;
@@ -39,7 +43,13 @@ import android.os.RemoteException;
import android.os.SharedMemory;
import android.service.wearable.WearableSensingService;
import android.system.OsConstants;
import android.util.Slog;

import com.android.internal.infra.AndroidFuture;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Retention;
@@ -154,6 +164,17 @@ public class WearableSensingManager {
    @Retention(RetentionPolicy.SOURCE)
    public @interface StatusCode {}

    /**
     * If the WearableSensingService implementation belongs to the same APK as the caller, calling
     * {@link #provideDataStream(ParcelFileDescriptor, Executor, Consumer)} will allow
     * WearableSensingService to read from the caller's file directory via {@link
     * Context#openFileInput(String)}. The read will be proxied via the caller's process and
     * executed by the {@code executor} provided to this method.
     */
    @ChangeId
    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
    static final long ALLOW_WEARABLE_SENSING_SERVICE_FILE_READ = 330701114L;

    /**
     * Retrieves a {@link WearableSensingDataRequest} from the Intent sent to the PendingIntent
     * provided to {@link #registerDataRequestObserver(int, PendingIntent, Executor, Consumer)}.
@@ -169,6 +190,7 @@ public class WearableSensingManager {
                EXTRA_WEARABLE_SENSING_DATA_REQUEST, WearableSensingDataRequest.class);
    }

    private static final String TAG = WearableSensingManager.class.getSimpleName();
    private final Context mContext;
    private final IWearableSensingManager mService;

@@ -216,6 +238,11 @@ public class WearableSensingManager {
     * dropped during the restart. The caller is responsible for ensuring other method calls are
     * queued until a success status is returned from the {@code statusConsumer}.
     *
     * <p>If the WearableSensingService implementation belongs to the same APK as the caller,
     * calling this method will allow WearableSensingService to read from the caller's file
     * directory via {@link Context#openFileInput(String)}. The read will be proxied via the
     * caller's process and executed by the {@code executor} provided to this method.
     *
     * @param wearableConnection The connection to provide
     * @param executor Executor on which to run the consumer callback
     * @param statusConsumer A consumer that handles the status codes for providing the connection
@@ -227,9 +254,14 @@ public class WearableSensingManager {
            @NonNull ParcelFileDescriptor wearableConnection,
            @NonNull @CallbackExecutor Executor executor,
            @NonNull @StatusCode Consumer<Integer> statusConsumer) {
        RemoteCallback statusCallback = createStatusCallback(executor, statusConsumer);
        try {
            RemoteCallback callback = createStatusCallback(executor, statusConsumer);
            mService.provideConnection(wearableConnection, callback);
            // The wearableSensingCallback is included in this method call even though it is not
            // semantically related to the connection because we want to avoid race conditions
            // during the process restart triggered by this method call. See
            // com.android.server.wearable.RemoteWearableSensingService for details.
            mService.provideConnection(
                    wearableConnection, createWearableSensingCallback(executor), statusCallback);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
@@ -237,15 +269,21 @@ public class WearableSensingManager {

    /**
     * Provides a data stream to the WearableSensingService that's backed by the
     * parcelFileDescriptor, and sends the result to the {@link Consumer} right after the call.
     * This is used by applications that will also provide an implementation of
     * an isolated WearableSensingService. If the data stream was provided successfully
     * {@link WearableSensingManager#STATUS_SUCCESS} will be provided.
     * parcelFileDescriptor, and sends the result to the {@link Consumer} right after the call. This
     * is used by applications that will also provide an implementation of an isolated
     * WearableSensingService. If the data stream was provided successfully {@link
     * WearableSensingManager#STATUS_SUCCESS} will be provided.
     *
     * <p>Starting from target SDK level 35, if the WearableSensingService implementation belongs to
     * the same APK as the caller, calling this method will allow WearableSensingService to read
     * from the caller's file directory via {@link Context#openFileInput(String)}. The read will be
     * proxied via the caller's process and executed by the {@code executor} provided to this
     * method.
     *
     * @param parcelFileDescriptor The data stream to provide
     * @param executor Executor on which to run the consumer callback
     * @param statusConsumer A consumer that handles the status codes, which is returned
     *                 right after the call.
     * @param statusConsumer A consumer that handles the status codes, which is returned right after
     *     the call.
     * @deprecated Use {@link #provideConnection(ParcelFileDescriptor, Executor, Consumer)} instead
     *     to provide a remote wearable device connection to the WearableSensingService
     */
@@ -255,9 +293,14 @@ public class WearableSensingManager {
            @NonNull ParcelFileDescriptor parcelFileDescriptor,
            @NonNull @CallbackExecutor Executor executor,
            @NonNull @StatusCode Consumer<Integer> statusConsumer) {
        RemoteCallback statusCallback = createStatusCallback(executor, statusConsumer);
        IWearableSensingCallback wearableSensingCallback = null;
        if (CompatChanges.isChangeEnabled(ALLOW_WEARABLE_SENSING_SERVICE_FILE_READ)) {
            wearableSensingCallback = createWearableSensingCallback(executor);
        }
        try {
            RemoteCallback callback = createStatusCallback(executor, statusConsumer);
            mService.provideDataStream(parcelFileDescriptor, callback);
            mService.provideDataStream(
                    parcelFileDescriptor, wearableSensingCallback, statusCallback);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
@@ -480,4 +523,47 @@ public class WearableSensingManager {
                    }
                });
    }

    private IWearableSensingCallback createWearableSensingCallback(Executor executor) {
        return new IWearableSensingCallback.Stub() {

            @Override
            public void openFile(String filename, AndroidFuture<ParcelFileDescriptor> future) {
                Slog.d(TAG, "IWearableSensingCallback#openFile " + filename);
                Binder.withCleanCallingIdentity(
                        () ->
                                executor.execute(
                                        () -> {
                                            File file = new File(mContext.getFilesDir(), filename);
                                            ParcelFileDescriptor pfd = null;
                                            try {
                                                pfd =
                                                        ParcelFileDescriptor.open(
                                                                file,
                                                                ParcelFileDescriptor
                                                                        .MODE_READ_ONLY);
                                                Slog.d(
                                                        TAG,
                                                        "Successfully opened a file with"
                                                                + " ParcelFileDescriptor.");
                                            } catch (FileNotFoundException e) {
                                                Slog.e(TAG, "Cannot open file.", e);
                                            } finally {
                                                future.complete(pfd);
                                                if (pfd != null) {
                                                    try {
                                                        pfd.close();
                                                    } catch (IOException ex) {
                                                        Slog.e(
                                                                TAG,
                                                                "Error closing"
                                                                        + " ParcelFileDescriptor.",
                                                                ex);
                                                    }
                                                }
                                            }
                                        }));
            }
        };
    }
}
+3 −2
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package android.service.wearable;

import android.app.ambientcontext.AmbientContextEventRequest;
import android.app.wearable.IWearableSensingCallback;
import android.os.PersistableBundle;
import android.os.RemoteCallback;
import android.os.SharedMemory;
@@ -28,8 +29,8 @@ import android.os.SharedMemory;
 * @hide
 */
oneway interface IWearableSensingService {
    void provideSecureConnection(in ParcelFileDescriptor parcelFileDescriptor, in RemoteCallback callback);
    void provideDataStream(in ParcelFileDescriptor parcelFileDescriptor, in RemoteCallback callback);
    void provideSecureConnection(in ParcelFileDescriptor parcelFileDescriptor, in IWearableSensingCallback wearableSensingCallback, in RemoteCallback statusCallback);
    void provideDataStream(in ParcelFileDescriptor parcelFileDescriptor, in IWearableSensingCallback wearableSensingCallback, in RemoteCallback statusCallback);
    void provideData(in PersistableBundle data, in SharedMemory sharedMemory, in RemoteCallback callback);
    void registerDataRequestObserver(int dataType, in RemoteCallback dataRequestCallback, int dataRequestObserverId, in String packageName, in RemoteCallback statusCallback);
    void unregisterDataRequestObserver(int dataType, int dataRequestObserverId, in String packageName, in RemoteCallback statusCallback);
+88 −2
Original line number Diff line number Diff line
@@ -20,13 +20,16 @@ import android.annotation.BinderThread;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.app.Service;
import android.app.ambientcontext.AmbientContextEvent;
import android.app.ambientcontext.AmbientContextEventRequest;
import android.app.wearable.Flags;
import android.app.wearable.IWearableSensingCallback;
import android.app.wearable.WearableSensingDataRequest;
import android.app.wearable.WearableSensingManager;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
@@ -34,18 +37,28 @@ import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.Process;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.SharedMemory;
import android.service.ambientcontext.AmbientContextDetectionResult;
import android.service.ambientcontext.AmbientContextDetectionServiceStatus;
import android.service.voice.HotwordAudioStream;
import android.text.TextUtils;
import android.util.Slog;
import android.util.SparseArray;

import com.android.internal.infra.AndroidFuture;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;

/**
@@ -102,9 +115,14 @@ public abstract class WearableSensingService extends Service {
    public static final String SERVICE_INTERFACE =
            "android.service.wearable.WearableSensingService";

    // Timeout to prevent thread from waiting on the openFile future indefinitely.
    private static final Duration OPEN_FILE_TIMEOUT = Duration.ofSeconds(5);

    private final SparseArray<WearableSensingDataRequester> mDataRequestObserverIdToRequesterMap =
            new SparseArray<>();

    private IWearableSensingCallback mWearableSensingCallback;

    @Nullable
    @Override
    public final IBinder onBind(@NonNull Intent intent) {
@@ -113,8 +131,13 @@ public abstract class WearableSensingService extends Service {
                /** {@inheritDoc} */
                @Override
                public void provideSecureConnection(
                        ParcelFileDescriptor secureWearableConnection, RemoteCallback callback) {
                        ParcelFileDescriptor secureWearableConnection,
                        IWearableSensingCallback wearableSensingCallback,
                        RemoteCallback callback) {
                    Objects.requireNonNull(secureWearableConnection);
                    if (wearableSensingCallback != null) {
                        mWearableSensingCallback = wearableSensingCallback;
                    }
                    Consumer<Integer> consumer = createWearableStatusConsumer(callback);
                    WearableSensingService.this.onSecureConnectionProvided(
                            secureWearableConnection, consumer);
@@ -123,8 +146,13 @@ public abstract class WearableSensingService extends Service {
                /** {@inheritDoc} */
                @Override
                public void provideDataStream(
                        ParcelFileDescriptor parcelFileDescriptor, RemoteCallback callback) {
                        ParcelFileDescriptor parcelFileDescriptor,
                        IWearableSensingCallback wearableSensingCallback,
                        RemoteCallback callback) {
                    Objects.requireNonNull(parcelFileDescriptor);
                    if (wearableSensingCallback != null) {
                        mWearableSensingCallback = wearableSensingCallback;
                    }
                    Consumer<Integer> consumer = createWearableStatusConsumer(callback);
                    WearableSensingService.this.onDataStreamProvided(
                            parcelFileDescriptor, consumer);
@@ -570,6 +598,64 @@ public abstract class WearableSensingService extends Service {
            @NonNull String packageName,
            @NonNull Consumer<AmbientContextDetectionServiceStatus> consumer);

    /**
     * Overrides {@link Context#openFileInput} to read files with the given {@code fileName} under
     * the internal app storage of the APK providing the implementation for this class. {@link
     * Context#getFilesDir()} will be added as a prefix to the provided {@code fileName}.
     *
     * <p>This method is only functional after {@link
     * #onSecureConnectionProvided(ParcelFileDescriptor, Consumer)} or {@link
     * #onDataStreamProvided(ParcelFileDescriptor, Consumer)} has been called as a result of a
     * process owned by the same APK calling {@link
     * WearableSensingManager#provideConnection(ParcelFileDescriptor, Executor, Consumer)} or {@link
     * WearableSensingManager#provideDataStream(ParcelFileDescriptor, Executor, Consumer)}.
     * Otherwise, it will throw an {@link IllegalStateException}. This is because this method
     * proxies the file read via that process. Also, the APK needs to have a targetSdkVersion of 35
     * or newer.
     *
     * @param fileName Relative path of a file under {@link Context#getFilesDir()}.
     * @throws IllegalStateException if the above condition is not satisfied.
     * @throws FileNotFoundException if the file does not exist or cannot be opened, or an error
     *     occurred during the RPC to proxy the file read via a non-isolated process.
     */
    // SuppressLint is needed because the parent Context class does not specify the nullability of
    // the parameter filename. If we remove the @NonNull annotation, the linter will complain about
    // MissingNullability
    @Override
    public @NonNull FileInputStream openFileInput(
            @SuppressLint("InvalidNullabilityOverride") @NonNull String fileName)
            throws FileNotFoundException {
        if (fileName == null) {
            throw new IllegalArgumentException("filename cannot be null");
        }
        try {
            if (mWearableSensingCallback == null) {
                throw new IllegalStateException(
                        "Cannot open file from WearableSensingService. WearableSensingCallback is"
                                + " not available.");
            }
            AndroidFuture<ParcelFileDescriptor> future = new AndroidFuture<>();
            mWearableSensingCallback.openFile(fileName, future);
            ParcelFileDescriptor pfd =
                    future.get(OPEN_FILE_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
            if (pfd == null) {
                throw new FileNotFoundException(
                        TextUtils.formatSimple(
                                "File %s not found or unable to be opened in read-only mode.",
                                fileName));
            }
            return new FileInputStream(pfd.getFileDescriptor());
        } catch (RemoteException | ExecutionException | TimeoutException e) {
            throw (FileNotFoundException)
                    new FileNotFoundException("Cannot open file due to remote service failure")
                            .initCause(e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw (FileNotFoundException)
                    new FileNotFoundException("Interrupted when opening a file.").initCause(e);
        }
    }

    @NonNull
    private static Integer[] intArrayToIntegerArray(@NonNull int[] integerSet) {
        Integer[] intArray = new Integer[integerSet.length];
Loading