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

Commit 4a39e1bb authored by Fyodor Kupolov's avatar Fyodor Kupolov Committed by Android (Google) Code Review
Browse files

Merge "Added unit test for RegisteredServicesCache"

parents 2bfd8678 9e0d81e8
Loading
Loading
Loading
Loading
+30 −16
Original line number Diff line number Diff line
@@ -35,6 +35,7 @@ import android.util.SparseArray;
import android.util.Xml;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.FastXmlSerializer;
import com.google.android.collect.Lists;
@@ -56,6 +57,8 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;

import libcore.io.IoUtils;

/**
 * Cache of registered services. This cache is lazily built by interrogating
 * {@link PackageManager} on a per-user basis. It's updated as packages are
@@ -113,13 +116,19 @@ public abstract class RegisteredServicesCache<V> {

    public RegisteredServicesCache(Context context, String interfaceName, String metaDataName,
            String attributeName, XmlSerializerAndParser<V> serializerAndParser) {
        this(context, interfaceName, metaDataName, attributeName, serializerAndParser,
                Environment.getDataDirectory());
    }

    @VisibleForTesting
    protected RegisteredServicesCache(Context context, String interfaceName, String metaDataName,
            String attributeName, XmlSerializerAndParser<V> serializerAndParser, File dataDir) {
        mContext = context;
        mInterfaceName = interfaceName;
        mMetaDataName = metaDataName;
        mAttributesName = attributeName;
        mSerializerAndParser = serializerAndParser;

        File dataDir = Environment.getDataDirectory();
        File systemDir = new File(dataDir, "system");
        File syncDir = new File(systemDir, "registered_services");
        mPersistentServicesFile = new AtomicFile(new File(syncDir, interfaceName + ".xml"));
@@ -303,7 +312,8 @@ public abstract class RegisteredServicesCache<V> {
        }
    }

    private boolean inSystemImage(int callerUid) {
    @VisibleForTesting
    protected boolean inSystemImage(int callerUid) {
        String[] packages = mContext.getPackageManager().getPackagesForUid(callerUid);
        for (String name : packages) {
            try {
@@ -319,6 +329,13 @@ public abstract class RegisteredServicesCache<V> {
        return false;
    }

    @VisibleForTesting
    protected List<ResolveInfo> queryIntentServices(int userId) {
        final PackageManager pm = mContext.getPackageManager();
        return pm.queryIntentServicesAsUser(
                new Intent(mInterfaceName), PackageManager.GET_META_DATA, userId);
    }

    /**
     * Populate {@link UserServices#services} by scanning installed packages for
     * given {@link UserHandle}.
@@ -331,10 +348,8 @@ public abstract class RegisteredServicesCache<V> {
            Slog.d(TAG, "generateServicesMap() for " + userId + ", changed UIDs = " + changedUids);
        }

        final PackageManager pm = mContext.getPackageManager();
        final ArrayList<ServiceInfo<V>> serviceInfos = new ArrayList<ServiceInfo<V>>();
        final List<ResolveInfo> resolveInfos = pm.queryIntentServicesAsUser(
                new Intent(mInterfaceName), PackageManager.GET_META_DATA, userId);
        final List<ResolveInfo> resolveInfos = queryIntentServices(userId);
        for (ResolveInfo resolveInfo : resolveInfos) {
            try {
                ServiceInfo<V> info = parseServiceInfo(resolveInfo);
@@ -343,9 +358,7 @@ public abstract class RegisteredServicesCache<V> {
                    continue;
                }
                serviceInfos.add(info);
            } catch (XmlPullParserException e) {
                Log.w(TAG, "Unable to load service info " + resolveInfo.toString(), e);
            } catch (IOException e) {
            } catch (XmlPullParserException|IOException e) {
                Log.w(TAG, "Unable to load service info " + resolveInfo.toString(), e);
            }
        }
@@ -481,7 +494,8 @@ public abstract class RegisteredServicesCache<V> {
        return false;
    }

    private ServiceInfo<V> parseServiceInfo(ResolveInfo service)
    @VisibleForTesting
    protected ServiceInfo<V> parseServiceInfo(ResolveInfo service)
            throws XmlPullParserException, IOException {
        android.content.pm.ServiceInfo si = service.serviceInfo;
        ComponentName componentName = new ComponentName(si.packageName, si.name);
@@ -571,12 +585,7 @@ public abstract class RegisteredServicesCache<V> {
        } catch (Exception e) {
            Log.w(TAG, "Error reading persistent services, starting from scratch", e);
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (java.io.IOException e1) {
                }
            }
            IoUtils.closeQuietly(fis);
        }
    }

@@ -607,7 +616,7 @@ public abstract class RegisteredServicesCache<V> {
            out.endTag(null, "services");
            out.endDocument();
            mPersistentServicesFile.finishWrite(fos);
        } catch (java.io.IOException e1) {
        } catch (IOException e1) {
            Log.w(TAG, "Error writing accounts", e1);
            if (fos != null) {
                mPersistentServicesFile.failWrite(fos);
@@ -615,6 +624,11 @@ public abstract class RegisteredServicesCache<V> {
        }
    }

    @VisibleForTesting
    protected Map<V, Integer> getPersistentServices(int userId) {
        return findOrCreateUserLocked(userId).persistentServices;
    }

    public abstract V parseServiceAttributes(Resources res,
            String packageName, AttributeSet attrs);
}
+283 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 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 android.content.pm;

import android.content.Context;
import android.content.res.Resources;
import android.os.FileUtils;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.UserHandle;
import android.test.AndroidTestCase;
import android.util.AttributeSet;
import android.util.SparseArray;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Tests for {@link android.content.pm.RegisteredServicesCache}
 */
