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 
17 package com.example.android.contactmanager;
18 
19 import android.accounts.Account;
20 import android.accounts.AccountManager;
21 import android.accounts.AuthenticatorDescription;
22 import android.accounts.OnAccountsUpdateListener;
23 import android.app.Activity;
24 import android.content.ContentProviderOperation;
25 import android.content.Context;
26 import android.content.pm.PackageManager;
27 import android.graphics.drawable.Drawable;
28 import android.os.Bundle;
29 import android.provider.ContactsContract;
30 import android.util.Log;
31 import android.view.LayoutInflater;
32 import android.view.View;
33 import android.view.ViewGroup;
34 import android.widget.AdapterView;
35 import android.widget.ArrayAdapter;
36 import android.widget.Button;
37 import android.widget.EditText;
38 import android.widget.ImageView;
39 import android.widget.Spinner;
40 import android.widget.TextView;
41 import android.widget.Toast;
42 import android.widget.AdapterView.OnItemSelectedListener;
43 
44 import java.util.ArrayList;
45 import java.util.Iterator;
46 
47 public final class ContactAdder extends Activity implements OnAccountsUpdateListener
48 {
49     public static final String TAG = "ContactsAdder";
50     public static final String ACCOUNT_NAME =
51             "com.example.android.contactmanager.ContactsAdder.ACCOUNT_NAME";
52     public static final String ACCOUNT_TYPE =
53             "com.example.android.contactmanager.ContactsAdder.ACCOUNT_TYPE";
54 
55     private ArrayList<AccountData> mAccounts;
56     private AccountAdapter mAccountAdapter;
57     private Spinner mAccountSpinner;
58     private EditText mContactEmailEditText;
59     private ArrayList<Integer> mContactEmailTypes;
60     private Spinner mContactEmailTypeSpinner;
61     private EditText mContactNameEditText;
62     private EditText mContactPhoneEditText;
63     private ArrayList<Integer> mContactPhoneTypes;
64     private Spinner mContactPhoneTypeSpinner;
65     private Button mContactSaveButton;
66     private AccountData mSelectedAccount;
67 
68     /**
69      * Called when the activity is first created. Responsible for initializing the UI.
70      */
71     @Override
onCreate(Bundle savedInstanceState)72     public void onCreate(Bundle savedInstanceState)
73     {
74         Log.v(TAG, "Activity State: onCreate()");
75         super.onCreate(savedInstanceState);
76         setContentView(R.layout.contact_adder);
77 
78         // Obtain handles to UI objects
79         mAccountSpinner = (Spinner) findViewById(R.id.accountSpinner);
80         mContactNameEditText = (EditText) findViewById(R.id.contactNameEditText);
81         mContactPhoneEditText = (EditText) findViewById(R.id.contactPhoneEditText);
82         mContactEmailEditText = (EditText) findViewById(R.id.contactEmailEditText);
83         mContactPhoneTypeSpinner = (Spinner) findViewById(R.id.contactPhoneTypeSpinner);
84         mContactEmailTypeSpinner = (Spinner) findViewById(R.id.contactEmailTypeSpinner);
85         mContactSaveButton = (Button) findViewById(R.id.contactSaveButton);
86 
87         // Prepare list of supported account types
88         // Note: Other types are available in ContactsContract.CommonDataKinds
89         //       Also, be aware that type IDs differ between Phone and Email, and MUST be computed
90         //       separately.
91         mContactPhoneTypes = new ArrayList<Integer>();
92         mContactPhoneTypes.add(ContactsContract.CommonDataKinds.Phone.TYPE_HOME);
93         mContactPhoneTypes.add(ContactsContract.CommonDataKinds.Phone.TYPE_WORK);
94         mContactPhoneTypes.add(ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
95         mContactPhoneTypes.add(ContactsContract.CommonDataKinds.Phone.TYPE_OTHER);
96         mContactEmailTypes = new ArrayList<Integer>();
97         mContactEmailTypes.add(ContactsContract.CommonDataKinds.Email.TYPE_HOME);
98         mContactEmailTypes.add(ContactsContract.CommonDataKinds.Email.TYPE_WORK);
99         mContactEmailTypes.add(ContactsContract.CommonDataKinds.Email.TYPE_MOBILE);
100         mContactEmailTypes.add(ContactsContract.CommonDataKinds.Email.TYPE_OTHER);
101 
102         // Prepare model for account spinner
103         mAccounts = new ArrayList<AccountData>();
104         mAccountAdapter = new AccountAdapter(this, mAccounts);
105         mAccountSpinner.setAdapter(mAccountAdapter);
106 
107         // Populate list of account types for phone
108         ArrayAdapter<String> adapter;
109         adapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item);
110         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
111         Iterator<Integer> iter;
112         iter = mContactPhoneTypes.iterator();
113         while (iter.hasNext()) {
114             adapter.add(ContactsContract.CommonDataKinds.Phone.getTypeLabel(
115                     this.getResources(),
116                     iter.next(),
117                     getString(R.string.undefinedTypeLabel)).toString());
118         }
119         mContactPhoneTypeSpinner.setAdapter(adapter);
120         mContactPhoneTypeSpinner.setPrompt(getString(R.string.selectLabel));
121 
122         // Populate list of account types for email
123         adapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item);
124         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
125         iter = mContactEmailTypes.iterator();
126         while (iter.hasNext()) {
127             adapter.add(ContactsContract.CommonDataKinds.Email.getTypeLabel(
128                     this.getResources(),
129                     iter.next(),
130                     getString(R.string.undefinedTypeLabel)).toString());
131         }
132         mContactEmailTypeSpinner.setAdapter(adapter);
133         mContactEmailTypeSpinner.setPrompt(getString(R.string.selectLabel));
134 
135         // Prepare the system account manager. On registering the listener below, we also ask for
136         // an initial callback to pre-populate the account list.
137         AccountManager.get(this).addOnAccountsUpdatedListener(this, null, true);
138 
139         // Register handlers for UI elements
140         mAccountSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
141             public void onItemSelected(AdapterView<?> parent, View view, int position, long i) {
142                 updateAccountSelection();
143             }
144 
145             public void onNothingSelected(AdapterView<?> parent) {
146                 // We don't need to worry about nothing being selected, since Spinners don't allow
147                 // this.
148             }
149         });
150         mContactSaveButton.setOnClickListener(new View.OnClickListener() {
151             public void onClick(View v) {
152                 onSaveButtonClicked();
153             }
154         });
155     }
156 
157     /**
158      * Actions for when the Save button is clicked. Creates a contact entry and terminates the
159      * activity.
160      */
onSaveButtonClicked()161     private void onSaveButtonClicked() {
162         Log.v(TAG, "Save button clicked");
163         createContactEntry();
164         finish();
165     }
166 
167     /**
168      * Creates a contact entry from the current UI values in the account named by mSelectedAccount.
169      */
createContactEntry()170     protected void createContactEntry() {
171         // Get values from UI
172         String name = mContactNameEditText.getText().toString();
173         String phone = mContactPhoneEditText.getText().toString();
174         String email = mContactEmailEditText.getText().toString();
175         int phoneType = mContactPhoneTypes.get(
176                 mContactPhoneTypeSpinner.getSelectedItemPosition());
177         int emailType = mContactEmailTypes.get(
178                 mContactEmailTypeSpinner.getSelectedItemPosition());;
179 
180         // Prepare contact creation request
181         //
182         // Note: We use RawContacts because this data must be associated with a particular account.
183         //       The system will aggregate this with any other data for this contact and create a
184         //       coresponding entry in the ContactsContract.Contacts provider for us.
185         ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
186         ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
187                 .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, mSelectedAccount.getType())
188                 .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, mSelectedAccount.getName())
189                 .build());
190         ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
191                 .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
192                 .withValue(ContactsContract.Data.MIMETYPE,
193                         ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
194                 .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name)
195                 .build());
196         ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
197                 .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
198                 .withValue(ContactsContract.Data.MIMETYPE,
199                         ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
200                 .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phone)
201                 .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, phoneType)
202                 .build());
203         ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
204                 .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
205                 .withValue(ContactsContract.Data.MIMETYPE,
206                         ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
207                 .withValue(ContactsContract.CommonDataKinds.Email.DATA, email)
208                 .withValue(ContactsContract.CommonDataKinds.Email.TYPE, emailType)
209                 .build());
210 
211         // Ask the Contact provider to create a new contact
212         Log.i(TAG,"Selected account: " + mSelectedAccount.getName() + " (" +
213                 mSelectedAccount.getType() + ")");
214         Log.i(TAG,"Creating contact: " + name);
215         try {
216             getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
217         } catch (Exception e) {
218             // Display warning
219             Context ctx = getApplicationContext();
220             CharSequence txt = getString(R.string.contactCreationFailure);
221             int duration = Toast.LENGTH_SHORT;
222             Toast toast = Toast.makeText(ctx, txt, duration);
223             toast.show();
224 
225             // Log exception
226             Log.e(TAG, "Exceptoin encoutered while inserting contact: " + e);
227         }
228     }
229 
230     /**
231      * Called when this activity is about to be destroyed by the system.
232      */
233     @Override
onDestroy()234     public void onDestroy() {
235         // Remove AccountManager callback
236         AccountManager.get(this).removeOnAccountsUpdatedListener(this);
237         super.onDestroy();
238     }
239 
240     /**
241      * Updates account list spinner when the list of Accounts on the system changes. Satisfies
242      * OnAccountsUpdateListener implementation.
243      */
onAccountsUpdated(Account[] a)244     public void onAccountsUpdated(Account[] a) {
245         Log.i(TAG, "Account list update detected");
246         // Clear out any old data to prevent duplicates
247         mAccounts.clear();
248 
249         // Get account data from system
250         AuthenticatorDescription[] accountTypes = AccountManager.get(this).getAuthenticatorTypes();
251 
252         // Populate tables
253         for (int i = 0; i < a.length; i++) {
254             // The user may have multiple accounts with the same name, so we need to construct a
255             // meaningful display name for each.
256             String systemAccountType = a[i].type;
257             AuthenticatorDescription ad = getAuthenticatorDescription(systemAccountType,
258                     accountTypes);
259             AccountData data = new AccountData(a[i].name, ad);
260             mAccounts.add(data);
261         }
262 
263         // Update the account spinner
264         mAccountAdapter.notifyDataSetChanged();
265     }
266 
267     /**
268      * Obtain the AuthenticatorDescription for a given account type.
269      * @param type The account type to locate.
270      * @param dictionary An array of AuthenticatorDescriptions, as returned by AccountManager.
271      * @return The description for the specified account type.
272      */
getAuthenticatorDescription(String type, AuthenticatorDescription[] dictionary)273     private static AuthenticatorDescription getAuthenticatorDescription(String type,
274             AuthenticatorDescription[] dictionary) {
275         for (int i = 0; i < dictionary.length; i++) {
276             if (dictionary[i].type.equals(type)) {
277                 return dictionary[i];
278             }
279         }
280         // No match found
281         throw new RuntimeException("Unable to find matching authenticator");
282     }
283 
284     /**
285      * Update account selection. If NO_ACCOUNT is selected, then we prohibit inserting new contacts.
286      */
updateAccountSelection()287     private void updateAccountSelection() {
288         // Read current account selection
289         mSelectedAccount = (AccountData) mAccountSpinner.getSelectedItem();
290     }
291 
292     /**
293      * A container class used to repreresent all known information about an account.
294      */
295     private class AccountData {
296         private String mName;
297         private String mType;
298         private CharSequence mTypeLabel;
299         private Drawable mIcon;
300 
301         /**
302          * @param name The name of the account. This is usually the user's email address or
303          *        username.
304          * @param description The description for this account. This will be dictated by the
305          *        type of account returned, and can be obtained from the system AccountManager.
306          */
AccountData(String name, AuthenticatorDescription description)307         public AccountData(String name, AuthenticatorDescription description) {
308             mName = name;
309             if (description != null) {
310                 mType = description.type;
311 
312                 // The type string is stored in a resource, so we need to convert it into something
313                 // human readable.
314                 String packageName = description.packageName;
315                 PackageManager pm = getPackageManager();
316 
317                 if (description.labelId != 0) {
318                     mTypeLabel = pm.getText(packageName, description.labelId, null);
319                     if (mTypeLabel == null) {
320                         throw new IllegalArgumentException("LabelID provided, but label not found");
321                     }
322                 } else {
323                     mTypeLabel = "";
324                 }
325 
326                 if (description.iconId != 0) {
327                     mIcon = pm.getDrawable(packageName, description.iconId, null);
328                     if (mIcon == null) {
329                         throw new IllegalArgumentException("IconID provided, but drawable not " +
330                                 "found");
331                     }
332                 } else {
333                     mIcon = getResources().getDrawable(android.R.drawable.sym_def_app_icon);
334                 }
335             }
336         }
337 
getName()338         public String getName() {
339             return mName;
340         }
341 
getType()342         public String getType() {
343             return mType;
344         }
345 
getTypeLabel()346         public CharSequence getTypeLabel() {
347             return mTypeLabel;
348         }
349 
getIcon()350         public Drawable getIcon() {
351             return mIcon;
352         }
353 
toString()354         public String toString() {
355             return mName;
356         }
357     }
358 
359     /**
360      * Custom adapter used to display account icons and descriptions in the account spinner.
361      */
362     private class AccountAdapter extends ArrayAdapter<AccountData> {
AccountAdapter(Context context, ArrayList<AccountData> accountData)363         public AccountAdapter(Context context, ArrayList<AccountData> accountData) {
364             super(context, android.R.layout.simple_spinner_item, accountData);
365             setDropDownViewResource(R.layout.account_entry);
366         }
367 
getDropDownView(int position, View convertView, ViewGroup parent)368         public View getDropDownView(int position, View convertView, ViewGroup parent) {
369             // Inflate a view template
370             if (convertView == null) {
371                 LayoutInflater layoutInflater = getLayoutInflater();
372                 convertView = layoutInflater.inflate(R.layout.account_entry, parent, false);
373             }
374             TextView firstAccountLine = (TextView) convertView.findViewById(R.id.firstAccountLine);
375             TextView secondAccountLine = (TextView) convertView.findViewById(R.id.secondAccountLine);
376             ImageView accountIcon = (ImageView) convertView.findViewById(R.id.accountIcon);
377 
378             // Populate template
379             AccountData data = getItem(position);
380             firstAccountLine.setText(data.getName());
381             secondAccountLine.setText(data.getTypeLabel());
382             Drawable icon = data.getIcon();
383             if (icon == null) {
384                 icon = getResources().getDrawable(android.R.drawable.ic_menu_search);
385             }
386             accountIcon.setImageDrawable(icon);
387             return convertView;
388         }
389     }
390 }
391