ExternalAccountType.java revision 3ef27fb18a2fe075c43131b653cd2e6306e187e2
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.Intent;
26import android.content.pm.PackageInfo;
27import android.content.pm.PackageManager;
28import android.content.pm.PackageManager.NameNotFoundException;
29import android.content.pm.ResolveInfo;
30import android.content.pm.ServiceInfo;
31import android.content.res.Resources;
32import android.content.res.TypedArray;
33import android.content.res.XmlResourceParser;
34import android.text.TextUtils;
35import android.util.AttributeSet;
36import android.util.Log;
37import android.util.Xml;
38
39import java.io.IOException;
40import java.util.ArrayList;
41import java.util.List;
42
43/**
44 * A general contacts account type descriptor.
45 */
46public class ExternalAccountType extends BaseAccountType {
47    private static final String TAG = "ExternalAccountType";
48
49    private static final String ACTION_SYNC_ADAPTER = "android.content.SyncAdapter";
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
56    private static final String ATTR_EDIT_CONTACT_ACTIVITY = "editContactActivity";
57    private static final String ATTR_CREATE_CONTACT_ACTIVITY = "createContactActivity";
58    private static final String ATTR_INVITE_CONTACT_ACTIVITY = "inviteContactActivity";
59    private static final String ATTR_INVITE_CONTACT_ACTION_LABEL = "inviteContactActionLabel";
60    private static final String ATTR_VIEW_CONTACT_NOTIFY_SERVICE = "viewContactNotifyService";
61    private static final String ATTR_DATA_SET = "dataSet";
62    private static final String ATTR_EXTENSION_PACKAGE_NAMES = "extensionPackageNames";
63
64    // The following attributes should only be set in non-sync-adapter account types.  They allow
65    // for the account type and resource IDs to be specified without an associated authenticator.
66    private static final String ATTR_ACCOUNT_TYPE = "accountType";
67    private static final String ATTR_READ_ONLY = "readOnly";
68    private static final String ATTR_ACCOUNT_LABEL = "accountTypeLabel";
69    private static final String ATTR_ACCOUNT_ICON = "accountTypeIcon";
70
71    private String mEditContactActivityClassName;
72    private String mCreateContactActivityClassName;
73    private String mInviteContactActivity;
74    private String mInviteActionLabelAttribute;
75    private String mViewContactNotifyService;
76    private List<String> mExtensionPackageNames;
77    private int mInviteActionLabelResId;
78    private String mAccountTypeLabelAttribute;
79    private String mAccountTypeIconAttribute;
80    private boolean mInitSuccessful;
81
82    public ExternalAccountType(Context context, String resPackageName) {
83        this.resPackageName = resPackageName;
84        this.summaryResPackageName = resPackageName;
85
86        // Handle unknown sources by searching their package
87        final PackageManager pm = context.getPackageManager();
88        try {
89            PackageInfo packageInfo = pm.getPackageInfo(resPackageName,
90                    PackageManager.GET_SERVICES|PackageManager.GET_META_DATA);
91            for (ServiceInfo serviceInfo : packageInfo.services) {
92                final XmlResourceParser parser = serviceInfo.loadXmlMetaData(pm,
93                        METADATA_CONTACTS);
94                if (parser == null) continue;
95                inflate(context, parser);
96            }
97        } catch (NameNotFoundException nnfe) {
98            // If the package name is not found, we can't initialize this account type.
99            return;
100        }
101
102        mExtensionPackageNames = new ArrayList<String>();
103        mInviteActionLabelResId = resolveExternalResId(context, mInviteActionLabelAttribute,
104                summaryResPackageName, ATTR_INVITE_CONTACT_ACTION_LABEL);
105        titleRes = resolveExternalResId(context, mAccountTypeLabelAttribute,
106                this.resPackageName, ATTR_ACCOUNT_LABEL);
107        iconRes = resolveExternalResId(context, mAccountTypeIconAttribute,
108                this.resPackageName, ATTR_ACCOUNT_ICON);
109
110        // Bring in name and photo from fallback source, which are non-optional
111        addDataKindStructuredName(context);
112        addDataKindDisplayName(context);
113        addDataKindPhoneticName(context);
114        addDataKindPhoto(context);
115
116        // If we reach this point, the account type has been successfully initialized.
117        mInitSuccessful = true;
118    }
119
120    @Override
121    public boolean isExternal() {
122        return true;
123    }
124
125    /**
126     * Whether this account type was able to be fully initialized.  This may be false if
127     * (for example) the package name associated with the account type could not be found.
128     */
129    public boolean isInitialized() {
130        return mInitSuccessful;
131    }
132
133    @Override
134    public String getEditContactActivityClassName() {
135        return mEditContactActivityClassName;
136    }
137
138    @Override
139    public String getCreateContactActivityClassName() {
140        return mCreateContactActivityClassName;
141    }
142
143    @Override
144    public String getInviteContactActivityClassName() {
145        return mInviteContactActivity;
146    }
147
148    @Override
149    protected int getInviteContactActionResId(Context context) {
150        return mInviteActionLabelResId;
151    }
152
153    @Override
154    public String getViewContactNotifyServiceClassName() {
155        return mViewContactNotifyService;
156    }
157
158    @Override
159    public List<String> getExtensionPackageNames() {
160        return mExtensionPackageNames;
161    }
162
163    /**
164     * Inflate this {@link AccountType} from the given parser. This may only
165     * load details matching the publicly-defined schema.
166     */
167    protected void inflate(Context context, XmlPullParser parser) {
168        final AttributeSet attrs = Xml.asAttributeSet(parser);
169
170        try {
171            int type;
172            while ((type = parser.next()) != XmlPullParser.START_TAG
173                    && type != XmlPullParser.END_DOCUMENT) {
174                // Drain comments and whitespace
175            }
176
177            if (type != XmlPullParser.START_TAG) {
178                throw new IllegalStateException("No start tag found");
179            }
180
181            String rootTag = parser.getName();
182            if (!TAG_CONTACTS_ACCOUNT_TYPE.equals(rootTag) &&
183                    !TAG_CONTACTS_SOURCE_LEGACY.equals(rootTag)) {
184                throw new IllegalStateException("Top level element must be "
185                        + TAG_CONTACTS_ACCOUNT_TYPE + ", not " + rootTag);
186            }
187
188            int attributeCount = parser.getAttributeCount();
189            for (int i = 0; i < attributeCount; i++) {
190                String attr = parser.getAttributeName(i);
191                String value = parser.getAttributeValue(i);
192                if (Log.isLoggable(TAG, Log.DEBUG)) {
193                    Log.d(TAG, attr + "=" + value);
194                }
195                if (ATTR_EDIT_CONTACT_ACTIVITY.equals(attr)) {
196                    mEditContactActivityClassName = value;
197                } else if (ATTR_CREATE_CONTACT_ACTIVITY.equals(attr)) {
198                    mCreateContactActivityClassName = value;
199                } else if (ATTR_INVITE_CONTACT_ACTIVITY.equals(attr)) {
200                    mInviteContactActivity = value;
201                } else if (ATTR_INVITE_CONTACT_ACTION_LABEL.equals(attr)) {
202                    mInviteActionLabelAttribute = value;
203                } else if (ATTR_VIEW_CONTACT_NOTIFY_SERVICE.equals(attr)) {
204                    mViewContactNotifyService = value;
205                } else if (ATTR_DATA_SET.equals(attr)) {
206                    dataSet = value;
207                } else if (ATTR_EXTENSION_PACKAGE_NAMES.equals(attr)) {
208                    mExtensionPackageNames.add(value);
209                } else if (ATTR_ACCOUNT_TYPE.equals(attr)) {
210                    accountType = value;
211                } else if (ATTR_READ_ONLY.equals(attr)) {
212                    readOnly = !"0".equals(value) && !"false".equals(value);
213                } else if (ATTR_ACCOUNT_LABEL.equals(attr)) {
214                    mAccountTypeLabelAttribute = value;
215                } else if (ATTR_ACCOUNT_ICON.equals(attr)) {
216                    mAccountTypeIconAttribute = value;
217                } else {
218                    Log.e(TAG, "Unsupported attribute " + attr);
219                }
220            }
221
222            // Parse all children kinds
223            final int depth = parser.getDepth();
224            while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
225                    && type != XmlPullParser.END_DOCUMENT) {
226                String tag = parser.getName();
227                if (type == XmlPullParser.END_TAG || !TAG_CONTACTS_DATA_KIND.equals(tag)) {
228                    continue;
229                }
230
231                final TypedArray a = context.obtainStyledAttributes(attrs,
232                        android.R.styleable.ContactsDataKind);
233                final DataKind kind = new DataKind();
234
235                kind.mimeType = a
236                        .getString(com.android.internal.R.styleable.ContactsDataKind_mimeType);
237                kind.iconRes = a.getResourceId(
238                        com.android.internal.R.styleable.ContactsDataKind_icon, -1);
239
240                final String summaryColumn = a
241                        .getString(com.android.internal.R.styleable.ContactsDataKind_summaryColumn);
242                if (summaryColumn != null) {
243                    // Inflate a specific column as summary when requested
244                    kind.actionHeader = new FallbackAccountType.SimpleInflater(summaryColumn);
245                }
246
247                final String detailColumn = a
248                        .getString(com.android.internal.R.styleable.ContactsDataKind_detailColumn);
249                final boolean detailSocialSummary = a.getBoolean(
250                        com.android.internal.R.styleable.ContactsDataKind_detailSocialSummary,
251                        false);
252
253                if (detailSocialSummary) {
254                    // Inflate social summary when requested
255                    kind.actionBodySocial = true;
256                }
257
258                if (detailColumn != null) {
259                    // Inflate specific column as summary
260                    kind.actionBody = new FallbackAccountType.SimpleInflater(detailColumn);
261                }
262
263                addKind(kind);
264                a.recycle();
265            }
266        } catch (XmlPullParserException e) {
267            throw new IllegalStateException("Problem reading XML", e);
268        } catch (IOException e) {
269            throw new IllegalStateException("Problem reading XML", e);
270        }
271    }
272
273    @Override
274    public int getHeaderColor(Context context) {
275        return 0xff6d86b4;
276    }
277
278    @Override
279    public int getSideBarColor(Context context) {
280        return 0xff6d86b4;
281    }
282
283    /**
284     * Takes a string in the "@xxx/yyy" format and return the resource ID for the resource in
285     * the resource package.
286     *
287     * If the argument is in the invalid format or isn't a resource name, it returns -1.
288     *
289     * @param context context
290     * @param resourceName Resource name in the "@xxx/yyy" format, e.g. "@string/invite_lavbel"
291     * @param packageName name of the package containing the resource.
292     * @param xmlAttributeName attribute name which the resource came from.  Used for logging.
293     */
294    @VisibleForTesting
295    static int resolveExternalResId(Context context, String resourceName,
296            String packageName, String xmlAttributeName) {
297        if (TextUtils.isEmpty(resourceName)) {
298            return -1; // Empty text is okay.
299        }
300        if (resourceName.charAt(0) != '@') {
301            Log.e(TAG, xmlAttributeName + " must be a resource name beginnig with '@'");
302            return -1;
303        }
304        final String name = resourceName.substring(1);
305        final Resources res;
306        try {
307             res = context.getPackageManager().getResourcesForApplication(packageName);
308        } catch (NameNotFoundException e) {
309            Log.e(TAG, "Unable to load package " + packageName);
310            return -1;
311        }
312        final int resId = res.getIdentifier(name, null, packageName);
313        if (resId == 0) {
314            Log.e(TAG, "Unable to load " + resourceName + " from package " + packageName);
315            return -1;
316        }
317        return resId;
318    }
319}
320