diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index ff748a982b33b..df8fc22827b1a 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -742,4 +742,8 @@
false
+
+
+
+
diff --git a/packages/SystemUI/src/com/android/systemui/communal/PackageObserver.java b/packages/SystemUI/src/com/android/systemui/communal/PackageObserver.java
new file mode 100644
index 0000000000000..3d25d126a2914
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/PackageObserver.java
@@ -0,0 +1,101 @@
+/*
+ * 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.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.PatternMatcher;
+import android.util.Log;
+
+import com.google.android.collect.Lists;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+/**
+ * {@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 CommunalSource.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;
+
+ public PackageObserver(Context context, String packageName) {
+ mContext = context;
+ mPackageName = packageName;
+ }
+
+ @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/communal/dagger/CommunalModule.java b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java
index 9b72655c9fb5b..a0986660ebdca 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java
+++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java
@@ -16,26 +16,40 @@
package com.android.systemui.communal.dagger;
+import android.content.ComponentName;
import android.content.Context;
+import android.content.res.Resources;
+import android.text.TextUtils;
import android.view.View;
import android.widget.FrameLayout;
+import androidx.annotation.Nullable;
+
+import com.android.systemui.R;
+import com.android.systemui.communal.CommunalSource;
+import com.android.systemui.communal.PackageObserver;
import com.android.systemui.communal.conditions.CommunalCondition;
import com.android.systemui.communal.conditions.CommunalSettingCondition;
+import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.idle.AmbientLightModeMonitor;
import com.android.systemui.idle.LightSensorEventsDebounceAlgorithm;
import com.android.systemui.idle.dagger.IdleViewComponent;
import java.util.Collections;
import java.util.HashSet;
+import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import javax.inject.Named;
+import javax.inject.Provider;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.ElementsIntoSet;
+import dagger.multibindings.IntoMap;
+import dagger.multibindings.StringKey;
/**
* Dagger Module providing Communal-related functionality.
@@ -56,6 +70,20 @@ public interface CommunalModule {
return view;
}
+ /** */
+ @Provides
+ static Optional provideCommunalSourcePackageObserver(
+ Context context, @Main Resources resources) {
+ final String componentName = resources.getString(R.string.config_communalSourceComponent);
+
+ if (TextUtils.isEmpty(componentName)) {
+ return Optional.empty();
+ }
+
+ return Optional.of(new PackageObserver(context,
+ ComponentName.unflattenFromString(componentName).getPackageName()));
+ }
+
/**
* Provides LightSensorEventsDebounceAlgorithm as an instance to DebounceAlgorithm interface.
* @param algorithm the instance of algorithm that is bound to the interface.
@@ -75,4 +103,36 @@ public interface CommunalModule {
CommunalSettingCondition communalSettingCondition) {
return new HashSet<>(Collections.singletonList(communalSettingCondition));
}
+
+ /**
+ * TODO(b/205638389): Remove when there is a base implementation of
+ * {@link CommunalSource.Connector}. Currently a place holder to allow a map to be present.
+ */
+ @Provides
+ @IntoMap
+ @Nullable
+ @StringKey("empty")
+ static CommunalSource.Connector provideEmptyCommunalSourceConnector() {
+ return null;
+ }
+
+ /** */
+ @Provides
+ static Optional provideCommunalSourceConnector(
+ @Main Resources resources,
+ Map, Provider> connectorCreators) {
+ final String className = resources.getString(R.string.config_communalSourceConnector);
+
+ if (TextUtils.isEmpty(className)) {
+ return Optional.empty();
+ }
+
+ try {
+ Class> clazz = Class.forName(className);
+ Provider provider = connectorCreators.get(clazz);
+ return provider != null ? Optional.of(provider.get()) : Optional.empty();
+ } catch (ClassNotFoundException e) {
+ return Optional.empty();
+ }
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/PackageObserverTest.java b/packages/SystemUI/tests/src/com/android/systemui/communal/PackageObserverTest.java
new file mode 100644
index 0000000000000..1cd60698353bf
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/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.communal;
+
+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.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 {
+ private static final String PACKAGE_NAME = "com.foo.bar";
+
+ @Mock
+ Context mContext;
+
+ @Mock
+ CommunalSource.Observer.Callback mCallback;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void testChange() {
+ final PackageObserver observer = new PackageObserver(mContext, PACKAGE_NAME);
+ 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());
+ }
+}