summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/applications/ServiceListing.java226
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ServiceListingTest.java113
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/testutils/shadow/ShadowPackageManagerWrapper.java54
3 files changed, 393 insertions, 0 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ServiceListing.java b/packages/SettingsLib/src/com/android/settingslib/applications/ServiceListing.java
new file mode 100644
index 000000000000..3c3c70ac364e
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ServiceListing.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2017 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.settingslib.applications;
+
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Settings;
+import android.util.Slog;
+
+import com.android.settingslib.wrapper.PackageManagerWrapper;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Class for managing services matching a given intent and requesting a given permission.
+ */
+public class ServiceListing {
+ private final ContentResolver mContentResolver;
+ private final Context mContext;
+ private final String mTag;
+ private final String mSetting;
+ private final String mIntentAction;
+ private final String mPermission;
+ private final String mNoun;
+ private final HashSet<ComponentName> mEnabledServices = new HashSet<>();
+ private final List<ServiceInfo> mServices = new ArrayList<>();
+ private final List<Callback> mCallbacks = new ArrayList<>();
+
+ private boolean mListening;
+
+ private ServiceListing(Context context, String tag,
+ String setting, String intentAction, String permission, String noun) {
+ mContentResolver = context.getContentResolver();
+ mContext = context;
+ mTag = tag;
+ mSetting = setting;
+ mIntentAction = intentAction;
+ mPermission = permission;
+ mNoun = noun;
+ }
+
+ public void addCallback(Callback callback) {
+ mCallbacks.add(callback);
+ }
+
+ public void removeCallback(Callback callback) {
+ mCallbacks.remove(callback);
+ }
+
+ public void setListening(boolean listening) {
+ if (mListening == listening) return;
+ mListening = listening;
+ if (mListening) {
+ // listen for package changes
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+ filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+ filter.addDataScheme("package");
+ mContext.registerReceiver(mPackageReceiver, filter);
+ mContentResolver.registerContentObserver(Settings.Secure.getUriFor(mSetting),
+ false, mSettingsObserver);
+ } else {
+ mContext.unregisterReceiver(mPackageReceiver);
+ mContentResolver.unregisterContentObserver(mSettingsObserver);
+ }
+ }
+
+ private void saveEnabledServices() {
+ StringBuilder sb = null;
+ for (ComponentName cn : mEnabledServices) {
+ if (sb == null) {
+ sb = new StringBuilder();
+ } else {
+ sb.append(':');
+ }
+ sb.append(cn.flattenToString());
+ }
+ Settings.Secure.putString(mContentResolver, mSetting,
+ sb != null ? sb.toString() : "");
+ }
+
+ private void loadEnabledServices() {
+ mEnabledServices.clear();
+ final String flat = Settings.Secure.getString(mContentResolver, mSetting);
+ if (flat != null && !"".equals(flat)) {
+ final String[] names = flat.split(":");
+ for (String name : names) {
+ final ComponentName cn = ComponentName.unflattenFromString(name);
+ if (cn != null) {
+ mEnabledServices.add(cn);
+ }
+ }
+ }
+ }
+
+ public void reload() {
+ loadEnabledServices();
+ mServices.clear();
+ final int user = ActivityManager.getCurrentUser();
+
+ final PackageManagerWrapper pmWrapper =
+ new PackageManagerWrapper(mContext.getPackageManager());
+ List<ResolveInfo> installedServices = pmWrapper.queryIntentServicesAsUser(
+ new Intent(mIntentAction),
+ PackageManager.GET_SERVICES | PackageManager.GET_META_DATA,
+ user);
+
+ for (ResolveInfo resolveInfo : installedServices) {
+ ServiceInfo info = resolveInfo.serviceInfo;
+
+ if (!mPermission.equals(info.permission)) {
+ Slog.w(mTag, "Skipping " + mNoun + " service "
+ + info.packageName + "/" + info.name
+ + ": it does not require the permission "
+ + mPermission);
+ continue;
+ }
+ mServices.add(info);
+ }
+ for (Callback callback : mCallbacks) {
+ callback.onServicesReloaded(mServices);
+ }
+ }
+
+ public boolean isEnabled(ComponentName cn) {
+ return mEnabledServices.contains(cn);
+ }
+
+ public void setEnabled(ComponentName cn, boolean enabled) {
+ if (enabled) {
+ mEnabledServices.add(cn);
+ } else {
+ mEnabledServices.remove(cn);
+ }
+ saveEnabledServices();
+ }
+
+ private final ContentObserver mSettingsObserver = new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ reload();
+ }
+ };
+
+ private final BroadcastReceiver mPackageReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ reload();
+ }
+ };
+
+ public interface Callback {
+ void onServicesReloaded(List<ServiceInfo> services);
+ }
+
+ public static class Builder {
+ private final Context mContext;
+ private String mTag;
+ private String mSetting;
+ private String mIntentAction;
+ private String mPermission;
+ private String mNoun;
+
+ public Builder(Context context) {
+ mContext = context;
+ }
+
+ public Builder setTag(String tag) {
+ mTag = tag;
+ return this;
+ }
+
+ public Builder setSetting(String setting) {
+ mSetting = setting;
+ return this;
+ }
+
+ public Builder setIntentAction(String intentAction) {
+ mIntentAction = intentAction;
+ return this;
+ }
+
+ public Builder setPermission(String permission) {
+ mPermission = permission;
+ return this;
+ }
+
+ public Builder setNoun(String noun) {
+ mNoun = noun;
+ return this;
+ }
+
+ public ServiceListing build() {
+ return new ServiceListing(mContext, mTag, mSetting, mIntentAction, mPermission, mNoun);
+ }
+ }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ServiceListingTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ServiceListingTest.java
new file mode 100644
index 000000000000..fa31a7d22ae3
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ServiceListingTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2017 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.settingslib.applications;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.ComponentName;
+import android.provider.Settings;
+
+import com.android.settingslib.SettingsLibRobolectricTestRunner;
+import com.android.settingslib.TestConfig;
+import com.android.settingslib.testutils.shadow.ShadowPackageManagerWrapper;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(SettingsLibRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION,
+ shadows = {ShadowPackageManagerWrapper.class})
+public class ServiceListingTest {
+
+ private static final String TEST_SETTING = "testSetting";
+ private static final String TEST_INTENT = "com.example.intent";
+ private static final String TEST_PERMISSION = "testPermission";
+
+ private ServiceListing mServiceListing;
+
+ @Before
+ public void setUp() {
+ mServiceListing = new ServiceListing.Builder(RuntimeEnvironment.application)
+ .setTag("testTag")
+ .setSetting(TEST_SETTING)
+ .setNoun("testNoun")
+ .setIntentAction(TEST_INTENT)
+ .setPermission("testPermission")
+ .build();
+ }
+
+ @After
+ public void tearDown() {
+ ShadowPackageManagerWrapper.reset();
+ }
+
+ @Test
+ public void testCallback() {
+ ServiceListing.Callback callback = mock(ServiceListing.Callback.class);
+ mServiceListing.addCallback(callback);
+ mServiceListing.reload();
+ verify(callback, times(1)).onServicesReloaded(anyList());
+ mServiceListing.removeCallback(callback);
+ mServiceListing.reload();
+ verify(callback, times(1)).onServicesReloaded(anyList());
+ }
+
+ @Test
+ public void testSaveLoad() {
+ ComponentName testComponent1 = new ComponentName("testPackage1", "testClass1");
+ ComponentName testComponent2 = new ComponentName("testPackage2", "testClass2");
+ Settings.Secure.putString(RuntimeEnvironment.application.getContentResolver(),
+ TEST_SETTING,
+ testComponent1.flattenToString() + ":" + testComponent2.flattenToString());
+
+ mServiceListing.reload();
+
+ assertThat(mServiceListing.isEnabled(testComponent1)).isTrue();
+ assertThat(mServiceListing.isEnabled(testComponent2)).isTrue();
+ assertThat(Settings.Secure.getString(RuntimeEnvironment.application.getContentResolver(),
+ TEST_SETTING)).contains(testComponent1.flattenToString());
+ assertThat(Settings.Secure.getString(RuntimeEnvironment.application.getContentResolver(),
+ TEST_SETTING)).contains(testComponent2.flattenToString());
+
+ mServiceListing.setEnabled(testComponent1, false);
+
+ assertThat(mServiceListing.isEnabled(testComponent1)).isFalse();
+ assertThat(mServiceListing.isEnabled(testComponent2)).isTrue();
+ assertThat(Settings.Secure.getString(RuntimeEnvironment.application.getContentResolver(),
+ TEST_SETTING)).doesNotContain(testComponent1.flattenToString());
+ assertThat(Settings.Secure.getString(RuntimeEnvironment.application.getContentResolver(),
+ TEST_SETTING)).contains(testComponent2.flattenToString());
+
+ mServiceListing.setEnabled(testComponent1, true);
+
+ assertThat(mServiceListing.isEnabled(testComponent1)).isTrue();
+ assertThat(mServiceListing.isEnabled(testComponent2)).isTrue();
+ assertThat(Settings.Secure.getString(RuntimeEnvironment.application.getContentResolver(),
+ TEST_SETTING)).contains(testComponent1.flattenToString());
+ assertThat(Settings.Secure.getString(RuntimeEnvironment.application.getContentResolver(),
+ TEST_SETTING)).contains(testComponent2.flattenToString());
+ }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/testutils/shadow/ShadowPackageManagerWrapper.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/testutils/shadow/ShadowPackageManagerWrapper.java
new file mode 100644
index 000000000000..1fdca27259e2
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/testutils/shadow/ShadowPackageManagerWrapper.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2017 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.settingslib.testutils.shadow;
+
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.util.ArrayMap;
+
+import com.android.settingslib.wrapper.PackageManagerWrapper;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Shadow for {@link PackageManagerWrapper} to allow stubbing hidden methods.
+ */
+@Implements(PackageManagerWrapper.class)
+public class ShadowPackageManagerWrapper {
+ private static final Map<Intent, List<ResolveInfo>> intentServices = new ArrayMap<>();
+
+ @Implementation
+ public List<ResolveInfo> queryIntentServicesAsUser(Intent intent, int i, int user) {
+ List<ResolveInfo> list = intentServices.get(intent);
+ return list != null ? list : Collections.emptyList();
+ }
+
+ public static void addResolveInfoForIntent(Intent intent, ResolveInfo info) {
+ List<ResolveInfo> infoList = intentServices.computeIfAbsent(intent, k -> new ArrayList<>());
+ infoList.add(info);
+ }
+
+ public static void reset() {
+ intentServices.clear();
+ }
+}