CommunalSourcePrimer Introduction.

This changelist	adds CommunalSourcePrimer, a SystemUI
component for loading a	communal source.

Bug: 202398773
Test: atest CommunalSourcePrimerTest
Change-Id: Iba295c93545d837659973ec6b6c1cce844010cd8
This commit is contained in:
Bryce Lee
2021-10-11 18:25:31 -07:00
parent 1a479f6960
commit 56bb75b1ac
3 changed files with 354 additions and 0 deletions

View File

@@ -23,12 +23,36 @@ import com.android.systemui.util.ViewController;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.Optional;
/**
* {@link CommunalSource} defines an interface for working with a source for communal data. Clients
* may request a communal surface that can be shown within a {@link android.view.SurfaceView}.
* Callbacks may also be registered to listen to state changes.
*/
public interface CommunalSource {
/**
* {@link Connector} defines an interface for {@link CommunalSource} instances to be generated.
*/
interface Connector {
ListenableFuture<Optional<CommunalSource>> connect();
}
/**
* The {@link Observer} interface specifies an entity which {@link CommunalSource} 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
* {@link CommunalSource} interface.
*/
interface Observer {
interface Callback {
void onSourceChanged();
}
void addCallback(Callback callback);
void removeCallback(Callback callback);
}
/**
* {@link CommunalViewResult} is handed back from {@link #requestCommunalView(Context)} and
* contains the view to be displayed and its associated controller.

View File

@@ -0,0 +1,161 @@
/*
* 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.communal;
import android.content.Context;
import android.content.res.Resources;
import android.util.Log;
import com.android.systemui.CoreStartable;
import com.android.systemui.R;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.util.concurrency.DelayableExecutor;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.Optional;
import javax.inject.Inject;
/**
* The {@link CommunalSourcePrimer} is responsible for priming SystemUI with a pre-configured
* Communal source. The SystemUI service binds to the component to retrieve the
* {@link CommunalSource}. {@link CommunalSourcePrimer} has no effect
* if there is no pre-defined value.
*/
@SysUISingleton
public class CommunalSourcePrimer extends CoreStartable {
private static final String TAG = "CommunalSourcePrimer";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private final DelayableExecutor mMainExecutor;
private final CommunalSourceMonitor mMonitor;
private final int mBaseReconnectDelayMs;
private final int mMaxReconnectAttempts;
private int mReconnectAttempts = 0;
private Runnable mCurrentReconnectCancelable;
private ListenableFuture<Optional<CommunalSource>> mGetSourceFuture;
private final Optional<CommunalSource.Connector> mConnector;
private final Optional<CommunalSource.Observer> mObserver;
private final Runnable mConnectRunnable = new Runnable() {
@Override
public void run() {
mCurrentReconnectCancelable = null;
connect();
}
};
@Inject
public CommunalSourcePrimer(Context context, @Main Resources resources,
DelayableExecutor mainExecutor,
CommunalSourceMonitor monitor,
Optional<CommunalSource.Connector> connector,
Optional<CommunalSource.Observer> observer) {
super(context);
mMainExecutor = mainExecutor;
mMonitor = monitor;
mConnector = connector;
mObserver = observer;
mMaxReconnectAttempts = resources.getInteger(
R.integer.config_communalSourceMaxReconnectAttempts);
mBaseReconnectDelayMs = resources.getInteger(
R.integer.config_communalSourceReconnectBaseDelay);
}
@Override
public void start() {
}
private void initiateConnectionAttempt() {
// Reset attempts
mReconnectAttempts = 0;
mMonitor.setSource(null);
// The first attempt is always a direct invocation rather than delayed.
connect();
}
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++;
}
@Override
protected void onBootCompleted() {
if (mObserver.isPresent()) {
mObserver.get().addCallback(() -> initiateConnectionAttempt());
}
initiateConnectionAttempt();
}
private void connect() {
if (DEBUG) {
Log.d(TAG, "attempting to communal to communal source");
}
if (mGetSourceFuture != null) {
if (DEBUG) {
Log.d(TAG, "canceling in-flight connection");
}
mGetSourceFuture.cancel(true);
}
mGetSourceFuture = mConnector.get().connect();
mGetSourceFuture.addListener(() -> {
try {
Optional<CommunalSource> result = mGetSourceFuture.get();
if (result.isPresent()) {
final CommunalSource source = result.get();
source.addCallback(() -> initiateConnectionAttempt());
mMonitor.setSource(source);
} else {
scheduleConnectionAttempt();
}
} catch (Exception e) {
e.printStackTrace();
}
}, mMainExecutor);
}
}

