注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

分享,态度 ·~~

—— 十年太长,五年;如果可以回到五年前,你最想对那时候的自己说什么?

 
 
 

日志

 
 

Writing an Android Sync Provider: Part 2  

2011-02-18 21:50:21|  分类: Android |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

One of the great new user-facing features of Android 2.0 is the is the new Facebook app, which brings your Facebook contacts and statuses into your Android contacts database:

Writing an Android Sync Provider: Part 2 - 乂乂 - 一个人,一支烟  ·~~ 

So, how exactly does my Nexus One know that Chris is excited about the upcoming launch of his new mobile apps? The answer is a Contacts sync provider in the Facebook app. Read on to learn how to create your own!

Sync Providers

Sync providers are services that allow an Account to synchronize data on the device on a regular basis. Not quite sure how to create an Account? Read part one first! To implement a Contacts sync provider, we’ll need a service, some xml files, and the following permissions added to the AndroidManifest.xml:

AndroidManifest.xml snippet

  1. <uses-permission android:name="android.permission.INTERNET" />
  2. <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  3. <uses-permission android:name="android.permission.READ_CONTACTS" />
  4. <uses-permission android:name="android.permission.WRITE_CONTACTS" />
  5. <uses-permission android:name="android.permission.GET_ACCOUNTS" />
  6. <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
  7. <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
  8. <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
  9. <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />

The Service

Similar to our Account Authenticator service, our Contacts Sync Provider service will return a subclass of AbstractThreadedSyncAdapter from the onBind method.

ContactsSyncAdapterService.java

  1. public class ContactsSyncAdapterService extends Service {
  2.  private static final String TAG = "ContactsSyncAdapterService";
  3.  private static SyncAdapterImpl sSyncAdapter = null;
  4.  private static ContentResolver mContentResolver = null;
  5.  
  6.  public ContactsSyncAdapterService() {
  7.   super();
  8.  }
  9.  
  10.  private static class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
  11.   private Context mContext;
  12.  
  13.   public SyncAdapterImpl(Context context) {
  14.    super(context, true);
  15.    mContext = context;
  16.   }
  17.  
  18.   @Override
  19.   public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
  20.    try {
  21.     ContactsSyncAdapterService.performSync(mContext, account, extras, authority, provider, syncResult);
  22.    } catch (OperationCanceledException e) {
  23.    }
  24.   }
  25.  }
  26.  
  27.  @Override
  28.  public IBinder onBind(Intent intent) {
  29.   IBinder ret = null;
  30.   ret = getSyncAdapter().getSyncAdapterBinder();
  31.   return ret;
  32.  }
  33.  
  34.  private SyncAdapterImpl getSyncAdapter() {
  35.   if (sSyncAdapter == null)
  36.    sSyncAdapter = new SyncAdapterImpl(this);
  37.   return sSyncAdapter;
  38.  }
  39.  
  40.  private static void performSync(Context context, Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult)
  41.    throws OperationCanceledException {
  42.   mContentResolver = context.getContentResolver();
  43.   Log.i(TAG, "performSync: " + account.toString());
  44.   //This is where the magic will happen!
  45.  }
  46. }

The service is defined in AndroidManifest.xml like so:

AndroidManifest.xml snippet

  1. <service android:name=".sync.ContactsSyncAdapterService"
  2.  android:exported="true" android:process=":contacts">
  3.  <intent-filter>
  4.   <action android:name="android.content.SyncAdapter" />
  5.  </intent-filter>
  6.  <meta-data android:name="android.content.SyncAdapter"
  7.   android:resource="@xml/sync_contacts" />
  8. </service>

Finally, we need an xml file to let Android know that our sync provider handles Contacts for the Account type we defined in part 1.

sync_contacts.xml

  1. <sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
  2.     android:contentAuthority="com.android.contacts"
  3.     android:accountType="fm.last.android.account"/>

At this point we have a sync provider that doesn’t do anything, but also shouldn’t crash the Android system. Just in case, lets test it in Dev Tools first. Start the Android emulator and launch the “Dev Tools” app, then scroll down to “Sync Tester”.

