Merge "Introduce PersistentConnectionManager."

This commit is contained in:
Bryce Lee
2022-02-01 07:10:45 +00:00
committed by Android (Google) Code Review
7 changed files with 629 additions and 0 deletions

View File

@@ -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);
}

View File

@@ -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<WeakReference<Callback>> 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<WeakReference<Callback>> 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);
}
}
}

View File

@@ -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 <T> The transformed connection type handled by the service.
*/
public class PersistentConnectionManager<T> {
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<T> 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<T> 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++;
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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<BroadcastReceiver> 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());
}
}

View File

@@ -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<Proxy> mConnection;
@Mock
private Observer mObserver;
private static class Proxy {
}
private PersistentConnectionManager<Proxy> 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<Proxy> captureCallbackAndSend(
ObservableServiceConnection<Proxy> mConnection, Proxy proxy) {
ArgumentCaptor<ObservableServiceConnection.Callback<Proxy>> 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<ObservableServiceConnection.Callback<Proxy>> 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<Observer.Callback> callbackCaptor =
ArgumentCaptor.forClass(Observer.Callback.class);
captureCallbackAndSend(mConnection, Mockito.mock(Proxy.class));
verify(mObserver).addCallback(callbackCaptor.capture());
callbackCaptor.getValue().onSourceChanged();
verify(mConnection).bind();
}
}