Loading media/tests/mediatestutils/java/com/android/media/mediatestutils/TestUtils.java +76 −46 Original line number Diff line number Diff line Loading @@ -20,36 +20,47 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.util.Log; import androidx.concurrent.futures.CallbackToFutureAdapter; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import java.lang.ref.WeakReference; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeUnit; import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; /** Utils for audio tests. */ public class TestUtils { /** * * Return a future for an intent delivered by a broadcast receiver which matches an * action and predicate. * @param context - Context to register the receiver with * @param action - String representing action to register receiver for * @param pred - Predicate which sets the future if evaluates to true, otherwise, leaves * the future unset. If the predicate throws, the future is set exceptionally * @return - The future representing intent delivery matching predicate. */ public class TestUtils { public static final String TAG = "MediaTestUtils"; public static ListenableFuture<Intent> getFutureForIntent(Context context, String action, Predicate<Intent> pred) { public static ListenableFuture<Intent> getFutureForIntent( Context context, String action, Predicate<Intent> pred) { // These are evaluated async Objects.requireNonNull(action); Objects.requireNonNull(pred); // Doesn't need to be thread safe since the resolver is called inline final WeakReference<BroadcastReceiver> wrapper[] = new WeakReference[1]; ListenableFuture<Intent> future = CallbackToFutureAdapter.getFuture(completer -> { var receiver = new BroadcastReceiver() { return getFutureForListener( (recv) -> context.registerReceiver( recv, new IntentFilter(action), Context.RECEIVER_NOT_EXPORTED), (recv) -> { try { context.unregisterReceiver(recv); } catch (IllegalArgumentException e) { // Thrown when receiver is already unregistered, nothing to do } }, (completer) -> new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { try { Loading @@ -60,30 +71,49 @@ public class TestUtils { completer.setException(e); } } }; wrapper[0] = new WeakReference(receiver); context.registerReceiver(receiver, new IntentFilter(action), Context.RECEIVER_NOT_EXPORTED); return "Intent receiver future for "; }, "Intent receiver future for action: " + action); } /** * Return a future for a callback registered to a listener interface. * @param registerFunc - Function which consumes the callback object for registration * @param unregisterFunc - Function which consumes the callback object for unregistration * This function is called when the future is completed or cancelled * @param instantiateCallback - Factory function for the callback object, provided a completer * object (see {@code CallbackToFutureAdapter.Completer<T>}), which is a logical reference * to the future returned by this function * @param debug - Debug string contained in future {@code toString} representation. */ public static <T, V> ListenableFuture<T> getFutureForListener( Consumer<V> registerFunc, Consumer<V> unregisterFunc, Function<CallbackToFutureAdapter.Completer<T>, V> instantiateCallback, String debug) { // Doesn't need to be thread safe since the resolver is called inline final WeakReference<V> wrapper[] = new WeakReference[1]; ListenableFuture<T> future = CallbackToFutureAdapter.getFuture( completer -> { final var cb = instantiateCallback.apply(completer); wrapper[0] = new WeakReference(cb); registerFunc.accept(cb); return debug; }); if (wrapper[0] == null) { throw new AssertionError("CallbackToFutureAdapter resolver should be called inline"); throw new AssertionError("Resolver should be called inline"); } final var weakref = wrapper[0]; future.addListener(() -> { try { var recv = weakref.get(); future.addListener( () -> { var cb = weakref.get(); // If there is no reference left, the receiver has already been unregistered if (recv != null) { context.unregisterReceiver(recv); if (cb != null) { unregisterFunc.accept(cb); return; } } catch (IllegalArgumentException e) { // Receiver already unregistered, nothing to do. } Log.d(TAG, "Intent receiver future for action: " + action + "unregistered prior to future completion/cancellation."); } , MoreExecutors.directExecutor()); // Direct executor is fine since lightweight }, MoreExecutors.directExecutor()); // Direct executor is fine since lightweight return future; } } media/tests/mediatestutils/tests/src/java/com/android/media/mediatestutils/GetFutureForIntentTest.java +48 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ package com.android.media.mediatestutils; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.android.media.mediatestutils.TestUtils.getFutureForIntent; import static com.android.media.mediatestutils.TestUtils.getFutureForListener; import static com.google.common.truth.Truth.assertThat; Loading @@ -30,6 +31,8 @@ import android.os.SystemClock; import androidx.test.runner.AndroidJUnit4; import com.google.common.util.concurrent.ListenableFuture; import org.junit.Test; import org.junit.runner.RunWith; Loading Loading @@ -100,6 +103,51 @@ public class GetFutureForIntentTest { assertThat(intent.getIntExtra(INTENT_EXTRA, -1)).isEqualTo(MAGIC_VALUE); } @Test public void unregisterListener_whenComplete() throws Exception { final var service = new FakeService(); final ListenableFuture<Void> future = getFutureForListener( service::registerListener, service::unregisterListener, completer -> () -> { completer.set(null); }, "FakeService listener future"); service.mRunnable.run(); assertThat(service.mRunnable).isNull(); } @Test public void unregisterListener_whenCancel() throws Exception { final var service = new FakeService(); final ListenableFuture<Void> future = getFutureForListener( service::registerListener, service::unregisterListener, completer -> () -> { completer.set(null); }, "FakeService listener future"); future.cancel(false); assertThat(service.mRunnable).isNull(); } private static class FakeService { Runnable mRunnable; void registerListener(Runnable r) { mRunnable = r; } void unregisterListener(Runnable r) { assertThat(r).isEqualTo(mRunnable); mRunnable = null; } } private void sendIntent(boolean correctValue) { final Intent intent = new Intent(INTENT_ACTION).setPackage(mContext.getPackageName()); intent.putExtra(INTENT_EXTRA, correctValue ? MAGIC_VALUE : MAGIC_VALUE + 1); Loading Loading
media/tests/mediatestutils/java/com/android/media/mediatestutils/TestUtils.java +76 −46 Original line number Diff line number Diff line Loading @@ -20,36 +20,47 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.util.Log; import androidx.concurrent.futures.CallbackToFutureAdapter; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import java.lang.ref.WeakReference; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeUnit; import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; /** Utils for audio tests. */ public class TestUtils { /** * * Return a future for an intent delivered by a broadcast receiver which matches an * action and predicate. * @param context - Context to register the receiver with * @param action - String representing action to register receiver for * @param pred - Predicate which sets the future if evaluates to true, otherwise, leaves * the future unset. If the predicate throws, the future is set exceptionally * @return - The future representing intent delivery matching predicate. */ public class TestUtils { public static final String TAG = "MediaTestUtils"; public static ListenableFuture<Intent> getFutureForIntent(Context context, String action, Predicate<Intent> pred) { public static ListenableFuture<Intent> getFutureForIntent( Context context, String action, Predicate<Intent> pred) { // These are evaluated async Objects.requireNonNull(action); Objects.requireNonNull(pred); // Doesn't need to be thread safe since the resolver is called inline final WeakReference<BroadcastReceiver> wrapper[] = new WeakReference[1]; ListenableFuture<Intent> future = CallbackToFutureAdapter.getFuture(completer -> { var receiver = new BroadcastReceiver() { return getFutureForListener( (recv) -> context.registerReceiver( recv, new IntentFilter(action), Context.RECEIVER_NOT_EXPORTED), (recv) -> { try { context.unregisterReceiver(recv); } catch (IllegalArgumentException e) { // Thrown when receiver is already unregistered, nothing to do } }, (completer) -> new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { try { Loading @@ -60,30 +71,49 @@ public class TestUtils { completer.setException(e); } } }; wrapper[0] = new WeakReference(receiver); context.registerReceiver(receiver, new IntentFilter(action), Context.RECEIVER_NOT_EXPORTED); return "Intent receiver future for "; }, "Intent receiver future for action: " + action); } /** * Return a future for a callback registered to a listener interface. * @param registerFunc - Function which consumes the callback object for registration * @param unregisterFunc - Function which consumes the callback object for unregistration * This function is called when the future is completed or cancelled * @param instantiateCallback - Factory function for the callback object, provided a completer * object (see {@code CallbackToFutureAdapter.Completer<T>}), which is a logical reference * to the future returned by this function * @param debug - Debug string contained in future {@code toString} representation. */ public static <T, V> ListenableFuture<T> getFutureForListener( Consumer<V> registerFunc, Consumer<V> unregisterFunc, Function<CallbackToFutureAdapter.Completer<T>, V> instantiateCallback, String debug) { // Doesn't need to be thread safe since the resolver is called inline final WeakReference<V> wrapper[] = new WeakReference[1]; ListenableFuture<T> future = CallbackToFutureAdapter.getFuture( completer -> { final var cb = instantiateCallback.apply(completer); wrapper[0] = new WeakReference(cb); registerFunc.accept(cb); return debug; }); if (wrapper[0] == null) { throw new AssertionError("CallbackToFutureAdapter resolver should be called inline"); throw new AssertionError("Resolver should be called inline"); } final var weakref = wrapper[0]; future.addListener(() -> { try { var recv = weakref.get(); future.addListener( () -> { var cb = weakref.get(); // If there is no reference left, the receiver has already been unregistered if (recv != null) { context.unregisterReceiver(recv); if (cb != null) { unregisterFunc.accept(cb); return; } } catch (IllegalArgumentException e) { // Receiver already unregistered, nothing to do. } Log.d(TAG, "Intent receiver future for action: " + action + "unregistered prior to future completion/cancellation."); } , MoreExecutors.directExecutor()); // Direct executor is fine since lightweight }, MoreExecutors.directExecutor()); // Direct executor is fine since lightweight return future; } }
media/tests/mediatestutils/tests/src/java/com/android/media/mediatestutils/GetFutureForIntentTest.java +48 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ package com.android.media.mediatestutils; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.android.media.mediatestutils.TestUtils.getFutureForIntent; import static com.android.media.mediatestutils.TestUtils.getFutureForListener; import static com.google.common.truth.Truth.assertThat; Loading @@ -30,6 +31,8 @@ import android.os.SystemClock; import androidx.test.runner.AndroidJUnit4; import com.google.common.util.concurrent.ListenableFuture; import org.junit.Test; import org.junit.runner.RunWith; Loading Loading @@ -100,6 +103,51 @@ public class GetFutureForIntentTest { assertThat(intent.getIntExtra(INTENT_EXTRA, -1)).isEqualTo(MAGIC_VALUE); } @Test public void unregisterListener_whenComplete() throws Exception { final var service = new FakeService(); final ListenableFuture<Void> future = getFutureForListener( service::registerListener, service::unregisterListener, completer -> () -> { completer.set(null); }, "FakeService listener future"); service.mRunnable.run(); assertThat(service.mRunnable).isNull(); } @Test public void unregisterListener_whenCancel() throws Exception { final var service = new FakeService(); final ListenableFuture<Void> future = getFutureForListener( service::registerListener, service::unregisterListener, completer -> () -> { completer.set(null); }, "FakeService listener future"); future.cancel(false); assertThat(service.mRunnable).isNull(); } private static class FakeService { Runnable mRunnable; void registerListener(Runnable r) { mRunnable = r; } void unregisterListener(Runnable r) { assertThat(r).isEqualTo(mRunnable); mRunnable = null; } } private void sendIntent(boolean correctValue) { final Intent intent = new Intent(INTENT_ACTION).setPackage(mContext.getPackageName()); intent.putExtra(INTENT_EXTRA, correctValue ? MAGIC_VALUE : MAGIC_VALUE + 1); Loading