Writing an Android Sync Provider: Part 2 - 乂乂 - 一个人,一支烟  ·~~ 

The drop-down should confirm that com.android.contacts can be synced with the account type you’ve created. Select the entry for your account type and press the “Bind” button to connect to your sync service. If all goes well, Sync Tester will say it’s connected to your service. Now click “Start Sync” and select your account from the popup. Sync Tester will let you know that the sync succeeded (even though we didn’t actually do anything). At this point it should be safe to enable contact syncing from the Accounts & Sync settings screen without Android crashing and rebooting.
Writing an Android Sync Provider: Part 2 - 乂乂 - 一个人,一支烟  ·~~ 

Contacts

Lets take a moment to discuss how Contacts on Android work. Each sync account, such as Google or Facebook, creates its own set of RawContacts which the Android system then aggregates into the single list of contacts you see in the Dialer. The RawContacts table contains several fields that Sync Providers can use for whatever they like, and in this implementation we will use the SYNC1 field to store the RawContact’s Last.fm username.

Contact Data

Data, such as name, phone number, email address, etc. is stored in a table that references a RawContact ID. The Data table can contain anything you like, and there are several predefined MIME-types available for phone numbers, email addresses, names, etc. Android will automatically try to combine RawContacts that contain the same name, email address, etc. into a single Contact, and the user can also combine and split Contacts manually from the contact edit screen.

Writing an Android Sync Provider: Part 2 - 乂乂 - 一个人,一支烟  ·~~ 

Custom Data Types

A sync provider can store additional data about a RawContact in the Data table, and provide an xml file to tell the Contacts app how to format this row. To create a custom MIME type, we need to add an additional meta-data tag to our service entry in AndroidManifest.xml to reference a new ContactsSource XML file:

AndroidManifest.xml snippet

  1. <meta-data android:name="android.provider.CONTACTS_STRUCTURE"
  2.  android:resource="@xml/contacts" />

This ContactsSource xml file will tell the Contacts app how to format our custom MIME-types. In the example below, we specify an icon, and tell the Contacts app to use the DATA2 and DATA3 columns to render the fields. Note that this file’s format doesn’t appear to be documented outside of the source code for the Contacts app.

contacts.xml

  1. <ContactsSource xmlns:android="http://schemas.android.com/apk/res/android">
  2.  <ContactsDataKind
  3.   android:icon="@drawable/icon"
  4.   android:mimeType="vnd.android.cursor.item/vnd.fm.last.android.profile"
  5.   android:summaryColumn="data2"
  6.   android:detailColumn="data3"
  7.   android:detailSocialSummary="true" />
  8. </ContactsSource>

Here’s how Facebook’s custom “Facebook Profile” field looks when rendered by the Contacts app:

Writing an Android Sync Provider: Part 2 - 乂乂 - 一个人,一支烟  ·~~ 

Creating a RawContact

Now lets put all the above information together to create a RawContact for a Last.fm user. Our contacts will display only a name and a custom field that links to the user’s Last.fm profile. We store the user’s username in the RawContact’s SYNC1 field so we can easily look it up later. We will batch together the creation of the RawContact and the insertion of the Data, as Android will run an aggregation pass after each batch completes.

