Merge changes from topic "202398773"

* changes:
  CommunalSourcePrimer Introduction.
  Marshal CommunalCoordinator callbacks to main thread.
This commit is contained in:
Bryce Lee
2021-11-19 18:28:20 +00:00
committed by Android (Google) Code Review
5 changed files with 377 additions and 4 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

@@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.collection.coordinator;
import androidx.annotation.NonNull;
import com.android.systemui.communal.CommunalStateController;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
@@ -26,6 +27,8 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
import java.util.concurrent.Executor;
import javax.inject.Inject;
/**
@@ -34,14 +37,17 @@ import javax.inject.Inject;
*/
@CoordinatorScope
public class CommunalCoordinator implements Coordinator {
final Executor mExecutor;
final CommunalStateController mCommunalStateController;
final NotificationEntryManager mNotificationEntryManager;
final NotificationLockscreenUserManager mNotificationLockscreenUserManager;
@Inject
public CommunalCoordinator(NotificationEntryManager notificationEntryManager,
public CommunalCoordinator(@Main Executor executor,
NotificationEntryManager notificationEntryManager,
NotificationLockscreenUserManager notificationLockscreenUserManager,
CommunalStateController communalStateController) {
mExecutor = executor;
mNotificationEntryManager = notificationEntryManager;
mNotificationLockscreenUserManager = notificationLockscreenUserManager;
mCommunalStateController = communalStateController;
@@ -57,8 +63,10 @@ public class CommunalCoordinator implements Coordinator {
final CommunalStateController.Callback mStateCallback = new CommunalStateController.Callback() {
@Override
public void onCommunalViewShowingChanged() {
mFilter.invalidateList();
mNotificationEntryManager.updateNotifications("Communal mode state changed");
mExecutor.execute(() -> {
mFilter.invalidateList();
mNotificationEntryManager.updateNotifications("Communal mode state changed");
});
}
};

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

View File

@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.notification.collection.coordinator;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import android.test.suitebuilder.annotation.SmallTest;
@@ -29,6 +30,8 @@ import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
import org.junit.Before;
import org.junit.Test;
@@ -39,6 +42,8 @@ import org.mockito.MockitoAnnotations;
@SmallTest
public class CommunalCoordinatorTest extends SysuiTestCase {
private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
@Mock
CommunalStateController mCommunalStateController;
@Mock
@@ -57,7 +62,7 @@ public class CommunalCoordinatorTest extends SysuiTestCase {
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mCoordinator = new CommunalCoordinator(mNotificationEntryManager,
mCoordinator = new CommunalCoordinator(mExecutor, mNotificationEntryManager,
mNotificationLockscreenUserManager, mCommunalStateController);
}
@@ -84,6 +89,12 @@ public class CommunalCoordinatorTest extends SysuiTestCase {
// Verify that notifications are filtered out when communal is showing and that the filter
// pipeline is notified.
stateCallback.onCommunalViewShowingChanged();
// Make sure callback depends on executor to run.
verify(mFilterListener, never()).onPluggableInvalidated(any());
verify(mNotificationEntryManager, never()).updateNotifications(any());
mExecutor.runAllReady();
verify(mFilterListener).onPluggableInvalidated(any());
verify(mNotificationEntryManager).updateNotifications(any());
assert (filter.shouldFilterOut(mNotificationEntry, 0));