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