addContact method

  1. private static void addContact(Account account, String name, String username) {
  2.  Log.i(TAG, "Adding contact: " + name);
  3.  ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
  4.  
  5.  //Create our RawContact
  6.  ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI);
  7.  builder.withValue(RawContacts.ACCOUNT_NAME, account.name);
  8.  builder.withValue(RawContacts.ACCOUNT_TYPE, account.type);
  9.  builder.withValue(RawContacts.SYNC1, username);
  10.  operationList.add(builder.build());
  11.  
  12.  //Create a Data record of common type 'StructuredName' for our RawContact
  13.  builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
  14.  builder.withValueBackReference(ContactsContract.CommonDataKinds.StructuredName.RAW_CONTACT_ID0);
  15.  builder.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
  16.  builder.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name);
  17.  operationList.add(builder.build());
  18.  
  19.  //Create a Data record of custom type "vnd.android.cursor.item/vnd.fm.last.android.profile" to display a link to the Last.fm profile
  20.  builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
  21.  builder.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID0);
  22.  builder.withValue(ContactsContract.Data.MIMETYPE"vnd.android.cursor.item/vnd.fm.last.android.profile");
  23.  builder.withValue(ContactsContract.Data.DATA1, username);
  24.  builder.withValue(ContactsContract.Data.DATA2"Last.fm Profile");
  25.  builder.withValue(ContactsContract.Data.DATA3"View profile");
  26.  operationList.add(builder.build());
  27.  
  28.  try {
  29.   mContentResolver.applyBatch(ContactsContract.AUTHORITY, operationList);
  30.  } catch (Exception e) {
  31.   Log.e(TAG, "Something went wrong during creation! " + e);
  32.   e.printStackTrace();
  33.  }
  34. }

Social status updates

Android keeps another table for social networking status updates. Inserting a record into this table will replace any previous status if the timestamp of the insert is newer than the previous timestamp, otherwise the previous record will remain and the new insert will be discarded. A status update record is associated with a Data record, in our implementation we will associate it with our Last.fm profile record. Status records contain the status text, a package name where resources are located, an icon resource, and a label resource. Below is a function that will insert a status update, as well as updating our Last.fm Profile Data record to display the last track the user listened to. Note that for efficiency purposes, we will send all the updates in a single batch, so Android will only run a single aggregation pass at the end.

updateContactStatus method

  1. private static void updateContactStatus(ArrayList<ContentProviderOperation> operationList, long rawContactId, Track track) {
  2.  Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
  3.  Uri entityUri = Uri.withAppendedPath(rawContactUri, Entity.CONTENT_DIRECTORY);
  4.  Cursor c = mContentResolver.query(entityUri, new String[] { RawContacts.SOURCE_IDEntity.DATA_IDEntity.MIMETYPEEntity.DATA1 }nullnullnull);
  5.  try {
  6.   while (c.moveToNext()) {
  7.    if (!c.isNull(1)) {
  8.     String mimeType = c.getString(2);
  9.     String status = "";
  10.     if (track.getNowPlaying() !null && track.getNowPlaying().equals("true"))
  11.      status = "Listening to " + track.getName() + " by " + track.getArtist();
  12.     else
  13.      status = "Listened to " + track.getName() + " by " + track.getArtist();
  14.  
  15.     if (mimeType.equals("vnd.android.cursor.item/vnd.fm.last.android.profile")) {
  16.      ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(ContactsContract.StatusUpdates.CONTENT_URI);
  17.      builder.withValue(ContactsContract.StatusUpdates.DATA_ID, c.getLong(1));
  18.      builder.withValue(ContactsContract.StatusUpdates.STATUS, status);
  19.      builder.withValue(ContactsContract.StatusUpdates.STATUS_RES_PACKAGE"fm.last.android");
  20.      builder.withValue(ContactsContract.StatusUpdates.STATUS_LABEL, R.string.app_name);
  21.      builder.withValue(ContactsContract.StatusUpdates.STATUS_ICON, R.drawable.icon);
  22.      if (track.getDate() !null) {
  23.       long date = Long.parseLong(track.getDate()) * 1000;
  24.       builder.withValue(ContactsContract.StatusUpdates.STATUS_TIMESTAMP, date);
  25.      }
  26.      operationList.add(builder.build());
  27.  
  28.      builder = ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI);
  29.      builder.withSelection(BaseColumns._ID + " = '" + c.getLong(1) + "'"null);
  30.      builder.withValue(ContactsContract.Data.DATA3, status);
  31.      operationList.add(builder.build());
  32.     }
  33.    }
  34.   }
  35.  } finally {
  36.   c.close();
  37.  }
  38. }

Here’s how the contact record looks after we’ve inserted a status update record and updated our data record:

Writing an Android Sync Provider: Part 2 - 乂乂 - 一个人,一支烟  ·~~ 