View File

@@ -0,0 +1,169 @@
/*
* 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.communal;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.res.Resources;
import android.testing.AndroidTestingRunner;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.test.filters.SmallTest;
import com.android.systemui.R;
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;
import java.util.Optional;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class CommunalSourcePrimerTest extends SysuiTestCase {
private static final String TEST_COMPONENT_NAME = "com.google.tests/.CommunalService";
private static final int MAX_RETRIES = 5;
private static final int RETRY_DELAY_MS = 1000;
@Mock
private Context mContext;
@Mock
private Resources mResources;
private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
@Mock
private CommunalSource mSource;
@Mock
private CommunalSourceMonitor mCommunalSourceMonitor;
@Mock
private CommunalSource.Connector mConnector;
@Mock
private CommunalSource.Observer mObserver;
private CommunalSourcePrimer mPrimer;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
when(mResources.getInteger(R.integer.config_communalSourceMaxReconnectAttempts))
.thenReturn(MAX_RETRIES);
when(mResources.getInteger(R.integer.config_communalSourceReconnectBaseDelay))
.thenReturn(RETRY_DELAY_MS);
when(mResources.getString(R.string.config_communalSourceComponent))
.thenReturn(TEST_COMPONENT_NAME);
mPrimer = new CommunalSourcePrimer(mContext, mResources, mFakeExecutor,
mCommunalSourceMonitor, Optional.of(mConnector), Optional.of(mObserver));
}
@Test
public void testConnect() {
when(mConnector.connect()).thenReturn(
CallbackToFutureAdapter.getFuture(completer -> {
completer.set(Optional.of(mSource));
return "test";
}));
mPrimer.onBootCompleted();
mFakeExecutor.runAllReady();
verify(mCommunalSourceMonitor).setSource(mSource);
}
@Test
public void testRetryOnBindFailure() throws Exception {
when(mConnector.connect()).thenReturn(
CallbackToFutureAdapter.getFuture(completer -> {
completer.set(Optional.empty());
return "test";
}));
mPrimer.onBootCompleted();
mFakeExecutor.runAllReady();
// 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(mConnector, times(1)).connect();
clearInvocations(mConnector);
mFakeExecutor.advanceClockToNext();
mFakeExecutor.runAllReady();
}
verify(mCommunalSourceMonitor, never()).setSource(Mockito.notNull());
}
@Test
public void testAttemptOnPackageChange() {
when(mConnector.connect()).thenReturn(
CallbackToFutureAdapter.getFuture(completer -> {
completer.set(Optional.empty());
return "test";
}));
mPrimer.onBootCompleted();
mFakeExecutor.runAllReady();
final ArgumentCaptor<CommunalSource.Observer.Callback> callbackCaptor =
ArgumentCaptor.forClass(CommunalSource.Observer.Callback.class);
verify(mObserver).addCallback(callbackCaptor.capture());
clearInvocations(mConnector);
callbackCaptor.getValue().onSourceChanged();
verify(mConnector, times(1)).connect();
}
@Test
public void testDisconnect() {
final ArgumentCaptor<CommunalSource.Callback> callbackCaptor =
ArgumentCaptor.forClass(CommunalSource.Callback.class);
when(mConnector.connect()).thenReturn(
CallbackToFutureAdapter.getFuture(completer -> {
completer.set(Optional.of(mSource));
return "test";
}));
mPrimer.onBootCompleted();
mFakeExecutor.runAllReady();
verify(mCommunalSourceMonitor).setSource(mSource);
verify(mSource).addCallback(callbackCaptor.capture());
clearInvocations(mConnector);
callbackCaptor.getValue().onDisconnected();
mFakeExecutor.runAllReady();
verify(mConnector).connect();
}
}