public class RegisteredServicesCacheTest extends AndroidTestCase {

    private final ResolveInfo r1 = new ResolveInfo();
    private final ResolveInfo r2 = new ResolveInfo();
    private final TestServiceType t1 = new TestServiceType("t1", "value1");
    private final TestServiceType t2 = new TestServiceType("t2", "value2");
    private File mDataDir;
    private File mSyncDir;

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        File cacheDir = mContext.getCacheDir();
        mDataDir = new File(cacheDir, "testServicesCache");
        FileUtils.deleteContents(mDataDir);
        mSyncDir = new File(mDataDir, "system/registered_services");
        mSyncDir.mkdirs();
    }

    public void testGetAllServicesHappyPath() {
        TestServicesCache cache = new TestServicesCache(mContext, mDataDir);
        cache.addServiceForQuerying(0, r1, new RegisteredServicesCache.ServiceInfo<>(t1, null, 1));
        cache.addServiceForQuerying(0, r2, new RegisteredServicesCache.ServiceInfo<>(t2, null, 2));
        assertEquals(2, cache.getAllServicesSize(0));
        assertEquals(2, cache.getPersistentServicesSize(0));
        File file = new File(mSyncDir, TestServicesCache.SERVICE_INTERFACE + ".xml");
        assertTrue("File should be created at " + file, file.length() > 0);
        // Make sure all services can be loaded from xml
        cache = new TestServicesCache(mContext, mDataDir);
        assertEquals(2, cache.getPersistentServicesSize(0));
    }

    public void testGetAllServicesReplaceUid() {
        TestServicesCache cache = new TestServicesCache(mContext, mDataDir);
        cache.addServiceForQuerying(0, r1, new RegisteredServicesCache.ServiceInfo<>(t1, null, 1));
        cache.addServiceForQuerying(0, r2, new RegisteredServicesCache.ServiceInfo<>(t2, null, 2));
        cache.getAllServices(0);
        // Invalidate cache and clear update query results
        cache.invalidateCache(0);
        cache.clearServicesForQuerying();
        cache.addServiceForQuerying(0, r1, new RegisteredServicesCache.ServiceInfo<>(t1, null, 1));
        cache.addServiceForQuerying(0, r2, new RegisteredServicesCache.ServiceInfo<>(t2, null,
                TestServicesCache.SYSTEM_IMAGE_UID));
        Collection<RegisteredServicesCache.ServiceInfo<TestServiceType>> allServices = cache
                .getAllServices(0);
        assertEquals(2, allServices.size());
        Set<Integer> uids = new HashSet<>();
        for (RegisteredServicesCache.ServiceInfo<TestServiceType> srv : allServices) {
            uids.add(srv.uid);
        }
        assertTrue("UID must be updated to the new value",
                uids.contains(TestServicesCache.SYSTEM_IMAGE_UID));
        assertFalse("UID must be updated to the new value", uids.contains(2));
    }

    public void testGetAllServicesServiceRemoved() {
        TestServicesCache cache = new TestServicesCache(mContext, mDataDir);
        cache.addServiceForQuerying(0, r1, new RegisteredServicesCache.ServiceInfo<>(t1, null, 1));
        cache.addServiceForQuerying(0, r2, new RegisteredServicesCache.ServiceInfo<>(t2, null, 2));
        assertEquals(2, cache.getAllServicesSize(0));
        assertEquals(2, cache.getPersistentServicesSize(0));
        // Re-read data from disk and verify services were saved
        cache = new TestServicesCache(mContext, mDataDir);
        assertEquals(2, cache.getPersistentServicesSize(0));
        // Now register only one service and verify that another one is removed
        cache.addServiceForQuerying(0, r1, new RegisteredServicesCache.ServiceInfo<>(t1, null, 1));
        assertEquals(1, cache.getAllServicesSize(0));
        assertEquals(1, cache.getPersistentServicesSize(0));
    }

    public void testGetAllServicesMultiUser() {
        TestServicesCache cache = new TestServicesCache(mContext, mDataDir);
        int u0 = 0;
        int u1 = 1;
        int pid1 = 1;
        cache.addServiceForQuerying(u0, r1, new RegisteredServicesCache.ServiceInfo<>(t1, null,
                pid1));
        int u1uid = UserHandle.getUid(u1, 0);
        cache.addServiceForQuerying(u1, r2, new RegisteredServicesCache.ServiceInfo<>(t2, null,
                u1uid));
        assertEquals(u1, cache.getAllServicesSize(u0));
        assertEquals(u1, cache.getPersistentServicesSize(u0));
        assertEquals(u1, cache.getAllServicesSize(u1));
        assertEquals(u1, cache.getPersistentServicesSize(u1));
        assertEquals("No services should be available for user 3", 0, cache.getAllServicesSize(3));
        // Re-read data from disk and verify services were saved
        cache = new TestServicesCache(mContext, mDataDir);
        assertEquals(u1, cache.getPersistentServicesSize(u0));
        assertEquals(u1, cache.getPersistentServicesSize(u1));
    }

    /**
     * Mock implementation of {@link android.content.pm.RegisteredServicesCache} for testing
     */
    private static class TestServicesCache extends RegisteredServicesCache<TestServiceType> {
        static final String SERVICE_INTERFACE = "RegisteredServicesCacheTest";
        static final String SERVICE_META_DATA = "RegisteredServicesCacheTest";
        static final String ATTRIBUTES_NAME = "test";
        // Represents UID of a system image process
        static final int SYSTEM_IMAGE_UID = 20;
        private SparseArray<Map<ResolveInfo, ServiceInfo<TestServiceType>>> mServices
                = new SparseArray<>();

        public TestServicesCache(Context context, File dir) {
            super(context, SERVICE_INTERFACE, SERVICE_META_DATA, ATTRIBUTES_NAME,
                    new TestSerializer(), dir);
        }

        @Override
        public TestServiceType parseServiceAttributes(Resources res, String packageName,
                AttributeSet attrs) {
            return null;
        }

        @Override
        protected List<ResolveInfo> queryIntentServices(int userId) {
            Map<ResolveInfo, ServiceInfo<TestServiceType>> map = mServices
                    .get(userId, new HashMap<ResolveInfo, ServiceInfo<TestServiceType>>());
            return new ArrayList<>(map.keySet());
        }

        void addServiceForQuerying(int userId, ResolveInfo resolveInfo,
                ServiceInfo<TestServiceType> serviceInfo) {
            Map<ResolveInfo, ServiceInfo<TestServiceType>> map = mServices.get(userId);
            if (map == null) {
                map = new HashMap<>();
                mServices.put(userId, map);
            }
            map.put(resolveInfo, serviceInfo);
        }

        void clearServicesForQuerying() {
            mServices.clear();
        }

        int getPersistentServicesSize(int user) {
            return getPersistentServices(user).size();
        }

        int getAllServicesSize(int user) {
            return getAllServices(user).size();
        }

        @Override
        protected boolean inSystemImage(int callerUid) {
            return callerUid == SYSTEM_IMAGE_UID;
        }

        @Override
        protected ServiceInfo<TestServiceType> parseServiceInfo(
                ResolveInfo resolveInfo) throws XmlPullParserException, IOException {
            int size = mServices.size();
            for (int i = 0; i < size; i++) {
                Map<ResolveInfo, ServiceInfo<TestServiceType>> map = mServices.valueAt(i);
                ServiceInfo<TestServiceType> serviceInfo = map.get(resolveInfo);
                if (serviceInfo != null) {
                    return serviceInfo;
                }
            }
            throw new IllegalArgumentException("Unexpected service " + resolveInfo);
        }
    }

    static class TestSerializer implements XmlSerializerAndParser<TestServiceType> {

        public void writeAsXml(TestServiceType item, XmlSerializer out) throws IOException {
            out.attribute(null, "type", item.type);
            out.attribute(null, "value", item.value);
        }

        public TestServiceType createFromXml(XmlPullParser parser)
                throws IOException, XmlPullParserException {
            final String type = parser.getAttributeValue(null, "type");
            final String value = parser.getAttributeValue(null, "value");
            return new TestServiceType(type, value);
        }
    }

    static class TestServiceType implements Parcelable {
        final String type;
        final String value;

        public TestServiceType(String type, String value) {
            this.type = type;
            this.value = value;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            TestServiceType that = (TestServiceType) o;

            return type.equals(that.type) && value.equals(that.value);
        }

        @Override
        public int hashCode() {
            return 31 * type.hashCode() + value.hashCode();
        }

        @Override
        public String toString() {
            return "TestServiceType{" +
                    "type='" + type + '\'' +
                    ", value='" + value + '\'' +
                    '}';
        }

        public int describeContents() {
            return 0;
        }

        public void writeToParcel(Parcel dest, int flags) {
            dest.writeString(type);
            dest.writeString(value);
        }

        public TestServiceType(Parcel source) {
            this(source.readString(), source.readString());
        }

        public static final Creator<TestServiceType> CREATOR = new Creator<TestServiceType>() {
            public TestServiceType createFromParcel(Parcel source) {
                return new TestServiceType(source);
            }

            public TestServiceType[] newArray(int size) {
                return new TestServiceType[size];
            }
        };
    }
}