Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 47394cdb authored by Tony Mantler's avatar Tony Mantler
Browse files

Move ServiceListing to SettingsLib and add tests

Bug: 70902607
Test: RunSettingsLibRoboTests
Change-Id: I172092f8c249e76667136442080d5fd7a6a6ef01
parent 247791f9
Loading
Loading
Loading
Loading
+226 −0
Original line number Diff line number Diff line
/*
 * 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);
        }
    }
}
+113 −0
Original line number Diff line number Diff line
/*
 * 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());
    }
}
+54 −0
Original line number Diff line number Diff line
/*
 * 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();
    }
}