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