diff options
31 files changed, 786 insertions, 1224 deletions
diff --git a/apct-tests/perftests/utils/src/android/perftests/utils/WindowPerfRunPreconditionBase.java b/apct-tests/perftests/utils/src/android/perftests/utils/WindowPerfRunPreconditionBase.java index 8d2ac0276592..330a19e03c39 100644 --- a/apct-tests/perftests/utils/src/android/perftests/utils/WindowPerfRunPreconditionBase.java +++ b/apct-tests/perftests/utils/src/android/perftests/utils/WindowPerfRunPreconditionBase.java @@ -89,7 +89,7 @@ public class WindowPerfRunPreconditionBase extends RunListener { navOverlay = WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY; break; } - executeShellCommand("cmd overlay enable-exclusive " + navOverlay); + executeShellCommand("cmd overlay enable-exclusive --category " + navOverlay); }); /** It only executes once before all tests. */ diff --git a/core/api/current.txt b/core/api/current.txt index d3d754c0d8a5..a21b6938c189 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -39026,7 +39026,9 @@ package android.speech { field public static final int ERROR_NO_MATCH = 7; // 0x7 field public static final int ERROR_RECOGNIZER_BUSY = 8; // 0x8 field public static final int ERROR_SERVER = 4; // 0x4 + field public static final int ERROR_SERVER_DISCONNECTED = 11; // 0xb field public static final int ERROR_SPEECH_TIMEOUT = 6; // 0x6 + field public static final int ERROR_TOO_MANY_REQUESTS = 10; // 0xa field public static final String RESULTS_RECOGNITION = "results_recognition"; } diff --git a/core/java/android/os/Trace.java b/core/java/android/os/Trace.java index 9c9e4995d673..c8cbc517b226 100644 --- a/core/java/android/os/Trace.java +++ b/core/java/android/os/Trace.java @@ -168,8 +168,10 @@ public final class Trace { } /** - * Set whether application tracing is allowed for this process. This is intended to be set - * once at application start-up time based on whether the application is debuggable. + * From Android S, this is no-op. + * + * Before, set whether application tracing is allowed for this process. This is intended to be + * set once at application start-up time based on whether the application is debuggable. * * @hide */ diff --git a/core/java/android/os/incremental/IncrementalFileStorages.java b/core/java/android/os/incremental/IncrementalFileStorages.java index f2fe71913bb1..a078e0434867 100644 --- a/core/java/android/os/incremental/IncrementalFileStorages.java +++ b/core/java/android/os/incremental/IncrementalFileStorages.java @@ -36,6 +36,7 @@ import android.annotation.Nullable; import android.content.Context; import android.content.pm.DataLoaderParams; import android.content.pm.IDataLoaderStatusListener; +import android.content.pm.IPackageLoadingProgressCallback; import android.content.pm.InstallationFileParcel; import java.io.File; @@ -71,7 +72,8 @@ public final class IncrementalFileStorages { @Nullable StorageHealthCheckParams healthCheckParams, @Nullable IStorageHealthListener healthListener, @NonNull List<InstallationFileParcel> addedFiles, - @NonNull PerUidReadTimeouts[] perUidReadTimeouts) throws IOException { + @NonNull PerUidReadTimeouts[] perUidReadTimeouts, + IPackageLoadingProgressCallback progressCallback) throws IOException { // TODO(b/136132412): validity check if session should not be incremental IncrementalManager incrementalManager = (IncrementalManager) context.getSystemService( Context.INCREMENTAL_SERVICE); @@ -95,6 +97,11 @@ public final class IncrementalFileStorages { throw new IOException("Unknown file location: " + file.location); } } + // Register progress loading callback after files have been added + if (progressCallback != null) { + incrementalManager.registerLoadingProgressCallback(stageDir.getAbsolutePath(), + progressCallback); + } result.startLoading(dataLoaderParams, statusListener, healthCheckParams, healthListener, perUidReadTimeouts); @@ -205,6 +212,7 @@ public final class IncrementalFileStorages { try { mDefaultStorage.unBind(mStageDir.getAbsolutePath()); + mDefaultStorage.unregisterLoadingProgressListener(); } catch (IOException ignored) { } mDefaultStorage = null; diff --git a/core/java/android/speech/IRecognitionService.aidl b/core/java/android/speech/IRecognitionService.aidl index f91e122ea9cb..cc1cdedd0f96 100644 --- a/core/java/android/speech/IRecognitionService.aidl +++ b/core/java/android/speech/IRecognitionService.aidl @@ -43,7 +43,7 @@ oneway interface IRecognitionService { * @param featureId The feature in the package */ void startListening(in Intent recognizerIntent, in IRecognitionListener listener, - String packageName, String featureId); + String packageName, String featureId, int callingUid); /** * Stops listening for speech. Speech captured so far will be recognized as @@ -62,6 +62,7 @@ oneway interface IRecognitionService { * @param listener to receive callbacks, note that this must be non-null * @param packageName the package name calling this API * @param featureId The feature in the package + * @param isShutdown Whether the cancellation is caused by a client calling #shutdown */ - void cancel(in IRecognitionListener listener, String packageName, String featureId); + void cancel(in IRecognitionListener listener, String packageName, String featureId, boolean isShutdown); } diff --git a/core/java/android/speech/IRecognitionServiceManager.aidl b/core/java/android/speech/IRecognitionServiceManager.aidl index 7158ba2f9f63..8e5292d1ddf1 100644 --- a/core/java/android/speech/IRecognitionServiceManager.aidl +++ b/core/java/android/speech/IRecognitionServiceManager.aidl @@ -16,6 +16,8 @@ package android.speech; +import android.content.ComponentName; + import android.speech.IRecognitionServiceManagerCallback; /** @@ -23,6 +25,10 @@ import android.speech.IRecognitionServiceManagerCallback; * * {@hide} */ -interface IRecognitionServiceManager { - void createSession(in IRecognitionServiceManagerCallback callback); +oneway interface IRecognitionServiceManager { + void createSession( + in ComponentName componentName, + in IBinder clientToken, + boolean onDevice, + in IRecognitionServiceManagerCallback callback); } diff --git a/core/java/android/speech/IRecognitionServiceManagerCallback.aidl b/core/java/android/speech/IRecognitionServiceManagerCallback.aidl index d760810deda8..26afdaafd919 100644 --- a/core/java/android/speech/IRecognitionServiceManagerCallback.aidl +++ b/core/java/android/speech/IRecognitionServiceManagerCallback.aidl @@ -25,5 +25,5 @@ import android.speech.IRecognitionService; */ oneway interface IRecognitionServiceManagerCallback { void onSuccess(in IRecognitionService service); - void onError(); + void onError(int errorCode); } diff --git a/core/java/android/speech/RecognitionService.java b/core/java/android/speech/RecognitionService.java index c97dbfe38ead..fd584f191743 100644 --- a/core/java/android/speech/RecognitionService.java +++ b/core/java/android/speech/RecognitionService.java @@ -105,17 +105,6 @@ public abstract class RecognitionService extends Service { int callingUid) { if (mCurrentCallback == null) { if (DBG) Log.d(TAG, "created new mCurrentCallback, listener = " + listener.asBinder()); - try { - listener.asBinder().linkToDeath(new IBinder.DeathRecipient() { - @Override - public void binderDied() { - mHandler.sendMessage(mHandler.obtainMessage(MSG_CANCEL, listener)); - } - }, 0); - } catch (RemoteException re) { - Log.e(TAG, "dead listener on startListening"); - return; - } mCurrentCallback = new Callback(listener, callingUid); RecognitionService.this.onStartListening(intent, mCurrentCallback); } else { @@ -352,7 +341,6 @@ public abstract class RecognitionService extends Service { * Return the Linux uid assigned to the process that sent you the current transaction that * is being processed. This is obtained from {@link Binder#getCallingUid()}. */ - // TODO(b/176578753): need to make sure this is fixed when proxied through system. public int getCallingUid() { return mCallingUid; } @@ -368,7 +356,7 @@ public abstract class RecognitionService extends Service { @Override public void startListening(Intent recognizerIntent, IRecognitionListener listener, - String packageName, String featureId) { + String packageName, String featureId, int callingUid) { Preconditions.checkNotNull(packageName); if (DBG) Log.d(TAG, "startListening called by:" + listener.asBinder()); @@ -377,7 +365,7 @@ public abstract class RecognitionService extends Service { packageName, featureId)) { service.mHandler.sendMessage(Message.obtain(service.mHandler, MSG_START_LISTENING, service.new StartListeningArgs( - recognizerIntent, listener, Binder.getCallingUid()))); + recognizerIntent, listener, callingUid))); } } @@ -397,7 +385,7 @@ public abstract class RecognitionService extends Service { @Override public void cancel(IRecognitionListener listener, String packageName, - String featureId) { + String featureId, boolean isShutdown) { Preconditions.checkNotNull(packageName); if (DBG) Log.d(TAG, "cancel called by:" + listener.asBinder()); diff --git a/core/java/android/speech/SpeechRecognizer.java b/core/java/android/speech/SpeechRecognizer.java index de879c63a1a6..850f997a2d2f 100644 --- a/core/java/android/speech/SpeechRecognizer.java +++ b/core/java/android/speech/SpeechRecognizer.java @@ -20,8 +20,8 @@ import android.annotation.NonNull; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.ServiceConnection; import android.content.pm.ResolveInfo; +import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -32,10 +32,9 @@ import android.os.ServiceManager; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; +import android.util.Slog; -import java.util.LinkedList; import java.util.List; -import java.util.Queue; /** * This class provides access to the speech recognition service. This service allows access to the @@ -107,6 +106,12 @@ public class SpeechRecognizer { /** Insufficient permissions */ public static final int ERROR_INSUFFICIENT_PERMISSIONS = 9; + /** Too many requests from the same client. */ + public static final int ERROR_TOO_MANY_REQUESTS = 10; + + /** Server has been disconnected, e.g. because the app has crashed. */ + public static final int ERROR_SERVER_DISCONNECTED = 11; + /** action codes */ private final static int MSG_START = 1; private final static int MSG_STOP = 2; @@ -116,9 +121,6 @@ public class SpeechRecognizer { /** The actual RecognitionService endpoint */ private IRecognitionService mService; - /** The connection to the actual service */ - private Connection mConnection; - /** Context with which the manager was created */ private final Context mContext; @@ -151,15 +153,11 @@ public class SpeechRecognizer { } }; - /** - * Temporary queue, saving the messages until the connection will be established, afterwards, - * only mHandler will receive the messages - */ - private final Queue<Message> mPendingTasks = new LinkedList<Message>(); - /** The Listener that will receive all the callbacks */ private final InternalListener mListener = new InternalListener(); + private final IBinder mClientToken = new Binder(); + /** * The right way to create a {@code SpeechRecognizer} is by using * {@link #createSpeechRecognizer} static factory method @@ -181,30 +179,6 @@ public class SpeechRecognizer { } /** - * Basic ServiceConnection that records the mService variable. Additionally, on creation it - * invokes the {@link IRecognitionService#startListening(Intent, IRecognitionListener)}. - */ - private class Connection implements ServiceConnection { - - public void onServiceConnected(final ComponentName name, final IBinder service) { - // always done on the application main thread, so no need to send message to mHandler - mService = IRecognitionService.Stub.asInterface(service); - if (DBG) Log.d(TAG, "onServiceConnected - Success"); - while (!mPendingTasks.isEmpty()) { - mHandler.sendMessage(mPendingTasks.poll()); - } - } - - public void onServiceDisconnected(final ComponentName name) { - // always done on the application main thread, so no need to send message to mHandler - mService = null; - mConnection = null; - mPendingTasks.clear(); - if (DBG) Log.d(TAG, "onServiceDisconnected - Success"); - } - } - - /** * Checks whether a speech recognition service is available on the system. If this method * returns {@code false}, {@link SpeechRecognizer#createSpeechRecognizer(Context)} will * fail. @@ -303,87 +277,52 @@ public class SpeechRecognizer { throw new IllegalArgumentException("intent must not be null"); } checkIsCalledFromMainThread(); - if (mConnection == null) { // first time connection - // TODO(b/176578753): both flows should go through system service. - if (mOnDevice) { - connectToSystemService(); - } else { - connectToService(); - } - } - putMessage(Message.obtain(mHandler, MSG_START, recognizerIntent)); - } - private void connectToSystemService() { - mManagerService = IRecognitionServiceManager.Stub.asInterface( - ServiceManager.getService(Context.SPEECH_RECOGNITION_SERVICE)); - - if (mManagerService == null) { - mListener.onError(ERROR_CLIENT); - return; - } - - try { - // TODO(b/176578753): this has to supply information on whether to use on-device impl. - mManagerService.createSession(new IRecognitionServiceManagerCallback.Stub(){ - @Override - public void onSuccess(IRecognitionService service) throws RemoteException { - mService = service; - } - - @Override - public void onError() throws RemoteException { - Log.e(TAG, "Bind to system recognition service failed"); - mListener.onError(ERROR_CLIENT); - } - }); - } catch (RemoteException e) { - e.rethrowFromSystemServer(); - } - } - - private void connectToService() { - mConnection = new Connection(); - - Intent serviceIntent = new Intent(RecognitionService.SERVICE_INTERFACE); - - if (mServiceComponent == null) { - String serviceComponent = Settings.Secure.getString(mContext.getContentResolver(), - Settings.Secure.VOICE_RECOGNITION_SERVICE); - - if (TextUtils.isEmpty(serviceComponent)) { - Log.e(TAG, "no selected voice recognition service"); - mListener.onError(ERROR_CLIENT); - return; + if (DBG) { + Slog.i(TAG, "#startListening called"); + if (mService == null) { + Slog.i(TAG, "Connection is not established yet"); } + } - serviceIntent.setComponent( - ComponentName.unflattenFromString(serviceComponent)); + if (mService == null) { + // First time connection: first establish a connection, then dispatch #startListening. + connectToSystemService( + () -> putMessage(Message.obtain(mHandler, MSG_START, recognizerIntent))); } else { - serviceIntent.setComponent(mServiceComponent); - } - if (!mContext.bindService(serviceIntent, mConnection, - Context.BIND_AUTO_CREATE | Context.BIND_INCLUDE_CAPABILITIES)) { - Log.e(TAG, "bind to recognition service failed"); - mConnection = null; - mService = null; - mListener.onError(ERROR_CLIENT); - return; + putMessage(Message.obtain(mHandler, MSG_START, recognizerIntent)); } } /** * Stops listening for speech. Speech captured so far will be recognized as if the user had - * stopped speaking at this point. Note that in the default case, this does not need to be - * called, as the speech endpointer will automatically stop the recognizer listening when it - * determines speech has completed. However, you can manipulate endpointer parameters directly - * using the intent extras defined in {@link RecognizerIntent}, in which case you may sometimes - * want to manually call this method to stop listening sooner. Please note that + * stopped speaking at this point. + * + * <p>Note that in the default case, this does not need to be called, as the speech endpointer + * will automatically stop the recognizer listening when it determines speech has completed. + * However, you can manipulate endpointer parameters directly using the intent extras defined in + * {@link RecognizerIntent}, in which case you may sometimes want to manually call this method + * to stop listening sooner. + * + * <p>Upon invocation clients must wait until {@link RecognitionListener#onResults} or + * {@link RecognitionListener#onError} are invoked before calling + * {@link SpeechRecognizer#startListening} again. Otherwise such an attempt would be rejected by + * recognition service. + * + * <p>Please note that * {@link #setRecognitionListener(RecognitionListener)} should be called beforehand, otherwise * no notifications will be received. */ public void stopListening() { checkIsCalledFromMainThread(); + + if (DBG) { + Slog.i(TAG, "#stopListening called"); + if (mService == null) { + Slog.i(TAG, "Connection is not established yet"); + } + } + putMessage(Message.obtain(mHandler, MSG_STOP)); } @@ -405,11 +344,7 @@ public class SpeechRecognizer { } private void putMessage(Message msg) { - if (mService == null) { - mPendingTasks.offer(msg); - } else { - mHandler.sendMessage(msg); - } + mHandler.sendMessage(msg); } /** sends the actual message to the service */ @@ -419,7 +354,7 @@ public class SpeechRecognizer { } try { mService.startListening(recognizerIntent, mListener, mContext.getOpPackageName(), - mContext.getAttributionTag()); + mContext.getAttributionTag(), android.os.Process.myUid()); if (DBG) Log.d(TAG, "service start listening command succeded"); } catch (final RemoteException e) { Log.e(TAG, "startListening() failed", e); @@ -448,7 +383,11 @@ public class SpeechRecognizer { return; } try { - mService.cancel(mListener, mContext.getOpPackageName(), mContext.getAttributionTag()); + mService.cancel( + mListener, + mContext.getOpPackageName(), + mContext.getAttributionTag(), + false /* isShutdown */); if (DBG) Log.d(TAG, "service cancel command succeded"); } catch (final RemoteException e) { Log.e(TAG, "cancel() failed", e); @@ -471,28 +410,94 @@ public class SpeechRecognizer { mListener.mInternalListener = listener; } - /** - * Destroys the {@code SpeechRecognizer} object. - */ + /** Destroys the {@code SpeechRecognizer} object. */ public void destroy() { if (mService != null) { try { mService.cancel(mListener, mContext.getOpPackageName(), - mContext.getAttributionTag()); + mContext.getAttributionTag(), true /* isShutdown */); } catch (final RemoteException e) { // Not important } } - if (mConnection != null) { - mContext.unbindService(mConnection); - } - mPendingTasks.clear(); mService = null; - mConnection = null; mListener.mInternalListener = null; } + /** Establishes a connection to system server proxy and initializes the session. */ + private void connectToSystemService(Runnable onSuccess) { + mManagerService = IRecognitionServiceManager.Stub.asInterface( + ServiceManager.getService(Context.SPEECH_RECOGNITION_SERVICE)); + + if (mManagerService == null) { + mListener.onError(ERROR_CLIENT); + return; + } + + ComponentName componentName = getSpeechRecognizerComponentName(); + + if (!mOnDevice && componentName == null) { + mListener.onError(ERROR_CLIENT); + return; + } + + try { + mManagerService.createSession( + componentName, + mClientToken, + mOnDevice, + new IRecognitionServiceManagerCallback.Stub(){ + @Override + public void onSuccess(IRecognitionService service) throws RemoteException { + mService = service; + onSuccess.run(); + } + + @Override + public void onError(int errorCode) throws RemoteException { + Log.e(TAG, "Bind to system recognition service failed"); + mListener.onError(errorCode); + } + }); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** + * Returns the component name to be used for establishing a connection, based on the parameters + * used during initialization. + * + * <p>Note the 3 different scenarios: + * <ol> + * <li>On-device speech recognizer which is determined by the manufacturer and not + * changeable by the user + * <li>Default user-selected speech recognizer as specified by + * {@code Settings.Secure.VOICE_RECOGNITION_SERVICE} + * <li>Custom speech recognizer supplied by the client. + */ + private ComponentName getSpeechRecognizerComponentName() { + if (mOnDevice) { + return null; + } + + if (mServiceComponent != null) { + return mServiceComponent; + } + + String serviceComponent = Settings.Secure.getString(mContext.getContentResolver(), + Settings.Secure.VOICE_RECOGNITION_SERVICE); + + if (TextUtils.isEmpty(serviceComponent)) { + Log.e(TAG, "no selected voice recognition service"); + mListener.onError(ERROR_CLIENT); + return null; + } + + return ComponentName.unflattenFromString(serviceComponent); + } + /** * Internal wrapper of IRecognitionListener which will propagate the results to * RecognitionListener diff --git a/core/java/android/uwb/OWNERS b/core/java/android/uwb/OWNERS index ea41c3984dfd..17936ae7bb73 100644 --- a/core/java/android/uwb/OWNERS +++ b/core/java/android/uwb/OWNERS @@ -1,5 +1,6 @@ bstack@google.com eliptus@google.com jsolnit@google.com +matbev@google.com siyuanh@google.com zachoverflow@google.com diff --git a/core/java/android/uwb/TEST_MAPPING b/core/java/android/uwb/TEST_MAPPING index 9e50bd64d089..08ed2c7b71d9 100644 --- a/core/java/android/uwb/TEST_MAPPING +++ b/core/java/android/uwb/TEST_MAPPING @@ -2,6 +2,9 @@ "presubmit": [ { "name": "UwbManagerTests" + }, + { + "name": "CtsUwbTestCases" } ] -}
\ No newline at end of file +} diff --git a/core/jni/android_os_Trace.cpp b/core/jni/android_os_Trace.cpp index 0f7611a8ead1..f67007cda209 100644 --- a/core/jni/android_os_Trace.cpp +++ b/core/jni/android_os_Trace.cpp @@ -83,7 +83,7 @@ static void android_os_Trace_nativeAsyncTraceEnd(JNIEnv* env, jclass, } static void android_os_Trace_nativeSetAppTracingAllowed(JNIEnv*, jclass, jboolean allowed) { - atrace_set_debuggable(allowed); + atrace_update_tags(); } static void android_os_Trace_nativeSetTracingEnabled(JNIEnv*, jclass, jboolean enabled) { diff --git a/core/tests/uwbtests/src/android/uwb/AngleMeasurementTest.java b/core/tests/uwbtests/src/android/uwb/AngleMeasurementTest.java deleted file mode 100644 index 7769c28202f2..000000000000 --- a/core/tests/uwbtests/src/android/uwb/AngleMeasurementTest.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2020 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.uwb; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -import android.os.Parcel; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Test of {@link AngleMeasurement}. - */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class AngleMeasurementTest { - private static final double EPSILON = 0.00000000001; - - @Test - public void testBuilder() { - double radians = 0.1234; - double errorRadians = 0.5678; - double confidence = 0.5; - - AngleMeasurement.Builder builder = new AngleMeasurement.Builder(); - tryBuild(builder, false); - - builder.setRadians(radians); - tryBuild(builder, false); - - builder.setErrorRadians(errorRadians); - tryBuild(builder, false); - - builder.setConfidenceLevel(confidence); - AngleMeasurement measurement = tryBuild(builder, true); - - assertEquals(measurement.getRadians(), radians, 0); - assertEquals(measurement.getErrorRadians(), errorRadians, 0); - assertEquals(measurement.getConfidenceLevel(), confidence, 0); - } - - private AngleMeasurement tryBuild(AngleMeasurement.Builder builder, boolean expectSuccess) { - AngleMeasurement measurement = null; - try { - measurement = builder.build(); - if (!expectSuccess) { - fail("Expected AngleMeasurement.Builder.build() to fail, but it succeeded"); - } - } catch (IllegalStateException e) { - if (expectSuccess) { - fail("Expected AngleMeasurement.Builder.build() to succeed, but it failed"); - } - } - return measurement; - } - - @Test - public void testParcel() { - Parcel parcel = Parcel.obtain(); - AngleMeasurement measurement = UwbTestUtils.getAngleMeasurement(); - measurement.writeToParcel(parcel, 0); - parcel.setDataPosition(0); - AngleMeasurement fromParcel = AngleMeasurement.CREATOR.createFromParcel(parcel); - assertEquals(measurement, fromParcel); - } -} diff --git a/core/tests/uwbtests/src/android/uwb/AngleOfArrivalMeasurementTest.java b/core/tests/uwbtests/src/android/uwb/AngleOfArrivalMeasurementTest.java deleted file mode 100644 index 9394dec7f46f..000000000000 --- a/core/tests/uwbtests/src/android/uwb/AngleOfArrivalMeasurementTest.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2020 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.uwb; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -import android.os.Parcel; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Test of {@link AngleOfArrivalMeasurement}. - */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class AngleOfArrivalMeasurementTest { - - @Test - public void testBuilder() { - AngleMeasurement azimuth = UwbTestUtils.getAngleMeasurement(); - AngleMeasurement altitude = UwbTestUtils.getAngleMeasurement(); - - AngleOfArrivalMeasurement.Builder builder = new AngleOfArrivalMeasurement.Builder(); - tryBuild(builder, false); - - builder.setAltitude(altitude); - tryBuild(builder, false); - - builder.setAzimuth(azimuth); - AngleOfArrivalMeasurement measurement = tryBuild(builder, true); - - assertEquals(azimuth, measurement.getAzimuth()); - assertEquals(altitude, measurement.getAltitude()); - } - - private AngleMeasurement getAngleMeasurement(double radian, double error, double confidence) { - return new AngleMeasurement.Builder() - .setRadians(radian) - .setErrorRadians(error) - .setConfidenceLevel(confidence) - .build(); - } - - private AngleOfArrivalMeasurement tryBuild(AngleOfArrivalMeasurement.Builder builder, - boolean expectSuccess) { - AngleOfArrivalMeasurement measurement = null; - try { - measurement = builder.build(); - if (!expectSuccess) { - fail("Expected AngleOfArrivalMeasurement.Builder.build() to fail"); - } - } catch (IllegalStateException e) { - if (expectSuccess) { - fail("Expected AngleOfArrivalMeasurement.Builder.build() to succeed"); - } - } - return measurement; - } - - @Test - public void testParcel() { - Parcel parcel = Parcel.obtain(); - AngleOfArrivalMeasurement measurement = UwbTestUtils.getAngleOfArrivalMeasurement(); - measurement.writeToParcel(parcel, 0); - parcel.setDataPosition(0); - AngleOfArrivalMeasurement fromParcel = - AngleOfArrivalMeasurement.CREATOR.createFromParcel(parcel); - assertEquals(measurement, fromParcel); - } -} diff --git a/core/tests/uwbtests/src/android/uwb/DistanceMeasurementTest.java b/core/tests/uwbtests/src/android/uwb/DistanceMeasurementTest.java deleted file mode 100644 index 439c884723be..000000000000 --- a/core/tests/uwbtests/src/android/uwb/DistanceMeasurementTest.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2020 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.uwb; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -import android.os.Parcel; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Test of {@link DistanceMeasurement}. - */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class DistanceMeasurementTest { - private static final double EPSILON = 0.00000000001; - - @Test - public void testBuilder() { - double meters = 0.12; - double error = 0.54; - double confidence = 0.99; - - DistanceMeasurement.Builder builder = new DistanceMeasurement.Builder(); - tryBuild(builder, false); - - builder.setMeters(meters); - tryBuild(builder, false); - - builder.setErrorMeters(error); - tryBuild(builder, false); - - builder.setConfidenceLevel(confidence); - DistanceMeasurement measurement = tryBuild(builder, true); - - assertEquals(meters, measurement.getMeters(), 0); - assertEquals(error, measurement.getErrorMeters(), 0); - assertEquals(confidence, measurement.getConfidenceLevel(), 0); - } - - private DistanceMeasurement tryBuild(DistanceMeasurement.Builder builder, - boolean expectSuccess) { - DistanceMeasurement measurement = null; - try { - measurement = builder.build(); - if (!expectSuccess) { - fail("Expected DistanceMeasurement.Builder.build() to fail"); - } - } catch (IllegalStateException e) { - if (expectSuccess) { - fail("Expected DistanceMeasurement.Builder.build() to succeed"); - } - } - return measurement; - } - - @Test - public void testParcel() { - Parcel parcel = Parcel.obtain(); - DistanceMeasurement measurement = UwbTestUtils.getDistanceMeasurement(); - measurement.writeToParcel(parcel, 0); - parcel.setDataPosition(0); - DistanceMeasurement fromParcel = - DistanceMeasurement.CREATOR.createFromParcel(parcel); - assertEquals(measurement, fromParcel); - } -} diff --git a/core/tests/uwbtests/src/android/uwb/RangingMeasurementTest.java b/core/tests/uwbtests/src/android/uwb/RangingMeasurementTest.java deleted file mode 100644 index edd4d08992ba..000000000000 --- a/core/tests/uwbtests/src/android/uwb/RangingMeasurementTest.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2020 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.uwb; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -import android.os.Parcel; -import android.os.SystemClock; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Test of {@link RangingMeasurement}. - */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class RangingMeasurementTest { - - @Test - public void testBuilder() { - int status = RangingMeasurement.RANGING_STATUS_SUCCESS; - UwbAddress address = UwbTestUtils.getUwbAddress(false); - long time = SystemClock.elapsedRealtimeNanos(); - AngleOfArrivalMeasurement angleMeasurement = UwbTestUtils.getAngleOfArrivalMeasurement(); - DistanceMeasurement distanceMeasurement = UwbTestUtils.getDistanceMeasurement(); - - RangingMeasurement.Builder builder = new RangingMeasurement.Builder(); - - builder.setStatus(status); - tryBuild(builder, false); - - builder.setElapsedRealtimeNanos(time); - tryBuild(builder, false); - - builder.setAngleOfArrivalMeasurement(angleMeasurement); - tryBuild(builder, false); - - builder.setDistanceMeasurement(distanceMeasurement); - tryBuild(builder, false); - - builder.setRemoteDeviceAddress(address); - RangingMeasurement measurement = tryBuild(builder, true); - - assertEquals(status, measurement.getStatus()); - assertEquals(address, measurement.getRemoteDeviceAddress()); - assertEquals(time, measurement.getElapsedRealtimeNanos()); - assertEquals(angleMeasurement, measurement.getAngleOfArrivalMeasurement()); - assertEquals(distanceMeasurement, measurement.getDistanceMeasurement()); - } - - private RangingMeasurement tryBuild(RangingMeasurement.Builder builder, - boolean expectSuccess) { - RangingMeasurement measurement = null; - try { - measurement = builder.build(); - if (!expectSuccess) { - fail("Expected RangingMeasurement.Builder.build() to fail"); - } - } catch (IllegalStateException e) { - if (expectSuccess) { - fail("Expected DistanceMeasurement.Builder.build() to succeed"); - } - } - return measurement; - } - - @Test - public void testParcel() { - Parcel parcel = Parcel.obtain(); - RangingMeasurement measurement = UwbTestUtils.getRangingMeasurement(); - measurement.writeToParcel(parcel, 0); - parcel.setDataPosition(0); - RangingMeasurement fromParcel = RangingMeasurement.CREATOR.createFromParcel(parcel); - assertEquals(measurement, fromParcel); - } -} diff --git a/core/tests/uwbtests/src/android/uwb/RangingReportTest.java b/core/tests/uwbtests/src/android/uwb/RangingReportTest.java deleted file mode 100644 index 64c48ba4b6f4..000000000000 --- a/core/tests/uwbtests/src/android/uwb/RangingReportTest.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2020 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.uwb; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -import android.os.Parcel; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.List; - -/** - * Test of {@link RangingReport}. - */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class RangingReportTest { - - @Test - public void testBuilder() { - List<RangingMeasurement> measurements = UwbTestUtils.getRangingMeasurements(5); - - RangingReport.Builder builder = new RangingReport.Builder(); - builder.addMeasurements(measurements); - RangingReport report = tryBuild(builder, true); - verifyMeasurementsEqual(measurements, report.getMeasurements()); - - - builder = new RangingReport.Builder(); - for (RangingMeasurement measurement : measurements) { - builder.addMeasurement(measurement); - } - report = tryBuild(builder, true); - verifyMeasurementsEqual(measurements, report.getMeasurements()); - } - - private void verifyMeasurementsEqual(List<RangingMeasurement> expected, - List<RangingMeasurement> actual) { - assertEquals(expected.size(), actual.size()); - for (int i = 0; i < expected.size(); i++) { - assertEquals(expected.get(i), actual.get(i)); - } - } - - private RangingReport tryBuild(RangingReport.Builder builder, - boolean expectSuccess) { - RangingReport report = null; - try { - report = builder.build(); - if (!expectSuccess) { - fail("Expected RangingReport.Builder.build() to fail"); - } - } catch (IllegalStateException e) { - if (expectSuccess) { - fail("Expected RangingReport.Builder.build() to succeed"); - } - } - return report; - } - - @Test - public void testParcel() { - Parcel parcel = Parcel.obtain(); - RangingReport report = UwbTestUtils.getRangingReports(5); - report.writeToParcel(parcel, 0); - parcel.setDataPosition(0); - RangingReport fromParcel = RangingReport.CREATOR.createFromParcel(parcel); - assertEquals(report, fromParcel); - } -} diff --git a/core/tests/uwbtests/src/android/uwb/RangingSessionTest.java b/core/tests/uwbtests/src/android/uwb/RangingSessionTest.java deleted file mode 100644 index e5eea26f5d11..000000000000 --- a/core/tests/uwbtests/src/android/uwb/RangingSessionTest.java +++ /dev/null @@ -1,378 +0,0 @@ -/* - * Copyright (C) 2020 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.uwb; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import android.os.PersistableBundle; -import android.os.RemoteException; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -import java.util.concurrent.Executor; - -/** - * Test of {@link RangingSession}. - */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class RangingSessionTest { - private static final Executor EXECUTOR = UwbTestUtils.getExecutor(); - private static final PersistableBundle PARAMS = new PersistableBundle(); - private static final @RangingSession.Callback.Reason int REASON = - RangingSession.Callback.REASON_GENERIC_ERROR; - - @Test - public void testOnRangingOpened_OnOpenSuccessCalled() { - SessionHandle handle = new SessionHandle(123); - RangingSession.Callback callback = mock(RangingSession.Callback.class); - IUwbAdapter adapter = mock(IUwbAdapter.class); - RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle); - verifyOpenState(session, false); - - session.onRangingOpened(); - verifyOpenState(session, true); - - // Verify that the onOpenSuccess callback was invoked - verify(callback, times(1)).onOpened(eq(session)); - verify(callback, times(0)).onClosed(anyInt(), any()); - } - - @Test - public void testOnRangingOpened_CannotOpenClosedSession() { - SessionHandle handle = new SessionHandle(123); - RangingSession.Callback callback = mock(RangingSession.Callback.class); - IUwbAdapter adapter = mock(IUwbAdapter.class); - RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle); - - session.onRangingOpened(); - verifyOpenState(session, true); - verify(callback, times(1)).onOpened(eq(session)); - verify(callback, times(0)).onClosed(anyInt(), any()); - - session.onRangingClosed(REASON, PARAMS); - verifyOpenState(session, false); - verify(callback, times(1)).onOpened(eq(session)); - verify(callback, times(1)).onClosed(anyInt(), any()); - - // Now invoke the ranging started callback and ensure the session remains closed - session.onRangingOpened(); - verifyOpenState(session, false); - verify(callback, times(1)).onOpened(eq(session)); - verify(callback, times(1)).onClosed(anyInt(), any()); - } - - @Test - public void testOnRangingClosed_OnClosedCalledWhenSessionNotOpen() { - SessionHandle handle = new SessionHandle(123); - RangingSession.Callback callback = mock(RangingSession.Callback.class); - IUwbAdapter adapter = mock(IUwbAdapter.class); - RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle); - verifyOpenState(session, false); - - session.onRangingClosed(REASON, PARAMS); - verifyOpenState(session, false); - - // Verify that the onOpenSuccess callback was invoked - verify(callback, times(0)).onOpened(eq(session)); - verify(callback, times(1)).onClosed(anyInt(), any()); - } - - @Test - public void testOnRangingClosed_OnClosedCalled() { - SessionHandle handle = new SessionHandle(123); - RangingSession.Callback callback = mock(RangingSession.Callback.class); - IUwbAdapter adapter = mock(IUwbAdapter.class); - RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle); - session.onRangingStarted(PARAMS); - session.onRangingClosed(REASON, PARAMS); - verify(callback, times(1)).onClosed(anyInt(), any()); - - verifyOpenState(session, false); - session.onRangingClosed(REASON, PARAMS); - verify(callback, times(2)).onClosed(anyInt(), any()); - } - - @Test - public void testOnRangingResult_OnReportReceivedCalled() { - SessionHandle handle = new SessionHandle(123); - RangingSession.Callback callback = mock(RangingSession.Callback.class); - IUwbAdapter adapter = mock(IUwbAdapter.class); - RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle); - verifyOpenState(session, false); - - session.onRangingStarted(PARAMS); - verifyOpenState(session, true); - - RangingReport report = UwbTestUtils.getRangingReports(1); - session.onRangingResult(report); - verify(callback, times(1)).onReportReceived(eq(report)); - } - - @Test - public void testStart_CannotStartIfAlreadyStarted() throws RemoteException { - SessionHandle handle = new SessionHandle(123); - RangingSession.Callback callback = mock(RangingSession.Callback.class); - IUwbAdapter adapter = mock(IUwbAdapter.class); - RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle); - doAnswer(new StartAnswer(session)).when(adapter).startRanging(any(), any()); - session.onRangingOpened(); - - session.start(PARAMS); - verify(callback, times(1)).onStarted(any()); - - // Calling start again should throw an illegal state - verifyThrowIllegalState(() -> session.start(PARAMS)); - verify(callback, times(1)).onStarted(any()); - } - - @Test - public void testStop_CannotStopIfAlreadyStopped() throws RemoteException { - SessionHandle handle = new SessionHandle(123); - RangingSession.Callback callback = mock(RangingSession.Callback.class); - IUwbAdapter adapter = mock(IUwbAdapter.class); - RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle); - doAnswer(new StartAnswer(session)).when(adapter).startRanging(any(), any()); - doAnswer(new StopAnswer(session)).when(adapter).stopRanging(any()); - session.onRangingOpened(); - session.start(PARAMS); - - verifyNoThrowIllegalState(session::stop); - verify(callback, times(1)).onStopped(); - - // Calling stop again should throw an illegal state - verifyThrowIllegalState(session::stop); - verify(callback, times(1)).onStopped(); - } - - @Test - public void testReconfigure_OnlyWhenOpened() throws RemoteException { - SessionHandle handle = new SessionHandle(123); - RangingSession.Callback callback = mock(RangingSession.Callback.class); - IUwbAdapter adapter = mock(IUwbAdapter.class); - RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle); - doAnswer(new StartAnswer(session)).when(adapter).startRanging(any(), any()); - doAnswer(new ReconfigureAnswer(session)).when(adapter).reconfigureRanging(any(), any()); - - verifyThrowIllegalState(() -> session.reconfigure(PARAMS)); - verify(callback, times(0)).onReconfigured(any()); - verifyOpenState(session, false); - - session.onRangingOpened(); - verifyNoThrowIllegalState(() -> session.reconfigure(PARAMS)); - verify(callback, times(1)).onReconfigured(any()); - verifyOpenState(session, true); - - session.onRangingStarted(PARAMS); - verifyNoThrowIllegalState(() -> session.reconfigure(PARAMS)); - verify(callback, times(2)).onReconfigured(any()); - verifyOpenState(session, true); - - session.onRangingStopped(); - verifyNoThrowIllegalState(() -> session.reconfigure(PARAMS)); - verify(callback, times(3)).onReconfigured(any()); - verifyOpenState(session, true); - - - session.onRangingClosed(REASON, PARAMS); - verifyThrowIllegalState(() -> session.reconfigure(PARAMS)); - verify(callback, times(3)).onReconfigured(any()); - verifyOpenState(session, false); - } - - @Test - public void testClose_NoCallbackUntilInvoked() throws RemoteException { - SessionHandle handle = new SessionHandle(123); - RangingSession.Callback callback = mock(RangingSession.Callback.class); - IUwbAdapter adapter = mock(IUwbAdapter.class); - RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle); - session.onRangingOpened(); - - // Calling close multiple times should invoke closeRanging until the session receives - // the onClosed callback. - int totalCallsBeforeOnRangingClosed = 3; - for (int i = 1; i <= totalCallsBeforeOnRangingClosed; i++) { - session.close(); - verifyOpenState(session, true); - verify(adapter, times(i)).closeRanging(handle); - verify(callback, times(0)).onClosed(anyInt(), any()); - } - - // After onClosed is invoked, then the adapter should no longer be called for each call to - // the session's close. - final int totalCallsAfterOnRangingClosed = 2; - for (int i = 1; i <= totalCallsAfterOnRangingClosed; i++) { - session.onRangingClosed(REASON, PARAMS); - verifyOpenState(session, false); - verify(adapter, times(totalCallsBeforeOnRangingClosed)).closeRanging(handle); - verify(callback, times(i)).onClosed(anyInt(), any()); - } - } - - @Test - public void testClose_OnClosedCalled() throws RemoteException { - SessionHandle handle = new SessionHandle(123); - RangingSession.Callback callback = mock(RangingSession.Callback.class); - IUwbAdapter adapter = mock(IUwbAdapter.class); - RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle); - doAnswer(new CloseAnswer(session)).when(adapter).closeRanging(any()); - session.onRangingOpened(); - - session.close(); - verify(callback, times(1)).onClosed(anyInt(), any()); - } - - @Test - public void testClose_CannotInteractFurther() throws RemoteException { - SessionHandle handle = new SessionHandle(123); - RangingSession.Callback callback = mock(RangingSession.Callback.class); - IUwbAdapter adapter = mock(IUwbAdapter.class); - RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle); - doAnswer(new CloseAnswer(session)).when(adapter).closeRanging(any()); - session.close(); - - verifyThrowIllegalState(() -> session.start(PARAMS)); - verifyThrowIllegalState(() -> session.reconfigure(PARAMS)); - verifyThrowIllegalState(() -> session.stop()); - verifyNoThrowIllegalState(() -> session.close()); - } - - @Test - public void testOnRangingResult_OnReportReceivedCalledWhenOpen() { - SessionHandle handle = new SessionHandle(123); - RangingSession.Callback callback = mock(RangingSession.Callback.class); - IUwbAdapter adapter = mock(IUwbAdapter.class); - RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle); - - assertFalse(session.isOpen()); - session.onRangingStarted(PARAMS); - assertTrue(session.isOpen()); - - // Verify that the onReportReceived callback was invoked - RangingReport report = UwbTestUtils.getRangingReports(1); - session.onRangingResult(report); - verify(callback, times(1)).onReportReceived(report); - } - - @Test - public void testOnRangingResult_OnReportReceivedNotCalledWhenNotOpen() { - SessionHandle handle = new SessionHandle(123); - RangingSession.Callback callback = mock(RangingSession.Callback.class); - IUwbAdapter adapter = mock(IUwbAdapter.class); - RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle); - - assertFalse(session.isOpen()); - - // Verify that the onReportReceived callback was invoked - RangingReport report = UwbTestUtils.getRangingReports(1); - session.onRangingResult(report); - verify(callback, times(0)).onReportReceived(report); - } - - private void verifyOpenState(RangingSession session, boolean expected) { - assertEquals(expected, session.isOpen()); - } - - private void verifyThrowIllegalState(Runnable runnable) { - try { - runnable.run(); - fail(); - } catch (IllegalStateException e) { - // Pass - } - } - - private void verifyNoThrowIllegalState(Runnable runnable) { - try { - runnable.run(); - } catch (IllegalStateException e) { - fail(); - } - } - - abstract class AdapterAnswer implements Answer { - protected RangingSession mSession; - - protected AdapterAnswer(RangingSession session) { - mSession = session; - } - } - - class StartAnswer extends AdapterAnswer { - StartAnswer(RangingSession session) { - super(session); - } - - @Override - public Object answer(InvocationOnMock invocation) { - mSession.onRangingStarted(PARAMS); - return null; - } - } - - class ReconfigureAnswer extends AdapterAnswer { - ReconfigureAnswer(RangingSession session) { - super(session); - } - - @Override - public Object answer(InvocationOnMock invocation) { - mSession.onRangingReconfigured(PARAMS); - return null; - } - } - - class StopAnswer extends AdapterAnswer { - StopAnswer(RangingSession session) { - super(session); - } - - @Override - public Object answer(InvocationOnMock invocation) { - mSession.onRangingStopped(); - return null; - } - } - - class CloseAnswer extends AdapterAnswer { - CloseAnswer(RangingSession session) { - super(session); - } - - @Override - public Object answer(InvocationOnMock invocation) { - mSession.onRangingClosed(REASON, PARAMS); - return null; - } - } -} diff --git a/core/tests/uwbtests/src/android/uwb/SessionHandleTest.java b/core/tests/uwbtests/src/android/uwb/SessionHandleTest.java deleted file mode 100644 index 8b42ff7f62a7..000000000000 --- a/core/tests/uwbtests/src/android/uwb/SessionHandleTest.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2020 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.uwb; - -import static org.junit.Assert.assertEquals; - -import android.os.Parcel; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Test of {@link SessionHandle}. - */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class SessionHandleTest { - - @Test - public void testBasic() { - int handleId = 12; - SessionHandle handle = new SessionHandle(handleId); - assertEquals(handle.getId(), handleId); - } - - @Test - public void testParcel() { - Parcel parcel = Parcel.obtain(); - SessionHandle handle = new SessionHandle(10); - handle.writeToParcel(parcel, 0); - parcel.setDataPosition(0); - SessionHandle fromParcel = SessionHandle.CREATOR.createFromParcel(parcel); - assertEquals(handle, fromParcel); - } -} diff --git a/core/tests/uwbtests/src/android/uwb/UwbAddressTest.java b/core/tests/uwbtests/src/android/uwb/UwbAddressTest.java deleted file mode 100644 index ccc88a9a5399..000000000000 --- a/core/tests/uwbtests/src/android/uwb/UwbAddressTest.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2020 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.uwb; - -import static org.junit.Assert.assertEquals; - -import android.os.Parcel; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Test of {@link UwbAddress}. - */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class UwbAddressTest { - - @Test - public void testFromBytes_Short() { - runFromBytes(UwbAddress.SHORT_ADDRESS_BYTE_LENGTH); - } - - @Test - public void testFromBytes_Extended() { - runFromBytes(UwbAddress.EXTENDED_ADDRESS_BYTE_LENGTH); - } - - private void runFromBytes(int len) { - byte[] addressBytes = getByteArray(len); - UwbAddress address = UwbAddress.fromBytes(addressBytes); - assertEquals(address.size(), len); - assertEquals(addressBytes, address.toBytes()); - } - - private byte[] getByteArray(int len) { - byte[] res = new byte[len]; - for (int i = 0; i < len; i++) { - res[i] = (byte) i; - } - return res; - } - - @Test - public void testParcel_Short() { - runParcel(true); - } - - @Test - public void testParcel_Extended() { - runParcel(false); - } - - private void runParcel(boolean useShortAddress) { - Parcel parcel = Parcel.obtain(); - UwbAddress address = UwbTestUtils.getUwbAddress(useShortAddress); - address.writeToParcel(parcel, 0); - parcel.setDataPosition(0); - UwbAddress fromParcel = UwbAddress.CREATOR.createFromParcel(parcel); - assertEquals(address, fromParcel); - } -} diff --git a/core/tests/uwbtests/src/android/uwb/UwbManagerTest.java b/core/tests/uwbtests/src/android/uwb/UwbManagerTest.java deleted file mode 100644 index 4983bed742fd..000000000000 --- a/core/tests/uwbtests/src/android/uwb/UwbManagerTest.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2020 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.uwb; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; - -import android.content.Context; - -import androidx.test.InstrumentationRegistry; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Test of {@link UwbManager}. - */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class UwbManagerTest { - - public final Context mContext = InstrumentationRegistry.getContext(); - - @Test - public void testServiceAvailable() { - UwbManager manager = mContext.getSystemService(UwbManager.class); - if (UwbTestUtils.isUwbSupported(mContext)) { - assertNotNull(manager); - } else { - assertNull(manager); - } - } -} diff --git a/media/java/android/media/MediaDrm.java b/media/java/android/media/MediaDrm.java index adb8a54c0167..ea5fe39ccc08 100644 --- a/media/java/android/media/MediaDrm.java +++ b/media/java/android/media/MediaDrm.java @@ -23,7 +23,11 @@ import android.annotation.Nullable; import android.annotation.StringDef; import android.annotation.TestApi; import android.app.ActivityThread; +import android.app.Application; import android.compat.annotation.UnsupportedAppUsage; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.Signature; import android.media.metrics.PlaybackComponent; import android.os.Handler; import android.os.HandlerExecutor; @@ -39,7 +43,10 @@ import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.time.Instant; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.Base64; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; @@ -144,6 +151,7 @@ public final class MediaDrm implements AutoCloseable { private static final String PERMISSION = android.Manifest.permission.ACCESS_DRM_CERTIFICATES; private long mNativeContext; + private final String mAppPackageName; /** * Specify no certificate type @@ -281,8 +289,9 @@ public final class MediaDrm implements AutoCloseable { /* Native setup requires a weak reference to our object. * It's easier to create it here than in C++. */ + mAppPackageName = ActivityThread.currentOpPackageName(); native_setup(new WeakReference<MediaDrm>(this), - getByteArrayFromUUID(uuid), ActivityThread.currentOpPackageName()); + getByteArrayFromUUID(uuid), mAppPackageName); mCloseGuard.open("release"); } @@ -1144,12 +1153,78 @@ public final class MediaDrm implements AutoCloseable { * problem with the certifcate */ @NonNull - public native KeyRequest getKeyRequest( + public KeyRequest getKeyRequest( @NonNull byte[] scope, @Nullable byte[] init, @Nullable String mimeType, @KeyType int keyType, @Nullable HashMap<String, String> optionalParameters) - throws NotProvisionedException; + throws NotProvisionedException { + HashMap<String, String> internalParams; + if (optionalParameters == null) { + internalParams = new HashMap<>(); + } else { + internalParams = new HashMap<>(optionalParameters); + } + byte[] rawBytes = getNewestAvailablePackageCertificateRawBytes(); + byte[] hashBytes = null; + if (rawBytes != null) { + hashBytes = getDigestBytes(rawBytes, "SHA-256"); + } + if (hashBytes != null) { + Base64.Encoder encoderB64 = Base64.getEncoder(); + String hashBytesB64 = encoderB64.encodeToString(hashBytes); + internalParams.put("package_certificate_hash_bytes", hashBytesB64); + } + return getKeyRequestNative(scope, init, mimeType, keyType, internalParams); + } + @Nullable + private byte[] getNewestAvailablePackageCertificateRawBytes() { + Application application = ActivityThread.currentApplication(); + if (application == null) { + Log.w(TAG, "pkg cert: Application is null"); + return null; + } + PackageManager pm = application.getPackageManager(); + if (pm == null) { + Log.w(TAG, "pkg cert: PackageManager is null"); + return null; + } + PackageInfo packageInfo = null; + try { + packageInfo = pm.getPackageInfo(mAppPackageName, + PackageManager.GET_SIGNING_CERTIFICATES); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, mAppPackageName, e); + } + if (packageInfo == null || packageInfo.signingInfo == null) { + Log.w(TAG, "pkg cert: PackageInfo or SigningInfo is null"); + return null; + } + Signature[] signers = packageInfo.signingInfo.getApkContentsSigners(); + if (signers != null && signers.length == 1) { + return signers[0].toByteArray(); + } + Log.w(TAG, "pkg cert: " + signers.length + " signers"); + return null; + } + + @Nullable + private static byte[] getDigestBytes(@NonNull byte[] rawBytes, @NonNull String algorithm) { + try { + MessageDigest messageDigest = MessageDigest.getInstance(algorithm); + return messageDigest.digest(rawBytes); + } catch (NoSuchAlgorithmException e) { + Log.w(TAG, algorithm, e); + } + return null; + } + + @NonNull + private native KeyRequest getKeyRequestNative( + @NonNull byte[] scope, @Nullable byte[] init, + @Nullable String mimeType, @KeyType int keyType, + @Nullable HashMap<String, String> optionalParameters) + throws NotProvisionedException; /** * A key response is received from the license server by the app, then it is diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java index 740bc2dbeb43..c0185dcc4539 100644 --- a/media/java/android/media/tv/TvInputManager.java +++ b/media/java/android/media/tv/TvInputManager.java @@ -2525,7 +2525,9 @@ public final class TvInputManager { /** * Pauses TV program recording in the current recording session. * - * @param params A set of extra parameters which might be handled with this event. + * @param params Domain-specific data for this request. Keys <em>must</em> be a scoped + * name, i.e. prefixed with a package name you own, so that different developers + * will not create conflicting keys. * {@link TvRecordingClient#pauseRecording(Bundle)}. */ void pauseRecording(@NonNull Bundle params) { @@ -2543,7 +2545,9 @@ public final class TvInputManager { /** * Resumes TV program recording in the current recording session. * - * @param params A set of extra parameters which might be handled with this event. + * @param params Domain-specific data for this request. Keys <em>must</em> be a scoped + * name, i.e. prefixed with a package name you own, so that different developers + * will not create conflicting keys. * {@link TvRecordingClient#resumeRecording(Bundle)}. */ void resumeRecording(@NonNull Bundle params) { diff --git a/media/jni/android_media_MediaDrm.cpp b/media/jni/android_media_MediaDrm.cpp index 22c3572e963b..d15e8485873d 100644 --- a/media/jni/android_media_MediaDrm.cpp +++ b/media/jni/android_media_MediaDrm.cpp @@ -2056,7 +2056,7 @@ static const JNINativeMethod gMethods[] = { { "closeSessionNative", "([B)V", (void *)android_media_MediaDrm_closeSession }, - { "getKeyRequest", "([B[BLjava/lang/String;ILjava/util/HashMap;)" + { "getKeyRequestNative", "([B[BLjava/lang/String;ILjava/util/HashMap;)" "Landroid/media/MediaDrm$KeyRequest;", (void *)android_media_MediaDrm_getKeyRequest }, diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java index f6b239e31e99..ebb6e30d4b3b 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java @@ -254,6 +254,7 @@ public class Task { public Task(Task other) { this(other.key, other.colorPrimary, other.colorBackground, other.isDockable, other.isLocked, other.taskDescription, other.topActivity); + lastSnapshotData.set(other.lastSnapshotData); } /** @@ -281,6 +282,25 @@ public class Task { : key.baseIntent.getComponent(); } + public void setLastSnapshotData(ActivityManager.RecentTaskInfo rawTask) { + lastSnapshotData.set(rawTask.lastSnapshotData); + } + + /** + * Returns the visible width to height ratio. Returns 0f if snapshot data is not available. + */ + public float getVisibleThumbnailRatio() { + if (lastSnapshotData.taskSize == null || lastSnapshotData.contentInsets == null) { + return 0f; + } + + float availableWidth = lastSnapshotData.taskSize.x - (lastSnapshotData.contentInsets.left + + lastSnapshotData.contentInsets.right); + float availableHeight = lastSnapshotData.taskSize.y - (lastSnapshotData.contentInsets.top + + lastSnapshotData.contentInsets.bottom); + return availableWidth / availableHeight; + } + @Override public boolean equals(Object o) { // Check that the id matches diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 0ce26739b51c..5cb9d8ff3f31 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -70,6 +70,7 @@ import android.content.pm.IDataLoaderStatusListener; import android.content.pm.IPackageInstallObserver2; import android.content.pm.IPackageInstallerSession; import android.content.pm.IPackageInstallerSessionFileSystemConnector; +import android.content.pm.IPackageLoadingProgressCallback; import android.content.pm.InstallationFile; import android.content.pm.InstallationFileParcel; import android.content.pm.PackageInfo; @@ -321,6 +322,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private float mProgress = 0; @GuardedBy("mLock") private float mReportedProgress = -1; + @GuardedBy("mLock") + private float mIncrementalProgress = 0; /** State of the session. */ @GuardedBy("mLock") @@ -3770,7 +3773,15 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { mIncrementalFileStorages = IncrementalFileStorages.initialize(mContext, stageDir, inheritedDir, params, statusListener, healthCheckParams, healthListener, - addedFiles, perUidReadTimeouts); + addedFiles, perUidReadTimeouts, + new IPackageLoadingProgressCallback.Stub() { + @Override + public void onPackageLoadingProgressChanged(float progress) { + synchronized (mLock) { + mIncrementalProgress = progress; + } + } + }); return false; } catch (IOException e) { throw new PackageManagerException(INSTALL_FAILED_MEDIA_UNAVAILABLE, e.getMessage(), diff --git a/services/core/java/com/android/server/speech/RemoteSpeechRecognitionService.java b/services/core/java/com/android/server/speech/RemoteSpeechRecognitionService.java index 96248c3711e3..0974537e2584 100644 --- a/services/core/java/com/android/server/speech/RemoteSpeechRecognitionService.java +++ b/services/core/java/com/android/server/speech/RemoteSpeechRecognitionService.java @@ -18,22 +18,65 @@ package com.android.server.speech; import static com.android.internal.infra.AbstractRemoteService.PERMANENT_BOUND_TIMEOUT_MS; +import android.annotation.Nullable; +import android.app.AppOpsManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.os.Binder; +import android.os.Bundle; import android.os.RemoteException; import android.speech.IRecognitionListener; import android.speech.IRecognitionService; import android.speech.RecognitionService; +import android.speech.SpeechRecognizer; +import android.util.Log; import android.util.Slog; +import com.android.internal.annotations.GuardedBy; import com.android.internal.infra.ServiceConnector; final class RemoteSpeechRecognitionService extends ServiceConnector.Impl<IRecognitionService> { private static final String TAG = RemoteSpeechRecognitionService.class.getSimpleName(); - private static final boolean DEBUG = true; + private static final boolean DEBUG = false; - RemoteSpeechRecognitionService(Context context, ComponentName serviceName, int userId) { + private static final String APP_OP_MESSAGE = "Recording audio for speech recognition"; + private static final String RECORD_AUDIO_APP_OP = + AppOpsManager.permissionToOp(android.Manifest.permission.RECORD_AUDIO); + + private final Object mLock = new Object(); + + private boolean mConnected = false; + + @Nullable + private IRecognitionListener mListener; + + @Nullable + @GuardedBy("mLock") + private String mPackageName; + + @Nullable + @GuardedBy("mLock") + private String mFeatureId; + + @Nullable + @GuardedBy("mLock") + private DelegatingListener mDelegatingListener; + + // Makes sure we can block startListening() if session is still in progress. + @GuardedBy("mLock") + private boolean mSessionInProgress = false; + + // Makes sure we call startProxyOp / finishProxyOp at right times and only once per session. + @GuardedBy("mLock") + private boolean mRecordingInProgress = false; + + private final int mCallingUid; + private final AppOpsManager mAppOpsManager; + private final ComponentName mComponentName; + + RemoteSpeechRecognitionService( + Context context, ComponentName serviceName, int userId, int callingUid) { super(context, new Intent(RecognitionService.SERVICE_INTERFACE).setComponent(serviceName), Context.BIND_AUTO_CREATE @@ -43,46 +86,197 @@ final class RemoteSpeechRecognitionService extends ServiceConnector.Impl<IRecogn userId, IRecognitionService.Stub::asInterface); + mCallingUid = callingUid; + mAppOpsManager = mContext.getSystemService(AppOpsManager.class); + mComponentName = serviceName; + if (DEBUG) { Slog.i(TAG, "Bound to recognition service at: " + serviceName.flattenToString()); } } + ComponentName getServiceComponentName() { + return mComponentName; + } + void startListening(Intent recognizerIntent, IRecognitionListener listener, String packageName, - String featureId) throws RemoteException { + String featureId) { if (DEBUG) { - Slog.i(TAG, "#startListening for package: " + packageName + ", feature=" + featureId); + Slog.i(TAG, String.format("#startListening for package: %s, feature=%s, callingUid=%d", + packageName, featureId, mCallingUid)); + } + + if (listener == null) { + Log.w(TAG, "#startListening called with no preceding #setListening - ignoring"); + return; + } + + if (!mConnected) { + tryRespondWithError(listener, SpeechRecognizer.ERROR_SERVER_DISCONNECTED); + return; + } + + synchronized (mLock) { + if (mSessionInProgress) { + Slog.i(TAG, "#startListening called while listening is in progress."); + tryRespondWithError(listener, SpeechRecognizer.ERROR_RECOGNIZER_BUSY); + return; + } + + if (startProxyOp(packageName, featureId) != AppOpsManager.MODE_ALLOWED) { + tryRespondWithError(listener, SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS); + return; + } + mSessionInProgress = true; + mRecordingInProgress = true; + + mListener = listener; + mDelegatingListener = new DelegatingListener(listener, () -> { + // To be invoked in terminal calls of the callback: results() or error() + if (DEBUG) { + Slog.i(TAG, "Recognition session complete"); + } + + synchronized (mLock) { + resetStateLocked(); + } + }); + mPackageName = packageName; + mFeatureId = featureId; + + run(service -> + service.startListening( + recognizerIntent, + mDelegatingListener, + packageName, + featureId, + mCallingUid)); } - run(service -> service.startListening(recognizerIntent, listener, packageName, featureId)); } - void stopListening(IRecognitionListener listener, String packageName, String featureId) - throws RemoteException { + void stopListening( + IRecognitionListener listener, String packageName, String featureId) { if (DEBUG) { Slog.i(TAG, "#stopListening for package: " + packageName + ", feature=" + featureId); } - run(service -> service.stopListening(listener, packageName, featureId)); + + if (!mConnected) { + tryRespondWithError(listener, SpeechRecognizer.ERROR_SERVER_DISCONNECTED); + return; + } + + synchronized (mLock) { + if (mListener == null) { + Log.w(TAG, "#stopListening called with no preceding #startListening - ignoring"); + tryRespondWithError(listener, SpeechRecognizer.ERROR_CLIENT); + return; + } + + if (mListener.asBinder() != listener.asBinder()) { + Log.w(TAG, "#stopListening called with an unexpected listener"); + tryRespondWithError(listener, SpeechRecognizer.ERROR_CLIENT); + return; + } + + if (!mRecordingInProgress) { + Slog.i(TAG, "#stopListening called while listening isn't in progress, ignoring."); + return; + } + mRecordingInProgress = false; + + finishProxyOp(packageName, featureId); + + run(service -> service.stopListening(mDelegatingListener, packageName, featureId)); + } } - void cancel(IRecognitionListener listener, String packageName, String featureId) - throws RemoteException { + void cancel( + IRecognitionListener listener, + String packageName, + String featureId, + boolean isShutdown) { if (DEBUG) { Slog.i(TAG, "#cancel for package: " + packageName + ", feature=" + featureId); } - run(service -> service.cancel(listener, packageName, featureId)); + + if (!mConnected) { + tryRespondWithError(listener, SpeechRecognizer.ERROR_SERVER_DISCONNECTED); + } + + synchronized (mLock) { + if (mListener == null) { + if (DEBUG) { + Log.w(TAG, "#cancel called with no preceding #startListening - ignoring"); + } + return; + } + + if (mListener.asBinder() != listener.asBinder()) { + Log.w(TAG, "#cancel called with an unexpected listener"); + tryRespondWithError(listener, SpeechRecognizer.ERROR_CLIENT); + return; + } + + // Temporary reference to allow for resetting the hard link mDelegatingListener to null. + IRecognitionListener delegatingListener = mDelegatingListener; + + run(service -> service.cancel(delegatingListener, packageName, featureId, isShutdown)); + + if (mRecordingInProgress) { + finishProxyOp(packageName, featureId); + } + mRecordingInProgress = false; + mSessionInProgress = false; + + mDelegatingListener = null; + mListener = null; + + // Schedule to unbind after cancel is delivered. + if (isShutdown) { + run(service -> unbind()); + } + } + } + + void shutdown() { + synchronized (mLock) { + if (this.mListener == null) { + if (DEBUG) { + Slog.i(TAG, "Package died, but session wasn't initialized. " + + "Not invoking #cancel"); + } + return; + } + } + + cancel(mListener, mPackageName, mFeatureId, true /* isShutdown */); } @Override // from ServiceConnector.Impl protected void onServiceConnectionStatusChanged( IRecognitionService service, boolean connected) { - if (!DEBUG) { - return; + mConnected = connected; + + if (DEBUG) { + if (connected) { + Slog.i(TAG, "Connected to speech recognition service"); + } else { + Slog.w(TAG, "Disconnected from speech recognition service"); + } } - if (connected) { - Slog.i(TAG, "Connected to ASR service"); - } else { - Slog.w(TAG, "Disconnected from ASR service"); + synchronized (mLock) { + if (!connected) { + if (mListener == null) { + Slog.i(TAG, "Connection to speech recognition service lost, but no " + + "#startListening has been invoked yet."); + return; + } + + tryRespondWithError(mListener, SpeechRecognizer.ERROR_SERVER_DISCONNECTED); + + resetStateLocked(); + } } } @@ -90,4 +284,119 @@ final class RemoteSpeechRecognitionService extends ServiceConnector.Impl<IRecogn protected long getAutoDisconnectTimeoutMs() { return PERMANENT_BOUND_TIMEOUT_MS; } + + private void resetStateLocked() { + if (mRecordingInProgress && mPackageName != null && mFeatureId != null) { + finishProxyOp(mPackageName, mFeatureId); + } + + mListener = null; + mDelegatingListener = null; + mSessionInProgress = false; + mRecordingInProgress = false; + } + + private int startProxyOp(String packageName, String featureId) { + final long identity = Binder.clearCallingIdentity(); + try { + return mAppOpsManager.startProxyOp( + RECORD_AUDIO_APP_OP, + mCallingUid, + packageName, + featureId, + APP_OP_MESSAGE); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + private void finishProxyOp(String packageName, String featureId) { + final long identity = Binder.clearCallingIdentity(); + try { + mAppOpsManager.finishProxyOp( + RECORD_AUDIO_APP_OP, mCallingUid, packageName, featureId); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + private static void tryRespondWithError(IRecognitionListener listener, int errorCode) { + if (DEBUG) { + Slog.i(TAG, "Responding with error " + errorCode); + } + + try { + if (listener != null) { + listener.onError(errorCode); + } + } catch (RemoteException e) { + Slog.w(TAG, + String.format("Failed to respond with an error %d to the client", errorCode), + e); + } + } + + private static class DelegatingListener extends IRecognitionListener.Stub { + + private final IRecognitionListener mRemoteListener; + private final Runnable mOnSessionComplete; + + DelegatingListener(IRecognitionListener listener, Runnable onSessionComplete) { + mRemoteListener = listener; + mOnSessionComplete = onSessionComplete; + } + + @Override + public void onReadyForSpeech(Bundle params) throws RemoteException { + mRemoteListener.onReadyForSpeech(params); + } + + @Override + public void onBeginningOfSpeech() throws RemoteException { + mRemoteListener.onBeginningOfSpeech(); + } + + @Override + public void onRmsChanged(float rmsdB) throws RemoteException { + mRemoteListener.onRmsChanged(rmsdB); + } + + @Override + public void onBufferReceived(byte[] buffer) throws RemoteException { + mRemoteListener.onBufferReceived(buffer); + } + + @Override + public void onEndOfSpeech() throws RemoteException { + mRemoteListener.onEndOfSpeech(); + } + + @Override + public void onError(int error) throws RemoteException { + if (DEBUG) { + Slog.i(TAG, String.format("Error %d during recognition session", error)); + } + mOnSessionComplete.run(); + mRemoteListener.onError(error); + } + + @Override + public void onResults(Bundle results) throws RemoteException { + if (DEBUG) { + Slog.i(TAG, "#onResults invoked for a recognition session"); + } + mOnSessionComplete.run(); + mRemoteListener.onResults(results); + } + + @Override + public void onPartialResults(Bundle results) throws RemoteException { + mRemoteListener.onPartialResults(results); + } + + @Override + public void onEvent(int eventType, Bundle params) throws RemoteException { + mRemoteListener.onEvent(eventType, params); + } + } } diff --git a/services/core/java/com/android/server/speech/SpeechRecognitionManagerService.java b/services/core/java/com/android/server/speech/SpeechRecognitionManagerService.java index 592ba9e616b8..dbe73546d748 100644 --- a/services/core/java/com/android/server/speech/SpeechRecognitionManagerService.java +++ b/services/core/java/com/android/server/speech/SpeechRecognitionManagerService.java @@ -18,7 +18,9 @@ package com.android.server.speech; import android.annotation.NonNull; import android.annotation.UserIdInt; +import android.content.ComponentName; import android.content.Context; +import android.os.IBinder; import android.os.UserHandle; import android.speech.IRecognitionServiceManager; import android.speech.IRecognitionServiceManagerCallback; @@ -42,6 +44,7 @@ public final class SpeechRecognitionManagerService extends public SpeechRecognitionManagerService(@NonNull Context context) { super(context, + // TODO(b/176578753): think if we want to favor the particular service here. new FrameworkResourcesServiceNameResolver( context, R.string.config_defaultOnDeviceSpeechRecognitionService), @@ -63,11 +66,15 @@ public final class SpeechRecognitionManagerService extends final class SpeechRecognitionManagerServiceStub extends IRecognitionServiceManager.Stub { @Override - public void createSession(IRecognitionServiceManagerCallback callback) { + public void createSession( + ComponentName componentName, + IBinder clientToken, + boolean onDevice, + IRecognitionServiceManagerCallback callback) { int userId = UserHandle.getCallingUserId(); synchronized (mLock) { SpeechRecognitionManagerServiceImpl service = getServiceForUserLocked(userId); - service.createSessionLocked(callback); + service.createSessionLocked(componentName, clientToken, onDevice, callback); } } } diff --git a/services/core/java/com/android/server/speech/SpeechRecognitionManagerServiceImpl.java b/services/core/java/com/android/server/speech/SpeechRecognitionManagerServiceImpl.java index bcaf174b1d92..2656a3d32555 100644 --- a/services/core/java/com/android/server/speech/SpeechRecognitionManagerServiceImpl.java +++ b/services/core/java/com/android/server/speech/SpeechRecognitionManagerServiceImpl.java @@ -24,30 +24,44 @@ import android.content.ComponentName; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; +import android.os.Binder; +import android.os.IBinder; import android.os.RemoteException; import android.speech.IRecognitionListener; import android.speech.IRecognitionService; import android.speech.IRecognitionServiceManagerCallback; +import android.speech.SpeechRecognizer; import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.server.infra.AbstractPerUserSystemService; +import com.google.android.collect.Sets; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + final class SpeechRecognitionManagerServiceImpl extends AbstractPerUserSystemService<SpeechRecognitionManagerServiceImpl, SpeechRecognitionManagerService> { - private static final String TAG = SpeechRecognitionManagerServiceImpl.class.getSimpleName(); + private static final int MAX_CONCURRENT_CONNECTIONS_BY_CLIENT = 10; + + private final Object mLock = new Object(); + + @NonNull @GuardedBy("mLock") - @Nullable - private RemoteSpeechRecognitionService mRemoteService; + private final Map<Integer, Set<RemoteSpeechRecognitionService>> mRemoteServicesByUid = + new HashMap<>(); SpeechRecognitionManagerServiceImpl( @NonNull SpeechRecognitionManagerService master, @NonNull Object lock, @UserIdInt int userId, boolean disabled) { super(master, lock, userId); - updateRemoteServiceLocked(); } @GuardedBy("mLock") @@ -67,92 +81,196 @@ final class SpeechRecognitionManagerServiceImpl extends @Override // from PerUserSystemService protected boolean updateLocked(boolean disabled) { final boolean enabledChanged = super.updateLocked(disabled); - updateRemoteServiceLocked(); return enabledChanged; } - /** - * Updates the reference to the remote service. - */ - @GuardedBy("mLock") - private void updateRemoteServiceLocked() { - if (mRemoteService != null) { - if (mMaster.debug) { - Slog.d(TAG, "updateRemoteService(): destroying old remote service"); - } - mRemoteService.unbind(); - mRemoteService = null; + void createSessionLocked( + ComponentName componentName, + IBinder clientToken, + boolean onDevice, + IRecognitionServiceManagerCallback callback) { + if (mMaster.debug) { + Slog.i(TAG, String.format("#createSessionLocked, component=%s, onDevice=%s", + componentName, onDevice)); + } + + ComponentName serviceComponent = componentName; + if (onDevice) { + serviceComponent = getOnDeviceComponentNameLocked(); } - } - void createSessionLocked(IRecognitionServiceManagerCallback callback) { - // TODO(b/176578753): check clients have record audio permission. - // TODO(b/176578753): verify caller package is the one supplied + if (serviceComponent == null) { + tryRespondWithError(callback, SpeechRecognizer.ERROR_CLIENT); + return; + } - RemoteSpeechRecognitionService service = ensureRemoteServiceLocked(); + final int creatorCallingUid = Binder.getCallingUid(); + Set<String> creatorPackageNames = + Sets.newArraySet( + getContext().getPackageManager().getPackagesForUid(creatorCallingUid)); + + RemoteSpeechRecognitionService service = createService(creatorCallingUid, serviceComponent); if (service == null) { - tryRespondWithError(callback); + tryRespondWithError(callback, SpeechRecognizer.ERROR_TOO_MANY_REQUESTS); return; } + IBinder.DeathRecipient deathRecipient = + () -> handleClientDeath(creatorCallingUid, service, true /* invoke #cancel */); + + try { + clientToken.linkToDeath(deathRecipient, 0); + } catch (RemoteException e) { + // RemoteException == binder already died, schedule disconnect anyway. + handleClientDeath(creatorCallingUid, service, true /* invoke #cancel */); + } + service.connect().thenAccept(binderService -> { if (binderService != null) { try { callback.onSuccess(new IRecognitionService.Stub() { @Override - public void startListening(Intent recognizerIntent, + public void startListening( + Intent recognizerIntent, IRecognitionListener listener, - String packageName, String featureId) throws RemoteException { + String packageName, + String featureId, + int callingUid) throws RemoteException { + verifyCallerIdentity( + creatorCallingUid, packageName, creatorPackageNames, listener); + if (callingUid != creatorCallingUid) { + listener.onError(SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS); + return; + } + service.startListening( recognizerIntent, listener, packageName, featureId); } @Override - public void stopListening(IRecognitionListener listener, + public void stopListening( + IRecognitionListener listener, String packageName, String featureId) throws RemoteException { + verifyCallerIdentity( + creatorCallingUid, packageName, creatorPackageNames, listener); + service.stopListening(listener, packageName, featureId); } @Override - public void cancel(IRecognitionListener listener, + public void cancel( + IRecognitionListener listener, String packageName, - String featureId) throws RemoteException { - service.cancel(listener, packageName, featureId); + String featureId, + boolean isShutdown) throws RemoteException { + verifyCallerIdentity( + creatorCallingUid, packageName, creatorPackageNames, listener); + + service.cancel(listener, packageName, featureId, isShutdown); + + if (isShutdown) { + handleClientDeath( + creatorCallingUid, + service, + false /* invoke #cancel */); + clientToken.unlinkToDeath(deathRecipient, 0); + } } }); } catch (RemoteException e) { Slog.e(TAG, "Error creating a speech recognition session", e); - tryRespondWithError(callback); + tryRespondWithError(callback, SpeechRecognizer.ERROR_CLIENT); } } else { - tryRespondWithError(callback); + tryRespondWithError(callback, SpeechRecognizer.ERROR_CLIENT); } }); } + private void verifyCallerIdentity( + int creatorCallingUid, + String packageName, + Set<String> creatorPackageNames, + IRecognitionListener listener) throws RemoteException { + if (creatorCallingUid != Binder.getCallingUid() + || !creatorPackageNames.contains(packageName)) { + listener.onError(SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS); + } + } + + private void handleClientDeath( + int callingUid, + RemoteSpeechRecognitionService service, boolean invokeCancel) { + if (invokeCancel) { + service.shutdown(); + } + removeService(callingUid, service); + } + @GuardedBy("mLock") @Nullable - private RemoteSpeechRecognitionService ensureRemoteServiceLocked() { - if (mRemoteService == null) { - final String serviceName = getComponentNameLocked(); - if (serviceName == null) { - if (mMaster.verbose) { - Slog.v(TAG, "ensureRemoteServiceLocked(): no service component name."); - } + private ComponentName getOnDeviceComponentNameLocked() { + final String serviceName = getComponentNameLocked(); + if (serviceName == null) { + if (mMaster.verbose) { + Slog.v(TAG, "ensureRemoteServiceLocked(): no service component name."); + } + return null; + } + return ComponentName.unflattenFromString(serviceName); + } + + private RemoteSpeechRecognitionService createService( + int callingUid, ComponentName serviceComponent) { + synchronized (mLock) { + Set<RemoteSpeechRecognitionService> servicesForClient = + mRemoteServicesByUid.get(callingUid); + + if (servicesForClient != null + && servicesForClient.size() >= MAX_CONCURRENT_CONNECTIONS_BY_CLIENT) { return null; } - final ComponentName serviceComponent = ComponentName.unflattenFromString(serviceName); - mRemoteService = - new RemoteSpeechRecognitionService(getContext(), serviceComponent, mUserId); + + if (servicesForClient != null) { + Optional<RemoteSpeechRecognitionService> existingService = + servicesForClient + .stream() + .filter(service -> + service.getServiceComponentName().equals(serviceComponent)) + .findFirst(); + if (existingService.isPresent()) { + return existingService.get(); + } + } + + RemoteSpeechRecognitionService service = + new RemoteSpeechRecognitionService( + getContext(), serviceComponent, getUserId(), callingUid); + + Set<RemoteSpeechRecognitionService> valuesByCaller = + mRemoteServicesByUid.computeIfAbsent(callingUid, key -> new HashSet<>()); + valuesByCaller.add(service); + + return service; + } + } + + private void removeService(int callingUid, RemoteSpeechRecognitionService service) { + synchronized (mLock) { + Set<RemoteSpeechRecognitionService> valuesByCaller = + mRemoteServicesByUid.get(callingUid); + if (valuesByCaller != null) { + valuesByCaller.remove(service); + } } - return mRemoteService; } - private static void tryRespondWithError(IRecognitionServiceManagerCallback callback) { + private static void tryRespondWithError(IRecognitionServiceManagerCallback callback, + int errorCode) { try { - callback.onError(); + callback.onError(errorCode); } catch (RemoteException e) { Slog.w(TAG, "Failed to respond with error"); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 1cf4ce163640..dd599c9cfb60 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -10092,7 +10092,6 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { final int userId = user.id; - // TODO(b/177547285): add CTS test if (mInjector.userManagerIsHeadlessSystemUserMode()) { ComponentName admin = mOwners.getDeviceOwnerComponent(); Slog.i(LOG_TAG, "Automatically setting profile owner (" + admin + ") on new user " @@ -12911,8 +12910,10 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { if (mOwners.hasProfileOwner(deviceOwnerUserId)) { return CODE_USER_HAS_PROFILE_OWNER; } + + boolean isHeadlessSystemUserMode = mInjector.userManagerIsHeadlessSystemUserMode(); // System user is always running in headless system user mode. - if (!mInjector.userManagerIsHeadlessSystemUserMode() + if (!isHeadlessSystemUserMode && !mUserManager.isUserRunning(new UserHandle(deviceOwnerUserId))) { return CODE_USER_NOT_RUNNING; } @@ -12920,7 +12921,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { return CODE_HAS_PAIRED; } - if (mInjector.userManagerIsHeadlessSystemUserMode()) { + if (isHeadlessSystemUserMode) { if (deviceOwnerUserId != UserHandle.USER_SYSTEM) { Slog.e(LOG_TAG, "In headless system user mode, " + "device owner can only be set on headless system user."); @@ -12932,9 +12933,13 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { // If shell command runs after user setup completed check device status. Otherwise, OK. if (mIsWatch || hasUserSetupCompleted(UserHandle.USER_SYSTEM)) { // In non-headless system user mode, DO can be setup only if - // there's no non-system user - if (!mInjector.userManagerIsHeadlessSystemUserMode() - && mUserManager.getUserCount() > 1) { + // there's no non-system user. + // In headless system user mode, DO can be setup only if there are + // two users: the headless system user and the foreground user. + // If there could be multiple foreground users, this constraint should be modified. + + int maxNumberOfExistingUsers = isHeadlessSystemUserMode ? 2 : 1; + if (mUserManager.getUserCount() > maxNumberOfExistingUsers) { return CODE_NONSYSTEM_USER_EXISTS; } diff --git a/tests/FlickerTests/OWNERS b/tests/FlickerTests/OWNERS index f35a318acbf7..b5561010e7f9 100644 --- a/tests/FlickerTests/OWNERS +++ b/tests/FlickerTests/OWNERS @@ -1,2 +1,3 @@ +# Bug component: 909476 include /services/core/java/com/android/server/wm/OWNERS natanieljr@google.com
\ No newline at end of file |