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