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