From 3856ac4be9cfb0589da5f5fcb948b69c6e075313 Mon Sep 17 00:00:00 2001 From: Bonian Chen Date: Tue, 19 Apr 2022 16:59:14 +0800 Subject: [PATCH] [Settings] Code refactor for async Lifecycle listening This is an extension of LifecycleCallbackAdapter. A postResult(T) and a Consumer is designed for supporting pass result back to UI thread, and is invoked only when required. Bug: 229689535 Test: unit test Change-Id: I0ef5afc31cd23aa865a2dd1d05f9b212242c2e41 (cherry picked from commit 5d2a76cbb4daf504547a6196d5ae9c5e79e8da48) --- .../helper/LifecycleCallbackConverter.java | 132 ++++++++++++++++++ .../LifecycleCallbackConverterTest.java | 120 ++++++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 src/com/android/settings/network/helper/LifecycleCallbackConverter.java create mode 100644 tests/unit/src/com/android/settings/network/helper/LifecycleCallbackConverterTest.java diff --git a/src/com/android/settings/network/helper/LifecycleCallbackConverter.java b/src/com/android/settings/network/helper/LifecycleCallbackConverter.java new file mode 100644 index 00000000000..f35b69a4177 --- /dev/null +++ b/src/com/android/settings/network/helper/LifecycleCallbackConverter.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2022 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 com.android.settings.network.helper; + +import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; +import androidx.annotation.UiThread; +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.Lifecycle; + +import com.android.settingslib.utils.ThreadUtils; + +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; + +/** + * A {@link LifecycleCallbackAdapter} which support carrying a result from any threads back to UI + * thread through {@link #postResult(T)}. + * + * A {@link Consumer} would be invoked from UI thread for further processing on the result. + * + * Note: Result not in STARTED or RESUMED stage will be discarded silently. + * This is to align with the criteria set within + * {@link LifecycleCallbackAdapter#onStateChanged()}. + */ +@VisibleForTesting +public class LifecycleCallbackConverter extends LifecycleCallbackAdapter { + private static final String TAG = "LifecycleCallbackConverter"; + + private final Thread mUiThread; + private final Consumer mResultCallback; + + /** + * A record of number of active status change. + * Even numbers (0, 2, 4, 6 ...) are inactive status. + * Odd numbers (1, 3, 5, 7 ...) are active status. + */ + private final AtomicLong mNumberOfActiveStatusChange = new AtomicLong(); + + /** + * Constructor + * + * @param lifecycle {@link Lifecycle} to monitor + * @param resultCallback for further processing the result + */ + @VisibleForTesting + @UiThread + public LifecycleCallbackConverter( + @NonNull Lifecycle lifecycle, @NonNull Consumer resultCallback) { + super(lifecycle); + mUiThread = Thread.currentThread(); + mResultCallback = resultCallback; + } + + /** + * Post a result (from any thread) back to UI thread. + * + * @param result the object ready to be passed back to {@link Consumer}. + */ + @AnyThread + @VisibleForTesting + public void postResult(T result) { + /** + * Since mNumberOfActiveStatusChange only increase, it is a concept of sequence number. + * Carry it when sending data in between different threads allow to verify if the data + * has arrived on time. And drop the data when expired. + */ + long currentNumberOfChange = mNumberOfActiveStatusChange.get(); + if (Thread.currentThread() == mUiThread) { + dispatchExtResult(currentNumberOfChange, result); // Dispatch directly + } else { + postResultToUiThread(currentNumberOfChange, result); + } + } + + @AnyThread + protected void postResultToUiThread(long numberOfStatusChange, T result) { + ThreadUtils.postOnMainThread(() -> dispatchExtResult(numberOfStatusChange, result)); + } + + @UiThread + protected void dispatchExtResult(long numberOfStatusChange, T result) { + /** + * For a postResult() sending in between different threads, not only create a latency + * but also enqueued into main UI thread for dispatch. + * + * To align behavior within {@link LifecycleCallbackAdapter#onStateChanged()}, + * some checking on both numberOfStatusChange and {@link Lifecycle} status are required. + */ + if (isActiveStatus(numberOfStatusChange) + && (numberOfStatusChange == mNumberOfActiveStatusChange.get()) + && getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) { + mResultCallback.accept(result); + } + } + + private static final boolean isActiveStatus(long numberOfStatusChange) { + return ((numberOfStatusChange & 1L) != 0L); + } + + /* Implementation of LifecycleCallbackAdapter */ + @UiThread + public boolean isCallbackActive() { + return isActiveStatus(mNumberOfActiveStatusChange.get()); + } + + /* Implementation of LifecycleCallbackAdapter */ + @UiThread + public void setCallbackActive(boolean updatedActiveStatus) { + /** + * Make sure only increase when active status got changed. + * This is to implement the definition of mNumberOfActiveStatusChange. + */ + if (isCallbackActive() != updatedActiveStatus) { + mNumberOfActiveStatusChange.getAndIncrement(); + } + } +} diff --git a/tests/unit/src/com/android/settings/network/helper/LifecycleCallbackConverterTest.java b/tests/unit/src/com/android/settings/network/helper/LifecycleCallbackConverterTest.java new file mode 100644 index 00000000000..149fd8293db --- /dev/null +++ b/tests/unit/src/com/android/settings/network/helper/LifecycleCallbackConverterTest.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2022 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 com.android.settings.network.helper; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LifecycleRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.Phaser; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +@RunWith(AndroidJUnit4.class) +public class LifecycleCallbackConverterTest implements LifecycleOwner { + + private final LifecycleRegistry mRegistry = LifecycleRegistry.createUnsafe(this); + + private Object mTestData; + private TestConsumer mConsumer; + private LifecycleCallbackConverter mConverter; + + @Before + public void setUp() { + mTestData = new Object(); + mConsumer = new TestConsumer(); + mConverter = new LifecycleCallbackConverter(getLifecycle(), mConsumer); + } + + public Lifecycle getLifecycle() { + return mRegistry; + } + + @Test + public void converter_dropResult_whenInActive() { + mConverter.postResult(mTestData); + + assertThat(mConsumer.getCallbackCount()).isEqualTo(0); + } + + @Test + public void converter_callbackResult_whenActive() { + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + + mConverter.postResult(mTestData); + assertThat(mConsumer.getCallbackCount()).isEqualTo(1); + assertThat(mConsumer.getData()).isEqualTo(mTestData); + } + + @Test + public void converter_dropResult_whenBackToInActive() { + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP); + + mConverter.postResult(mTestData); + assertThat(mConsumer.getCallbackCount()).isEqualTo(0); + } + + @Test + public void converter_passResultToUiThread_whenActive() { + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + + final Phaser phaser = new Phaser(1); + Thread executionThread = new Thread(() -> { + mConverter.postResult(phaser); + }); + executionThread.start(); + phaser.awaitAdvance(0); + + assertThat(mConsumer.getData()).isEqualTo(phaser); + assertThat(mConsumer.getCallbackCount()).isEqualTo(1); + } + + public static class TestConsumer implements Consumer { + long mNumberOfCallback; + AtomicReference mLatestData; + + public TestConsumer() { + mLatestData = new AtomicReference(); + } + + public void accept(Object data) { + mLatestData.set(data); + mNumberOfCallback ++; + if ((data != null) && (data instanceof Phaser)) { + ((Phaser)data).arrive(); + } + } + + protected long getCallbackCount() { + return mNumberOfCallback; + } + + protected Object getData() { + return mLatestData.get(); + } + } +}