Putting It All Together

Now that we have our utility functions written, we can fill in the body of our performSync method. We’re going to take a very simple approach to syncing: we’ll grab the list of RawContact IDs currently associated with Last.fm usernames, and we’ll create a RawContact for any Last.fm friend that doesn’t currently have one. If a RawContact already exists, we’ll fetch their most recent track and post a status update. Note that this is a one-way sync, we’re not dealing with local deletions or changes.

performSync method

  1. private static void performSync(Context context, Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult)
  2.   throws OperationCanceledException {
  3.  HashMap<String, Long> localContacts = new HashMap<String, Long>();
  4.  mContentResolver = context.getContentResolver();
  5.  Log.i(TAG, "performSync: " + account.toString());
  6.  
  7.  // Load the local Last.fm contacts
  8.  Uri rawContactUri = RawContacts.CONTENT_URI.buildUpon().appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name).appendQueryParameter(
  9.    RawContacts.ACCOUNT_TYPE, account.type).build();
  10.  Cursor c1 = mContentResolver.query(rawContactUri, new String[] { BaseColumns._ID, RawContacts.SYNC1 }nullnullnull);
  11.  while (c1.moveToNext()) {
  12.   localContacts.put(c1.getString(1), c1.getLong(0));
  13.  }
  14.  
  15.  ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
  16.  LastFmServer server = AndroidLastFmServerFactory.getServer();
  17.  try {
  18.   Friends friends = server.getFriends(account.name"""50");
  19.   for (User user : friends.getFriends()) {
  20.    if (!localContacts.containsKey(user.getName())) {
  21.     if (user.getRealName().length() > 0)
  22.      addContact(account, user.getRealName(), user.getName());
  23.     else
  24.      addContact(account, user.getName(), user.getName());
  25.    } else {
  26.     Track[] tracks = server.getUserRecentTracks(user.getName()"true"1);
  27.     if (tracks.length > 0) {
  28.      updateContactStatus(operationList, localContacts.get(user.getName()), tracks[0]);
  29.     }
  30.    }
  31.   }
  32.   if(operationList.size() > 0)
  33.    mContentResolver.applyBatch(ContactsContract.AUTHORITY, operationList);
  34.  } catch (Exception e1) {
  35.   // TODO Auto-generated catch block
  36.   e1.printStackTrace();
  37.  }
  38. }

Contact visibility

By default, new contacts that you create from your sync provider are not shown by the Contacts app. Instead, it will show only contacts it has aggregated with other visible sources. To enable the display of contacts that only exist in your provider, press the Menu button and select “Display options”, then expand the entry for your account and tap the checkbox next to “All Contacts”. This is a good default, as I don’t really want my address book to be littered with hundreds of twitter contacts!

Final Thoughts

The Android platform is awesome. The lack of documentation, however, is quite disappointing. While it’s great that I can dig through the system source code while troubleshooting and figuring things out, I shouldn’t have to!

Sync providers require Android 2.0, which is currently only available on 2 devices. The Account modifications I made to our login activity will need to be reimplemented using class introspection in order to keep compatibility with Android 1.5.

This sync provider is far from complete — I still need to figure out how to control the sync interval, set my sync provider as read-only since local changes are ignored, I might grab the user’s avatar if they don’t currently have an icon, and add a fancy “Do you want to sync your contacts?” popup to the login process similar to Facebook’s.

Hopefully this will help other developers out there struggling with the lack of documentation, as I’m looking forward to seeing twitter status updates in the contacts app in the future!

The source code for the implementation referenced here is available in my Last.fm github project under the terms of the GNU General Public License. A standalone sample project is also available here under the terms of the Apache License 2.0. Google has also released their own sample sync provider on the Android developer portal that’s a bit more complete than mine.

【from http://www.c99.org/2010/01/23/writing-an-android-sync-provider-part-2/
【for more:
http://zwkufo.blog.163.com/blog/static/25882512011118937543/

  评论这张
 
阅读(2541)| 评论(0)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017