RegisteredServicesCache.java revision 4428e17c5e05c0dad76da8f1c28ccba62a66cd91
1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.content.pm;
18
19import android.content.Context;
20import android.content.BroadcastReceiver;
21import android.content.Intent;
22import android.content.IntentFilter;
23import android.content.ComponentName;
24import android.content.pm.PackageManager.NameNotFoundException;
25import android.content.res.Resources;
26import android.content.res.XmlResourceParser;
27import android.os.Environment;
28import android.os.Handler;
29import android.util.AtomicFile;
30import android.util.Log;
31import android.util.AttributeSet;
32import android.util.Xml;
33
34import java.util.Map;
35import java.util.Collection;
36import java.util.Collections;
37import java.util.HashMap;
38import java.util.List;
39import java.util.ArrayList;
40import java.util.concurrent.atomic.AtomicReference;
41import java.io.File;
42import java.io.FileOutputStream;
43import java.io.FileDescriptor;
44import java.io.PrintWriter;
45import java.io.IOException;
46import java.io.FileInputStream;
47
48import com.android.internal.util.FastXmlSerializer;
49
50import com.google.android.collect.Maps;
51import com.google.android.collect.Lists;
52
53import org.xmlpull.v1.XmlPullParserException;
54import org.xmlpull.v1.XmlPullParser;
55import org.xmlpull.v1.XmlSerializer;
56
57/**
58 * A cache of registered services. This cache
59 * is built by interrogating the {@link PackageManager} and is updated as packages are added,
60 * removed and changed. The services are referred to by type V and
61 * are made available via the {@link #getServiceInfo} method.
62 * @hide
63 */
64public abstract class RegisteredServicesCache<V> {
65    private static final String TAG = "PackageManager";
66
67    public final Context mContext;
68    private final String mInterfaceName;
69    private final String mMetaDataName;
70    private final String mAttributesName;
71    private final XmlSerializerAndParser<V> mSerializerAndParser;
72    private final AtomicReference<BroadcastReceiver> mReceiver;
73
74    private final Object mServicesLock = new Object();
75    // synchronized on mServicesLock
76    private HashMap<V, Integer> mPersistentServices;
77    // synchronized on mServicesLock
78    private Map<V, ServiceInfo<V>> mServices;
79    // synchronized on mServicesLock
80    private boolean mPersistentServicesFileDidNotExist;
81
82    /**
83     * This file contains the list of known services. We would like to maintain this forever
84     * so we store it as an XML file.
85     */
86    private final AtomicFile mPersistentServicesFile;
87
88    // the listener and handler are synchronized on "this" and must be updated together
89    private RegisteredServicesCacheListener<V> mListener;
90    private Handler mHandler;
91
92    public RegisteredServicesCache(Context context, String interfaceName, String metaDataName,
93            String attributeName, XmlSerializerAndParser<V> serializerAndParser) {
94        mContext = context;
95        mInterfaceName = interfaceName;
96        mMetaDataName = metaDataName;
97        mAttributesName = attributeName;
98        mSerializerAndParser = serializerAndParser;
99
100        File dataDir = Environment.getDataDirectory();
101        File systemDir = new File(dataDir, "system");
102        File syncDir = new File(systemDir, "registered_services");
103        mPersistentServicesFile = new AtomicFile(new File(syncDir, interfaceName + ".xml"));
104
105        generateServicesMap();
106
107        final BroadcastReceiver receiver = new BroadcastReceiver() {
108            @Override
109            public void onReceive(Context context1, Intent intent) {
110                generateServicesMap();
111            }
112        };
113        mReceiver = new AtomicReference<BroadcastReceiver>(receiver);
114        IntentFilter intentFilter = new IntentFilter();
115        intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
116        intentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
117        intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
118        intentFilter.addDataScheme("package");
119        mContext.registerReceiver(receiver, intentFilter);
120        // Register for events related to sdcard installation.
121        IntentFilter sdFilter = new IntentFilter();
122        sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
123        sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
124        mContext.registerReceiver(receiver, sdFilter);
125    }
126
127    public void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
128        Map<V, ServiceInfo<V>> services;
129        synchronized (mServicesLock) {
130            services = mServices;
131        }
132        fout.println("RegisteredServicesCache: " + services.size() + " services");
133        for (ServiceInfo info : services.values()) {
134            fout.println("  " + info);
135        }
136    }
137
138    public RegisteredServicesCacheListener<V> getListener() {
139        synchronized (this) {
140            return mListener;
141        }
142    }
143
144    public void setListener(RegisteredServicesCacheListener<V> listener, Handler handler) {
145        if (handler == null) {
146            handler = new Handler(mContext.getMainLooper());
147        }
148        synchronized (this) {
149            mHandler = handler;
150            mListener = listener;
151        }
152    }
153
154    private void notifyListener(final V type, final boolean removed) {
155        if (Log.isLoggable(TAG, Log.VERBOSE)) {
156            Log.d(TAG, "notifyListener: " + type + " is " + (removed ? "removed" : "added"));
157        }
158        RegisteredServicesCacheListener<V> listener;
159        Handler handler;
160        synchronized (this) {
161            listener = mListener;
162            handler = mHandler;
163        }
164        if (listener == null) {
165            return;
166        }
167
168        final RegisteredServicesCacheListener<V> listener2 = listener;
169        handler.post(new Runnable() {
170            public void run() {
171                listener2.onServiceChanged(type, removed);
172            }
173        });
174    }
175
176    /**
177     * Value type that describes a Service. The information within can be used
178     * to bind to the service.
179     */
180    public static class ServiceInfo<V> {
181        public final V type;
182        public final ComponentName componentName;
183        public final int uid;
184
185        /** @hide */
186        public ServiceInfo(V type, ComponentName componentName, int uid) {
187            this.type = type;
188            this.componentName = componentName;
189            this.uid = uid;
190        }
191
192        @Override
193        public String toString() {
194            return "ServiceInfo: " + type + ", " + componentName + ", uid " + uid;
195        }
196    }
197
198    /**
199     * Accessor for the registered authenticators.
200     * @param type the account type of the authenticator
201     * @return the AuthenticatorInfo that matches the account type or null if none is present
202     */
203    public ServiceInfo<V> getServiceInfo(V type) {
204        synchronized (mServicesLock) {
205            return mServices.get(type);
206        }
207    }
208
209    /**
210     * @return a collection of {@link RegisteredServicesCache.ServiceInfo} objects for all
211     * registered authenticators.
212     */
213    public Collection<ServiceInfo<V>> getAllServices() {
214        synchronized (mServicesLock) {
215            return Collections.unmodifiableCollection(mServices.values());
216        }
217    }
218
219    /**
220     * Stops the monitoring of package additions, removals and changes.
221     */
222    public void close() {
223        final BroadcastReceiver receiver = mReceiver.getAndSet(null);
224        if (receiver != null) {
225            mContext.unregisterReceiver(receiver);
226        }
227    }
228
229    @Override
230    protected void finalize() throws Throwable {
231        if (mReceiver.get() != null) {
232            Log.e(TAG, "RegisteredServicesCache finalized without being closed");
233        }
234        close();
235        super.finalize();
236    }
237
238    private boolean inSystemImage(int callerUid) {
239        String[] packages = mContext.getPackageManager().getPackagesForUid(callerUid);
240        for (String name : packages) {
241            try {
242                PackageInfo packageInfo =
243                        mContext.getPackageManager().getPackageInfo(name, 0 /* flags */);
244                if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
245                    return true;
246                }
247            } catch (PackageManager.NameNotFoundException e) {
248                return false;
249            }
250        }
251        return false;
252    }
253
254    public void generateServicesMap() {
255        PackageManager pm = mContext.getPackageManager();
256        ArrayList<ServiceInfo<V>> serviceInfos = new ArrayList<ServiceInfo<V>>();
257        List<ResolveInfo> resolveInfos = pm.queryIntentServices(new Intent(mInterfaceName),
258                PackageManager.GET_META_DATA);
259        for (ResolveInfo resolveInfo : resolveInfos) {
260            try {
261                ServiceInfo<V> info = parseServiceInfo(resolveInfo);
262                if (info == null) {
263                    Log.w(TAG, "Unable to load service info " + resolveInfo.toString());
264                    continue;
265                }
266                serviceInfos.add(info);
267            } catch (XmlPullParserException e) {
268                Log.w(TAG, "Unable to load service info " + resolveInfo.toString(), e);
269            } catch (IOException e) {
270                Log.w(TAG, "Unable to load service info " + resolveInfo.toString(), e);
271            }
272        }
273
274        synchronized (mServicesLock) {
275            if (mPersistentServices == null) {
276                readPersistentServicesLocked();
277            }
278            mServices = Maps.newHashMap();
279            StringBuilder changes = new StringBuilder();
280            for (ServiceInfo<V> info : serviceInfos) {
281                // four cases:
282                // - doesn't exist yet
283                //   - add, notify user that it was added
284                // - exists and the UID is the same
285                //   - replace, don't notify user
286                // - exists, the UID is different, and the new one is not a system package
287                //   - ignore
288                // - exists, the UID is different, and the new one is a system package
289                //   - add, notify user that it was added
290                Integer previousUid = mPersistentServices.get(info.type);
291                if (previousUid == null) {
292                    changes.append("  New service added: ").append(info).append("\n");
293                    mServices.put(info.type, info);
294                    mPersistentServices.put(info.type, info.uid);
295                    if (!mPersistentServicesFileDidNotExist) {
296                        notifyListener(info.type, false /* removed */);
297                    }
298                } else if (previousUid == info.uid) {
299                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
300                        changes.append("  Existing service (nop): ").append(info).append("\n");
301                    }
302                    mServices.put(info.type, info);
303                } else if (inSystemImage(info.uid)
304                        || !containsTypeAndUid(serviceInfos, info.type, previousUid)) {
305                    if (inSystemImage(info.uid)) {
306                        changes.append("  System service replacing existing: ").append(info)
307                                .append("\n");
308                    } else {
309                        changes.append("  Existing service replacing a removed service: ")
310                                .append(info).append("\n");
311                    }
312                    mServices.put(info.type, info);
313                    mPersistentServices.put(info.type, info.uid);
314                    notifyListener(info.type, false /* removed */);
315                } else {
316                    // ignore
317                    changes.append("  Existing service with new uid ignored: ").append(info)
318                            .append("\n");
319                }
320            }
321
322            ArrayList<V> toBeRemoved = Lists.newArrayList();
323            for (V v1 : mPersistentServices.keySet()) {
324                if (!containsType(serviceInfos, v1)) {
325                    toBeRemoved.add(v1);
326                }
327            }
328            for (V v1 : toBeRemoved) {
329                mPersistentServices.remove(v1);
330                changes.append("  Service removed: ").append(v1).append("\n");
331                notifyListener(v1, true /* removed */);
332            }
333            if (changes.length() > 0) {
334                if (Log.isLoggable(TAG, Log.VERBOSE)) {
335                    Log.d(TAG, "generateServicesMap(" + mInterfaceName + "): " +
336                            serviceInfos.size() + " services:\n" + changes);
337                }
338                writePersistentServicesLocked();
339            } else {
340                if (Log.isLoggable(TAG, Log.VERBOSE)) {
341                    Log.d(TAG, "generateServicesMap(" + mInterfaceName + "): " +
342                            serviceInfos.size() + " services unchanged");
343                }
344            }
345            mPersistentServicesFileDidNotExist = false;
346        }
347    }
348
349    private boolean containsType(ArrayList<ServiceInfo<V>> serviceInfos, V type) {
350        for (int i = 0, N = serviceInfos.size(); i < N; i++) {
351            if (serviceInfos.get(i).type.equals(type)) {
352                return true;
353            }
354        }
355
356        return false;
357    }
358
359    private boolean containsTypeAndUid(ArrayList<ServiceInfo<V>> serviceInfos, V type, int uid) {
360        for (int i = 0, N = serviceInfos.size(); i < N; i++) {
361            final ServiceInfo<V> serviceInfo = serviceInfos.get(i);
362            if (serviceInfo.type.equals(type) && serviceInfo.uid == uid) {
363                return true;
364            }
365        }
366
367        return false;
368    }
369
370    private ServiceInfo<V> parseServiceInfo(ResolveInfo service)
371            throws XmlPullParserException, IOException {
372        android.content.pm.ServiceInfo si = service.serviceInfo;
373        ComponentName componentName = new ComponentName(si.packageName, si.name);
374
375        PackageManager pm = mContext.getPackageManager();
376
377        XmlResourceParser parser = null;
378        try {
379            parser = si.loadXmlMetaData(pm, mMetaDataName);
380            if (parser == null) {
381                throw new XmlPullParserException("No " + mMetaDataName + " meta-data");
382            }
383
384            AttributeSet attrs = Xml.asAttributeSet(parser);
385
386            int type;
387            while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
388                    && type != XmlPullParser.START_TAG) {
389            }
390
391            String nodeName = parser.getName();
392            if (!mAttributesName.equals(nodeName)) {
393                throw new XmlPullParserException(
394                        "Meta-data does not start with " + mAttributesName +  " tag");
395            }
396
397            V v = parseServiceAttributes(pm.getResourcesForApplication(si.applicationInfo),
398                    si.packageName, attrs);
399            if (v == null) {
400                return null;
401            }
402            final android.content.pm.ServiceInfo serviceInfo = service.serviceInfo;
403            final ApplicationInfo applicationInfo = serviceInfo.applicationInfo;
404            final int uid = applicationInfo.uid;
405            return new ServiceInfo<V>(v, componentName, uid);
406        } catch (NameNotFoundException e) {
407            throw new XmlPullParserException(
408                    "Unable to load resources for pacakge " + si.packageName);
409        } finally {
410            if (parser != null) parser.close();
411        }
412    }
413
414    /**
415     * Read all sync status back in to the initial engine state.
416     */
417    private void readPersistentServicesLocked() {
418        mPersistentServices = Maps.newHashMap();
419        if (mSerializerAndParser == null) {
420            return;
421        }
422        FileInputStream fis = null;
423        try {
424            mPersistentServicesFileDidNotExist = !mPersistentServicesFile.getBaseFile().exists();
425            if (mPersistentServicesFileDidNotExist) {
426                return;
427            }
428            fis = mPersistentServicesFile.openRead();
429            XmlPullParser parser = Xml.newPullParser();
430            parser.setInput(fis, null);
431            int eventType = parser.getEventType();
432            while (eventType != XmlPullParser.START_TAG) {
433                eventType = parser.next();
434            }
435            String tagName = parser.getName();
436            if ("services".equals(tagName)) {
437                eventType = parser.next();
438                do {
439                    if (eventType == XmlPullParser.START_TAG && parser.getDepth() == 2) {
440                        tagName = parser.getName();
441                        if ("service".equals(tagName)) {
442                            V service = mSerializerAndParser.createFromXml(parser);
443                            if (service == null) {
444                                break;
445                            }
446                            String uidString = parser.getAttributeValue(null, "uid");
447                            int uid = Integer.parseInt(uidString);
448                            mPersistentServices.put(service, uid);
449                        }
450                    }
451                    eventType = parser.next();
452                } while (eventType != XmlPullParser.END_DOCUMENT);
453            }
454        } catch (Exception e) {
455            Log.w(TAG, "Error reading persistent services, starting from scratch", e);
456        } finally {
457            if (fis != null) {
458                try {
459                    fis.close();
460                } catch (java.io.IOException e1) {
461                }
462            }
463        }
464    }
465
466    /**
467     * Write all sync status to the sync status file.
468     */
469    private void writePersistentServicesLocked() {
470        if (mSerializerAndParser == null) {
471            return;
472        }
473        FileOutputStream fos = null;
474        try {
475            fos = mPersistentServicesFile.startWrite();
476            XmlSerializer out = new FastXmlSerializer();
477            out.setOutput(fos, "utf-8");
478            out.startDocument(null, true);
479            out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
480            out.startTag(null, "services");
481            for (Map.Entry<V, Integer> service : mPersistentServices.entrySet()) {
482                out.startTag(null, "service");
483                out.attribute(null, "uid", Integer.toString(service.getValue()));
484                mSerializerAndParser.writeAsXml(service.getKey(), out);
485                out.endTag(null, "service");
486            }
487            out.endTag(null, "services");
488            out.endDocument();
489            mPersistentServicesFile.finishWrite(fos);
490        } catch (java.io.IOException e1) {
491            Log.w(TAG, "Error writing accounts", e1);
492            if (fos != null) {
493                mPersistentServicesFile.failWrite(fos);
494            }
495        }
496    }
497
498    public abstract V parseServiceAttributes(Resources res,
499            String packageName, AttributeSet attrs);
500}
501