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

Commit 0b65a14b authored by Tom Chan's avatar Tom Chan Committed by Android (Google) Code Review
Browse files

Merge "Allow WearableSensingService to read from disk via Context#openFileInput" into main

parents 3f4d7847 c47d12e5
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