From dc24107b4d08f492675e9bfc740329b93eb12ddf Mon Sep 17 00:00:00 2001 From: Bryce Lee Date: Wed, 26 Jan 2022 13:08:11 -0800 Subject: [PATCH] Introduce PersistentConnectionManager. PersistentConnectionManager monitors and re-establishes an ObservableServiceConnection on disconnect. Test: atest PackageObserverTest PersistentConnectionManagerTest ObservableServiceConnectionTest Bug: 213906355 Change-Id: I51fb79a2c05cf2f86e7c5e4a3ceecddd162c0ec8 --- .../systemui/util/service/Observer.java | 45 +++++ .../util/service/PackageObserver.java | 107 ++++++++++++ .../service/PersistentConnectionManager.java | 155 ++++++++++++++++++ .../dagger/ObservableServiceModule.java | 65 ++++++++ .../dagger/PackageObserverComponent.java | 42 +++++ .../util/service/PackageObserverTest.java | 77 +++++++++ .../PersistentConnectionManagerTest.java | 138 ++++++++++++++++ 7 files changed, 629 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/util/service/Observer.java create mode 100644 packages/SystemUI/src/com/android/systemui/util/service/PackageObserver.java create mode 100644 packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java create mode 100644 packages/SystemUI/src/com/android/systemui/util/service/dagger/ObservableServiceModule.java create mode 100644 packages/SystemUI/src/com/android/systemui/util/service/dagger/PackageObserverComponent.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/util/service/PackageObserverTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.java diff --git a/packages/SystemUI/src/com/android/systemui/util/service/Observer.java b/packages/SystemUI/src/com/android/systemui/util/service/Observer.java new file mode 100644 index 0000000000000..768743217cc78 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/service/Observer.java @@ -0,0 +1,45 @@ +/* + * 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.systemui.util.service; + +/** + * The {@link Observer} interface specifies an entity which listeners + * can be informed of changes to the source, which will require updating. Note that this deals + * with changes to the source itself, not content which will be updated through the interface. + */ +public interface Observer { + /** + * Callback for receiving updates from the {@link Observer}. + */ + interface Callback { + /** + * Invoked when the source has changed. + */ + void onSourceChanged(); + } + + /** + * Adds a callback to receive future updates from the {@link Observer}. + */ + void addCallback(Callback callback); + + /** + * Removes a callback from receiving further updates. + * @param callback + */ + void removeCallback(Callback callback); +} diff --git a/packages/SystemUI/src/com/android/systemui/util/service/PackageObserver.java b/packages/SystemUI/src/com/android/systemui/util/service/PackageObserver.java new file mode 100644 index 0000000000000..2ee7b20c1f93d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/service/PackageObserver.java @@ -0,0 +1,107 @@ +/* + * 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.systemui.util.service; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.PatternMatcher; +import android.util.Log; + +import com.android.systemui.communal.CommunalSource; + +import com.google.android.collect.Lists; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Iterator; + +import javax.inject.Inject; + +/** + * {@link PackageObserver} allows for monitoring the system for changes relating to a particular + * package. This can be used by {@link CommunalSource} clients to detect when a related package + * has changed and reloading is necessary. + */ +public class PackageObserver implements Observer { + private static final String TAG = "PackageObserver"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private final ArrayList> mCallbacks = Lists.newArrayList(); + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (DEBUG) { + Log.d(TAG, "package added receiver - onReceive"); + } + + final Iterator> iter = mCallbacks.iterator(); + while (iter.hasNext()) { + final Callback callback = iter.next().get(); + if (callback != null) { + callback.onSourceChanged(); + } else { + iter.remove(); + } + } + } + }; + + private final String mPackageName; + private final Context mContext; + + @Inject + public PackageObserver(Context context, ComponentName component) { + mContext = context; + mPackageName = component.getPackageName(); + } + + @Override + public void addCallback(Callback callback) { + if (DEBUG) { + Log.d(TAG, "addCallback:" + callback); + } + mCallbacks.add(new WeakReference<>(callback)); + + // Only register for listening to package additions on first callback. + if (mCallbacks.size() > 1) { + return; + } + + final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + filter.addDataScheme("package"); + filter.addDataSchemeSpecificPart(mPackageName, PatternMatcher.PATTERN_LITERAL); + // Note that we directly register the receiver here as data schemes are not supported by + // BroadcastDispatcher. + mContext.registerReceiver(mReceiver, filter, Context.RECEIVER_EXPORTED); + } + + @Override + public void removeCallback(Callback callback) { + if (DEBUG) { + Log.d(TAG, "removeCallback:" + callback); + } + final boolean removed = mCallbacks.removeIf(el -> el.get() == callback); + + if (removed && mCallbacks.isEmpty()) { + mContext.unregisterReceiver(mReceiver); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java b/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java new file mode 100644 index 0000000000000..292c062369c1d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/service/PersistentConnectionManager.java @@ -0,0 +1,155 @@ +/* + * 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.systemui.util.service; + +import static com.android.systemui.util.service.dagger.ObservableServiceModule.BASE_RECONNECT_DELAY_MS; +import static com.android.systemui.util.service.dagger.ObservableServiceModule.MAX_RECONNECT_ATTEMPTS; +import static com.android.systemui.util.service.dagger.ObservableServiceModule.MIN_CONNECTION_DURATION_MS; +import static com.android.systemui.util.service.dagger.ObservableServiceModule.OBSERVER; +import static com.android.systemui.util.service.dagger.ObservableServiceModule.SERVICE_CONNECTION; + +import android.util.Log; + +import com.android.systemui.util.concurrency.DelayableExecutor; +import com.android.systemui.util.time.SystemClock; + +import javax.inject.Inject; +import javax.inject.Named; + +/** + * The {@link PersistentConnectionManager} is responsible for maintaining a connection to a + * {@link ObservableServiceConnection}. + * @param The transformed connection type handled by the service. + */ +public class PersistentConnectionManager { + private static final String TAG = "PersistentConnManager"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private final SystemClock mSystemClock; + private final DelayableExecutor mMainExecutor; + private final int mBaseReconnectDelayMs; + private final int mMaxReconnectAttempts; + private final int mMinConnectionDuration; + private final Observer mObserver; + + private int mReconnectAttempts = 0; + private Runnable mCurrentReconnectCancelable; + + private final ObservableServiceConnection mConnection; + + private final Runnable mConnectRunnable = new Runnable() { + @Override + public void run() { + mCurrentReconnectCancelable = null; + mConnection.bind(); + } + }; + + private final Observer.Callback mObserverCallback = () -> initiateConnectionAttempt(); + + private final ObservableServiceConnection.Callback mConnectionCallback = + new ObservableServiceConnection.Callback() { + private long mStartTime; + + @Override + public void onConnected(ObservableServiceConnection connection, Object proxy) { + mStartTime = mSystemClock.currentTimeMillis(); + } + + @Override + public void onDisconnected(ObservableServiceConnection connection, int reason) { + if (mSystemClock.currentTimeMillis() - mStartTime > mMinConnectionDuration) { + initiateConnectionAttempt(); + } else { + scheduleConnectionAttempt(); + } + } + }; + + @Inject + public PersistentConnectionManager( + SystemClock clock, + DelayableExecutor mainExecutor, + @Named(SERVICE_CONNECTION) ObservableServiceConnection serviceConnection, + @Named(MAX_RECONNECT_ATTEMPTS) int maxReconnectAttempts, + @Named(BASE_RECONNECT_DELAY_MS) int baseReconnectDelayMs, + @Named(MIN_CONNECTION_DURATION_MS) int minConnectionDurationMs, + @Named(OBSERVER) Observer observer) { + mSystemClock = clock; + mMainExecutor = mainExecutor; + mConnection = serviceConnection; + mObserver = observer; + + mMaxReconnectAttempts = maxReconnectAttempts; + mBaseReconnectDelayMs = baseReconnectDelayMs; + mMinConnectionDuration = minConnectionDurationMs; + } + + /** + * Begins the {@link PersistentConnectionManager} by connecting to the associated service. + */ + public void start() { + mConnection.addCallback(mConnectionCallback); + mObserver.addCallback(mObserverCallback); + initiateConnectionAttempt(); + } + + /** + * Brings down the {@link PersistentConnectionManager}, disconnecting from the service. + */ + public void stop() { + mConnection.removeCallback(mConnectionCallback); + mObserver.removeCallback(mObserverCallback); + mConnection.unbind(); + } + + private void initiateConnectionAttempt() { + // Reset attempts + mReconnectAttempts = 0; + + // The first attempt is always a direct invocation rather than delayed. + mConnection.bind(); + } + + private void scheduleConnectionAttempt() { + // always clear cancelable if present. + if (mCurrentReconnectCancelable != null) { + mCurrentReconnectCancelable.run(); + mCurrentReconnectCancelable = null; + } + + if (mReconnectAttempts >= mMaxReconnectAttempts) { + if (DEBUG) { + Log.d(TAG, "exceeded max connection attempts."); + } + return; + } + + final long reconnectDelayMs = + (long) Math.scalb(mBaseReconnectDelayMs, mReconnectAttempts); + + if (DEBUG) { + Log.d(TAG, + "scheduling connection attempt in " + reconnectDelayMs + "milliseconds"); + } + + mCurrentReconnectCancelable = mMainExecutor.executeDelayed(mConnectRunnable, + reconnectDelayMs); + + mReconnectAttempts++; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/service/dagger/ObservableServiceModule.java b/packages/SystemUI/src/com/android/systemui/util/service/dagger/ObservableServiceModule.java new file mode 100644 index 0000000000000..c62c95755ce8e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/service/dagger/ObservableServiceModule.java @@ -0,0 +1,65 @@ +/* + * 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.systemui.util.service.dagger; + +import android.content.res.Resources; + +import com.android.systemui.R; +import com.android.systemui.dagger.qualifiers.Main; + +import javax.inject.Named; + +import dagger.Module; +import dagger.Provides; + +/** + * Module containing components and parameters for + * {@link com.android.systemui.util.service.ObservableServiceConnection} + * and {@link com.android.systemui.util.service.PersistentConnectionManager}. + */ +@Module(subcomponents = { + PackageObserverComponent.class, +}) +public class ObservableServiceModule { + public static final String MAX_RECONNECT_ATTEMPTS = "max_reconnect_attempts"; + public static final String BASE_RECONNECT_DELAY_MS = "base_reconnect_attempts"; + public static final String MIN_CONNECTION_DURATION_MS = "min_connection_duration_ms"; + public static final String SERVICE_CONNECTION = "service_connection"; + public static final String OBSERVER = "observer"; + + @Provides + @Named(MAX_RECONNECT_ATTEMPTS) + static int providesMaxReconnectAttempts(@Main Resources resources) { + return resources.getInteger( + R.integer.config_communalSourceMaxReconnectAttempts); + } + + @Provides + @Named(BASE_RECONNECT_DELAY_MS) + static int provideBaseReconnectDelayMs(@Main Resources resources) { + return resources.getInteger( + R.integer.config_communalSourceReconnectBaseDelay); + } + + @Provides + @Named(MIN_CONNECTION_DURATION_MS) + static int providesMinConnectionDuration(@Main Resources resources) { + return resources.getInteger( + R.integer.config_connectionMinDuration); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/service/dagger/PackageObserverComponent.java b/packages/SystemUI/src/com/android/systemui/util/service/dagger/PackageObserverComponent.java new file mode 100644 index 0000000000000..8ee39b34cdf57 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/service/dagger/PackageObserverComponent.java @@ -0,0 +1,42 @@ +/* + * 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.systemui.util.service.dagger; + +import android.content.ComponentName; + +import com.android.systemui.util.service.PackageObserver; + +import dagger.BindsInstance; +import dagger.Subcomponent; + +/** + * Generates a scoped {@link PackageObserver}. + */ +@Subcomponent +public interface PackageObserverComponent { + /** + * Generates a {@link PackageObserverComponent} instance. + */ + @Subcomponent.Factory + interface Factory { + PackageObserverComponent create(@BindsInstance ComponentName component); + } + + /** + * Creates a {@link PackageObserver}. + */ + PackageObserver getPackageObserver(); +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/service/PackageObserverTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/service/PackageObserverTest.java new file mode 100644 index 0000000000000..a2fd288ef33e9 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/util/service/PackageObserverTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2021 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.systemui.util.service; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.verify; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class PackageObserverTest extends SysuiTestCase { + @Mock + Context mContext; + + @Mock + Observer.Callback mCallback; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testChange() { + final PackageObserver observer = new PackageObserver(mContext, + ComponentName.unflattenFromString("com.foo.bar/baz")); + final ArgumentCaptor receiverCapture = + ArgumentCaptor.forClass(BroadcastReceiver.class); + + observer.addCallback(mCallback); + + // Verify broadcast receiver registered. + verify(mContext).registerReceiver(receiverCapture.capture(), any(), anyInt()); + + // Simulate package change. + receiverCapture.getValue().onReceive(mContext, new Intent()); + + // Check that callback was informed. + verify(mCallback).onSourceChanged(); + + observer.removeCallback(mCallback); + + // Make sure receiver is unregistered on last callback removal + verify(mContext).unregisterReceiver(receiverCapture.getValue()); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.java new file mode 100644 index 0000000000000..53d4a96b06403 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/util/service/PersistentConnectionManagerTest.java @@ -0,0 +1,138 @@ +/* + * 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.systemui.util.service; + +import static org.mockito.Mockito.verify; + +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.time.FakeSystemClock; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class PersistentConnectionManagerTest extends SysuiTestCase { + private static final int MAX_RETRIES = 5; + private static final int RETRY_DELAY_MS = 1000; + private static final int CONNECTION_MIN_DURATION_MS = 5000; + + private FakeSystemClock mFakeClock = new FakeSystemClock(); + private FakeExecutor mFakeExecutor = new FakeExecutor(mFakeClock); + + @Mock + private ObservableServiceConnection mConnection; + + @Mock + private Observer mObserver; + + private static class Proxy { + } + + private PersistentConnectionManager mConnectionManager; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mConnectionManager = new PersistentConnectionManager<>( + mFakeClock, + mFakeExecutor, + mConnection, + MAX_RETRIES, + RETRY_DELAY_MS, + CONNECTION_MIN_DURATION_MS, + mObserver); + } + + private ObservableServiceConnection.Callback captureCallbackAndSend( + ObservableServiceConnection mConnection, Proxy proxy) { + ArgumentCaptor> connectionCallbackCaptor = + ArgumentCaptor.forClass(ObservableServiceConnection.Callback.class); + + verify(mConnection).addCallback(connectionCallbackCaptor.capture()); + verify(mConnection).bind(); + Mockito.clearInvocations(mConnection); + + final ObservableServiceConnection.Callback callback = connectionCallbackCaptor.getValue(); + if (proxy != null) { + callback.onConnected(mConnection, proxy); + } else { + callback.onDisconnected(mConnection, 0); + } + + return callback; + } + + /** + * Validates initial connection. + */ + @Test + public void testConnect() { + mConnectionManager.start(); + captureCallbackAndSend(mConnection, Mockito.mock(Proxy.class)); + } + + /** + * Ensures reconnection on disconnect. + */ + @Test + public void testRetryOnBindFailure() { + mConnectionManager.start(); + ArgumentCaptor> connectionCallbackCaptor = + ArgumentCaptor.forClass(ObservableServiceConnection.Callback.class); + + verify(mConnection).addCallback(connectionCallbackCaptor.capture()); + + // Verify attempts happen. Note that we account for the retries plus initial attempt, which + // is not scheduled. + for (int attemptCount = 0; attemptCount < MAX_RETRIES + 1; attemptCount++) { + verify(mConnection).bind(); + Mockito.clearInvocations(mConnection); + connectionCallbackCaptor.getValue().onDisconnected(mConnection, 0); + mFakeExecutor.advanceClockToNext(); + mFakeExecutor.runAllReady(); + } + } + + /** + * Ensures rebind on package change. + */ + @Test + public void testAttemptOnPackageChange() { + mConnectionManager.start(); + verify(mConnection).bind(); + ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(Observer.Callback.class); + captureCallbackAndSend(mConnection, Mockito.mock(Proxy.class)); + + verify(mObserver).addCallback(callbackCaptor.capture()); + + callbackCaptor.getValue().onSourceChanged(); + verify(mConnection).bind(); + } +}