ExternalAccountType.java revision 86ccb6ce2ff6078b06add93aab489951ef83bc72
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 com.android.contacts.model;
18
19import com.google.common.annotations.VisibleForTesting;
20
21import org.xmlpull.v1.XmlPullParser;
22import org.xmlpull.v1.XmlPullParserException;
23
24import android.content.Context;
25import android.content.pm.PackageInfo;
26import android.content.pm.PackageManager;
27import android.content.pm.PackageManager.NameNotFoundException;
28import android.content.pm.ServiceInfo;
29import android.content.res.Resources;
30import android.content.res.TypedArray;
31import android.content.res.XmlResourceParser;
32import android.text.TextUtils;
33import android.util.AttributeSet;
34import android.util.Log;
35import android.util.Xml;
36
37import java.io.IOException;
38import java.util.ArrayList;
39import java.util.List;
40
41/**
42 * A general contacts account type descriptor.
43 */
44public class ExternalAccountType extends BaseAccountType {
45    private static final String TAG = "ExternalAccountType";
46
47    private static final String METADATA_CONTACTS = "android.provider.CONTACTS_STRUCTURE";
48
49    private static final String TAG_CONTACTS_SOURCE_LEGACY = "ContactsSource";
50    private static final String TAG_CONTACTS_ACCOUNT_TYPE = "ContactsAccountType";
51    private static final String TAG_CONTACTS_DATA_KIND = "ContactsDataKind";
52    private static final String TAG_EDIT_SCHEMA = "EditSchema";
53
54    private static final String ATTR_EDIT_CONTACT_ACTIVITY = "editContactActivity";
55    private static final String ATTR_CREATE_CONTACT_ACTIVITY = "createContactActivity";
56    private static final String ATTR_INVITE_CONTACT_ACTIVITY = "inviteContactActivity";
57    private static final String ATTR_INVITE_CONTACT_ACTION_LABEL = "inviteContactActionLabel";
58    private static final String ATTR_VIEW_CONTACT_NOTIFY_SERVICE = "viewContactNotifyService";
59    private static final String ATTR_VIEW_GROUP_ACTIVITY = "viewGroupActivity";
60    private static final String ATTR_VIEW_GROUP_ACTION_LABEL = "viewGroupActionLabel";
61    private static final String ATTR_VIEW_STREAM_ITEM_ACTIVITY = "viewStreamItemActivity";
62    private static final String ATTR_VIEW_STREAM_ITEM_PHOTO_ACTIVITY =
63            "viewStreamItemPhotoActivity";
64    private static final String ATTR_DATA_SET = "dataSet";
65    private static final String ATTR_EXTENSION_PACKAGE_NAMES = "extensionPackageNames";
66
67    // The following attributes should only be set in non-sync-adapter account types.  They allow
68    // for the account type and resource IDs to be specified without an associated authenticator.
69    private static final String ATTR_ACCOUNT_TYPE = "accountType";
70    private static final String ATTR_ACCOUNT_LABEL = "accountTypeLabel";
71    private static final String ATTR_ACCOUNT_ICON = "accountTypeIcon";
72
73    private final boolean mIsExtension;
74
75    private String mEditContactActivityClassName;
76    private String mCreateContactActivityClassName;
77    private String mInviteContactActivity;
78    private String mInviteActionLabelAttribute;
79    private int mInviteActionLabelResId;
80    private String mViewContactNotifyService;
81    private String mViewGroupActivity;
82    private String mViewGroupLabelAttribute;
83    private int mViewGroupLabelResId;
84    private String mViewStreamItemActivity;
85    private String mViewStreamItemPhotoActivity;
86    private List<String> mExtensionPackageNames;
87    private String mAccountTypeLabelAttribute;
88    private String mAccountTypeIconAttribute;
89    private boolean mHasContactsMetadata;
90    private boolean mHasEditSchema;
91
92    public ExternalAccountType(Context context, String resPackageName, boolean isExtension) {
93        this(context, resPackageName, isExtension, null);
94    }
95
96    /**
97     * Constructor used for testing to initialize with any arbitrary XML.
98     *
99     * @param injectedMetadata If non-null, it'll be used to initialize the type.  Only set by
100     *     tests.  If null, the metadata is loaded from the specified package.
101     */
102    ExternalAccountType(Context context, String resPackageName, boolean isExtension,
103            XmlResourceParser injectedMetadata) {
104        this.mIsExtension = isExtension;
105        this.resPackageName = resPackageName;
106        this.summaryResPackageName = resPackageName;
107
108        final PackageManager pm = context.getPackageManager();
109        final XmlResourceParser parser;
110        if (injectedMetadata == null) {
111            try {
112                parser = loadContactsXml(context, resPackageName);
113            } catch (NameNotFoundException e1) {
114                // If the package name is not found, we can't initialize this account type.
115                return;
116            }
117        } else {
118            parser = injectedMetadata;
119        }
120        try {
121            if (parser != null) {
122                inflate(context, parser);
123            }
124
125            if (!mHasEditSchema) {
126                // Bring in name and photo from fallback source, which are non-optional
127                addDataKindStructuredName(context);
128                addDataKindDisplayName(context);
129                addDataKindPhoneticName(context);
130                addDataKindPhoto(context);
131            }
132        } catch (DefinitionException e) {
133            String message = "Problem reading XML";
134            if (parser != null) {
135                message = message + " in line " + parser.getLineNumber();
136            }
137            Log.e(TAG, message, e);
138            return;
139        } finally {
140            if (parser != null) {
141                parser.close();
142            }
143        }
144
145        mExtensionPackageNames = new ArrayList<String>();
146        mInviteActionLabelResId = resolveExternalResId(context, mInviteActionLabelAttribute,
147                summaryResPackageName, ATTR_INVITE_CONTACT_ACTION_LABEL);
148        mViewGroupLabelResId = resolveExternalResId(context, mViewGroupLabelAttribute,
149                summaryResPackageName, ATTR_VIEW_GROUP_ACTION_LABEL);
150        titleRes = resolveExternalResId(context, mAccountTypeLabelAttribute,
151                this.resPackageName, ATTR_ACCOUNT_LABEL);
152        iconRes = resolveExternalResId(context, mAccountTypeIconAttribute,
153                this.resPackageName, ATTR_ACCOUNT_ICON);
154
155        // If we reach this point, the account type has been successfully initialized.
156        mIsInitialized = true;
157    }
158
159    /**
160     * Returns the CONTACTS_STRUCTURE metadata (aka "contacts.xml") in the given apk package.
161     *
162     * Unfortunately, there's no public way to determine which service defines a sync service for
163     * which account type, so this method looks through all services in the package, and just
164     * returns the first CONTACTS_STRUCTURE metadata defined in any of them.
165     *
166     * Returns {@code null} if the package has no CONTACTS_STRUCTURE metadata.  In this case
167     * the account type *will* be initialized with minimal configuration.
168     *
169     * On the other hand, if the package is not found, it throws a {@link NameNotFoundException},
170     * in which case the account type will *not* be initialized.
171     */
172    private XmlResourceParser loadContactsXml(Context context, String resPackageName)
173            throws NameNotFoundException {
174        final PackageManager pm = context.getPackageManager();
175        PackageInfo packageInfo = pm.getPackageInfo(resPackageName,
176                PackageManager.GET_SERVICES|PackageManager.GET_META_DATA);
177        for (ServiceInfo serviceInfo : packageInfo.services) {
178            final XmlResourceParser parser = serviceInfo.loadXmlMetaData(pm,
179                    METADATA_CONTACTS);
180            if (parser != null) {
181                return parser;
182            }
183        }
184        // Package was found, but that doesn't contain the CONTACTS_STRUCTURE metadata.
185        return null;
186    }
187
188    @Override
189    public boolean isEmbedded() {
190        return false;
191    }
192
193    @Override
194    public boolean isExtension() {
195        return mIsExtension;
196    }
197
198    @Override
199    public boolean areContactsWritable() {
200        return mHasEditSchema;
201    }
202
203    /**
204     * Whether this account type has the android.provider.CONTACTS_STRUCTURE metadata xml.
205     */
206    public boolean hasContactsMetadata() {
207        return mHasContactsMetadata;
208    }
209
210    @Override
211    public String getEditContactActivityClassName() {
212        return mEditContactActivityClassName;
213    }
214
215    @Override
216    public String getCreateContactActivityClassName() {
217        return mCreateContactActivityClassName;
218    }
219
220    @Override
221    public String getInviteContactActivityClassName() {
222        return mInviteContactActivity;
223    }
224
225    @Override
226    protected int getInviteContactActionResId() {
227        return mInviteActionLabelResId;
228    }
229
230    @Override
231    public String getViewContactNotifyServiceClassName() {
232        return mViewContactNotifyService;
233    }
234
235    @Override
236    public String getViewGroupActivity() {
237        return mViewGroupActivity;
238    }
239
240    @Override
241    protected int getViewGroupLabelResId() {
242        return mViewGroupLabelResId;
243    }
244
245    @Override
246    public String getViewStreamItemActivity() {
247        return mViewStreamItemActivity;
248    }
249
250    @Override
251    public String getViewStreamItemPhotoActivity() {
252        return mViewStreamItemPhotoActivity;
253    }
254
255    @Override
256    public List<String> getExtensionPackageNames() {
257        return mExtensionPackageNames;
258    }
259
260    /**
261     * Inflate this {@link AccountType} from the given parser. This may only
262     * load details matching the publicly-defined schema.
263     */
264    protected void inflate(Context context, XmlPullParser parser) throws DefinitionException {
265        final AttributeSet attrs = Xml.asAttributeSet(parser);
266
267        try {
268            int type;
269            while ((type = parser.next()) != XmlPullParser.START_TAG
270                    && type != XmlPullParser.END_DOCUMENT) {
271                // Drain comments and whitespace
272            }
273
274            if (type != XmlPullParser.START_TAG) {
275                throw new IllegalStateException("No start tag found");
276            }
277
278            String rootTag = parser.getName();
279            if (!TAG_CONTACTS_ACCOUNT_TYPE.equals(rootTag) &&
280                    !TAG_CONTACTS_SOURCE_LEGACY.equals(rootTag)) {
281                throw new IllegalStateException("Top level element must be "
282                        + TAG_CONTACTS_ACCOUNT_TYPE + ", not " + rootTag);
283            }
284
285            mHasContactsMetadata = true;
286
287            int attributeCount = parser.getAttributeCount();
288            for (int i = 0; i < attributeCount; i++) {
289                String attr = parser.getAttributeName(i);
290                String value = parser.getAttributeValue(i);
291                if (Log.isLoggable(TAG, Log.DEBUG)) {
292                    Log.d(TAG, attr + "=" + value);
293                }
294                if (ATTR_EDIT_CONTACT_ACTIVITY.equals(attr)) {
295                    mEditContactActivityClassName = value;
296                } else if (ATTR_CREATE_CONTACT_ACTIVITY.equals(attr)) {
297                    mCreateContactActivityClassName = value;
298                } else if (ATTR_INVITE_CONTACT_ACTIVITY.equals(attr)) {
299                    mInviteContactActivity = value;
300                } else if (ATTR_INVITE_CONTACT_ACTION_LABEL.equals(attr)) {
301                    mInviteActionLabelAttribute = value;
302                } else if (ATTR_VIEW_CONTACT_NOTIFY_SERVICE.equals(attr)) {
303                    mViewContactNotifyService = value;
304                } else if (ATTR_VIEW_GROUP_ACTIVITY.equals(attr)) {
305                    mViewGroupActivity = value;
306                } else if (ATTR_VIEW_GROUP_ACTION_LABEL.equals(attr)) {
307                    mViewGroupLabelAttribute = value;
308                } else if (ATTR_VIEW_STREAM_ITEM_ACTIVITY.equals(attr)) {
309                    mViewStreamItemActivity = value;
310                } else if (ATTR_VIEW_STREAM_ITEM_PHOTO_ACTIVITY.equals(attr)) {
311                    mViewStreamItemPhotoActivity = value;
312                } else if (ATTR_DATA_SET.equals(attr)) {
313                    dataSet = value;
314                } else if (ATTR_EXTENSION_PACKAGE_NAMES.equals(attr)) {
315                    mExtensionPackageNames.add(value);
316                } else if (ATTR_ACCOUNT_TYPE.equals(attr)) {
317                    accountType = value;
318                } else if (ATTR_ACCOUNT_LABEL.equals(attr)) {
319                    mAccountTypeLabelAttribute = value;
320                } else if (ATTR_ACCOUNT_ICON.equals(attr)) {
321                    mAccountTypeIconAttribute = value;
322                } else {
323                    Log.e(TAG, "Unsupported attribute " + attr);
324                }
325            }
326
327            // Parse all children kinds
328            final int depth = parser.getDepth();
329            while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
330                    && type != XmlPullParser.END_DOCUMENT) {
331
332                String tag = parser.getName();
333                if (TAG_EDIT_SCHEMA.equals(tag)) {
334                    mHasEditSchema = true;
335                    parseEditSchema(context, parser, attrs);
336                } else if (TAG_CONTACTS_DATA_KIND.equals(tag)) {
337                    final TypedArray a = context.obtainStyledAttributes(attrs,
338                            android.R.styleable.ContactsDataKind);
339                    final DataKind kind = new DataKind();
340
341                    kind.mimeType = a
342                            .getString(com.android.internal.R.styleable.ContactsDataKind_mimeType);
343
344                    final String summaryColumn = a.getString(
345                            com.android.internal.R.styleable.ContactsDataKind_summaryColumn);
346                    if (summaryColumn != null) {
347                        // Inflate a specific column as summary when requested
348                        kind.actionHeader = new SimpleInflater(summaryColumn);
349                    }
350
351                    final String detailColumn = a.getString(
352                            com.android.internal.R.styleable.ContactsDataKind_detailColumn);
353                    final boolean detailSocialSummary = a.getBoolean(
354                            com.android.internal.R.styleable.ContactsDataKind_detailSocialSummary,
355                            false);
356
357                    if (detailSocialSummary) {
358                        // Inflate social summary when requested
359                        kind.actionBodySocial = true;
360                    }
361
362                    if (detailColumn != null) {
363                        // Inflate specific column as summary
364                        kind.actionBody = new SimpleInflater(detailColumn);
365                    }
366
367                    a.recycle();
368
369                    addKind(kind);
370                }
371            }
372        } catch (XmlPullParserException e) {
373            throw new DefinitionException("Problem reading XML", e);
374        } catch (IOException e) {
375            throw new DefinitionException("Problem reading XML", e);
376        }
377    }
378
379    /**
380     * Takes a string in the "@xxx/yyy" format and return the resource ID for the resource in
381     * the resource package.
382     *
383     * If the argument is in the invalid format or isn't a resource name, it returns -1.
384     *
385     * @param context context
386     * @param resourceName Resource name in the "@xxx/yyy" format, e.g. "@string/invite_lavbel"
387     * @param packageName name of the package containing the resource.
388     * @param xmlAttributeName attribute name which the resource came from.  Used for logging.
389     */
390    @VisibleForTesting
391    static int resolveExternalResId(Context context, String resourceName,
392            String packageName, String xmlAttributeName) {
393        if (TextUtils.isEmpty(resourceName)) {
394            return -1; // Empty text is okay.
395        }
396        if (resourceName.charAt(0) != '@') {
397            Log.e(TAG, xmlAttributeName + " must be a resource name beginnig with '@'");
398            return -1;
399        }
400        final String name = resourceName.substring(1);
401        final Resources res;
402        try {
403             res = context.getPackageManager().getResourcesForApplication(packageName);
404        } catch (NameNotFoundException e) {
405            Log.e(TAG, "Unable to load package " + packageName);
406            return -1;
407        }
408        final int resId = res.getIdentifier(name, null, packageName);
409        if (resId == 0) {
410            Log.e(TAG, "Unable to load " + resourceName + " from package " + packageName);
411            return -1;
412        }
413        return resId;
414    }
415}
416