/**
*
*/
package fm.last.android.sync;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import fm.last.android.AndroidLastFmServerFactory;
import fm.last.android.LastFMApplication;
import fm.last.android.R;
import fm.last.android.player.RadioPlayerService;
import fm.last.api.Friends;
import fm.last.api.LastFmServer;
import fm.last.api.Tasteometer;
import fm.last.api.Track;
import fm.last.api.User;
import fm.last.api.WSError;
import fm.last.util.UrlUtil;
import android.accounts.Account;
import android.accounts.OperationCanceledException;
import android.app.Service;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.content.SharedPreferences.Editor;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.provider.ContactsContract;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.RawContacts.Entity;
/**
* @author sam
*
*/
public class ContactsSyncAdapterService extends Service {
private static SyncAdapterImpl sSyncAdapter = null;
private static ContentResolver mContentResolver = null;
private static String UsernameColumn = ContactsContract.RawContacts.SYNC1;
private static String PhotoUrlColumn = ContactsContract.RawContacts.SYNC2;
private static String PhotoTimestampColumn = ContactsContract.RawContacts.SYNC3;
private static String TasteTimestampColumn = ContactsContract.RawContacts.SYNC4;
private static Integer syncSchema = 1;
public ContactsSyncAdapterService() {
super();
}
private static class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
private Context mContext;
public SyncAdapterImpl(Context context) {
super(context, true);
mContext = context;
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
try {
ContactsSyncAdapterService.performSync(mContext, account, extras, authority, provider, syncResult);
} catch (OperationCanceledException e) {
}
}
}
@Override
public IBinder onBind(Intent intent) {
IBinder ret = null;
ret = getSyncAdapter().getSyncAdapterBinder();
return ret;
}
private SyncAdapterImpl getSyncAdapter() {
if (sSyncAdapter == null)
sSyncAdapter = new SyncAdapterImpl(this);
return sSyncAdapter;
}
private static long addContact(Account account, String name, String username) {
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI);
builder.withValue(RawContacts.ACCOUNT_NAME, account.name);
builder.withValue(RawContacts.ACCOUNT_TYPE, account.type);
builder.withValue(UsernameColumn, username);
operationList.add(builder.build());
if(name.length() > 0 && PreferenceManager.getDefaultSharedPreferences(LastFMApplication.getInstance()).getBoolean("sync_names", true)) {
builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
builder.withValueBackReference(ContactsContract.CommonDataKinds.StructuredName.RAW_CONTACT_ID, 0);
builder.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
builder.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name);
operationList.add(builder.build());
} else {
builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
builder.withValueBackReference(ContactsContract.CommonDataKinds.Nickname.RAW_CONTACT_ID, 0);
builder.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE);
builder.withValue(ContactsContract.CommonDataKinds.Nickname.NAME, username);
operationList.add(builder.build());
}
builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
builder.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0);
builder.withValue(ContactsContract.Data.MIMETYPE, "vnd.android.cursor.item/vnd.fm.last.android.profile");
builder.withValue(ContactsContract.Data.DATA1, username);
builder.withValue(ContactsContract.Data.DATA2, "Last.fm Profile");
builder.withValue(ContactsContract.Data.DATA3, "View profile");
operationList.add(builder.build());
if(!PreferenceManager.getDefaultSharedPreferences(LastFMApplication.getInstance()).getBoolean("sync_website", true)) {
builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
builder.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0);
builder.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE);
builder.withValue(ContactsContract.CommonDataKinds.Website.TYPE, ContactsContract.CommonDataKinds.Website.TYPE_PROFILE);
builder.withValue(ContactsContract.CommonDataKinds.Website.URL, "http://www.last.fm/user/" + username);
operationList.add(builder.build());
}
try {
mContentResolver.applyBatch(ContactsContract.AUTHORITY, operationList);
// Load the local Last.fm contacts
Uri rawContactUri = RawContacts.CONTENT_URI.buildUpon().appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name).appendQueryParameter(
RawContacts.ACCOUNT_TYPE, account.type).build();
Cursor c1 = mContentResolver.query(rawContactUri, new String[] { BaseColumns._ID, UsernameColumn }, UsernameColumn + " = '" + username + "'", null, null);
if (c1.moveToNext()) {
return c1.getLong(0);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return -1;
}
private static void updateContactStatus(ArrayList<ContentProviderOperation> operationList, long rawContactId, Track track) {
Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
Uri entityUri = Uri.withAppendedPath(rawContactUri, Entity.CONTENT_DIRECTORY);
Cursor c = mContentResolver.query(entityUri, new String[] { Entity.DATA_ID }, Entity.MIMETYPE + " = 'vnd.android.cursor.item/vnd.fm.last.android.profile'", null, null);
try {
if (c.moveToNext()) {
if (!c.isNull(0)) {
String status = "";
Boolean gotTrack = false;
if (track.getNowPlaying() != null && track.getNowPlaying().equals("true")) {
status = "Listening to ";
} else {
status = "Listened to ";
}
if(track.getName() != null && !track.getName().equals(RadioPlayerService.UNKNOWN)) {
status += track.getName();
gotTrack = true;
}
if(track.getArtist() != null && track.getArtist().getName() != null && !track.getArtist().getName().equals(RadioPlayerService.UNKNOWN)) {
if(gotTrack)
status += " by ";
status += track.getArtist().getName();
}
ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(ContactsContract.StatusUpdates.CONTENT_URI);
builder.withValue(ContactsContract.StatusUpdates.DATA_ID, c.getLong(0));
builder.withValue(ContactsContract.StatusUpdates.STATUS, status);
builder.withValue(ContactsContract.StatusUpdates.STATUS_RES_PACKAGE, "fm.last.android");
builder.withValue(ContactsContract.StatusUpdates.STATUS_LABEL, R.string.app_name);
builder.withValue(ContactsContract.StatusUpdates.STATUS_ICON, R.drawable.icon);
if (track.getDate() != null) {
long date = Long.parseLong(track.getDate()) * 1000;
builder.withValue(ContactsContract.StatusUpdates.STATUS_TIMESTAMP, date);
}
operationList.add(builder.build());
if(Integer.decode(Build.VERSION.SDK) <= 10) {
builder = ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI);
builder.withSelection(BaseColumns._ID + " = '" + c.getLong(0) + "'", null);
builder.withValue(ContactsContract.Data.DATA3, status);
operationList.add(builder.build());
}
}
}
} finally {
c.close();
}
}
private static void updateContactName(ArrayList<ContentProviderOperation> operationList, long rawContactId, String name, String username) {
ContentProviderOperation.Builder builder = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI);
builder.withSelection(ContactsContract.Data.RAW_CONTACT_ID + " = '" + rawContactId
+ "' AND (" + ContactsContract.Data.MIMETYPE + " = '" + ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE + "'"
+ " OR " + ContactsContract.Data.MIMETYPE + " = '" + ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE + "')", null);
operationList.add(builder.build());
if(name.length() > 0 && PreferenceManager.getDefaultSharedPreferences(LastFMApplication.getInstance()).getBoolean("sync_names", true)) {
builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
builder.withValue(ContactsContract.CommonDataKinds.StructuredName.RAW_CONTACT_ID, rawContactId);
builder.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
builder.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name);
operationList.add(builder.build());
} else {
builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
builder.withValue(ContactsContract.CommonDataKinds.Nickname.RAW_CONTACT_ID, rawContactId);
builder.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE);
builder.withValue(ContactsContract.CommonDataKinds.Nickname.NAME, username);
operationList.add(builder.build());
}
}
private static void updateContactPhoto(ArrayList<ContentProviderOperation> operationList, long rawContactId, String url) {
byte[] image;
ContentProviderOperation.Builder builder = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI);
builder.withSelection(ContactsContract.Data.RAW_CONTACT_ID + " = '" + rawContactId
+ "' AND " + ContactsContract.Data.MIMETYPE + " = '" + ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE + "'", null);
operationList.add(builder.build());
if(!PreferenceManager.getDefaultSharedPreferences(LastFMApplication.getInstance()).getBoolean("sync_icons", true)) {
return;
}
try {
if(url != null && url.length() > 0) {
image = UrlUtil.doGetAndReturnBytes(new URL(url), 65535);
if(image.length > 0) {
builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
builder.withValue(ContactsContract.CommonDataKinds.Photo.RAW_CONTACT_ID, rawContactId);
builder.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE);
builder.withValue(ContactsContract.CommonDataKinds.Photo.PHOTO, image);
operationList.add(builder.build());
builder = ContentProviderOperation.newUpdate(ContactsContract.RawContacts.CONTENT_URI);
builder.withSelection(ContactsContract.RawContacts.CONTACT_ID + " = '" + rawContactId + "'", null);
builder.withValue(PhotoUrlColumn, url);
builder.withValue(PhotoTimestampColumn, String.valueOf(System.currentTimeMillis()));
operationList.add(builder.build());
}
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private static void updateTasteometer(ArrayList<ContentProviderOperation> operationList, long rawContactId, String username, Tasteometer taste) {
ContentProviderOperation.Builder builder = ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI);
builder.withSelection(ContactsContract.Data.RAW_CONTACT_ID + " = '" + rawContactId
+ "' AND " + ContactsContract.Data.MIMETYPE + " = 'vnd.android.cursor.item/vnd.fm.last.android.tasteometer'", null);
operationList.add(builder.build());
if(!PreferenceManager.getDefaultSharedPreferences(LastFMApplication.getInstance()).getBoolean("sync_taste", true)) {
return;
}
String tastes[] = { "very low", "low", "medium", "high", "super" };
String artists = "";
Integer tasteIdx = (int)(taste.getScore() * 5);
if(tasteIdx > 4)
tasteIdx = 4;
for(String artist : taste.getResults()) {
if(artists.length() > 0)
artists += ", ";
artists += artist;
}
try {
builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
builder.withValue(ContactsContract.Data.RAW_CONTACT_ID, rawContactId);
builder.withValue(ContactsContract.Data.MIMETYPE, "vnd.android.cursor.item/vnd.fm.last.android.tasteometer");
builder.withValue(ContactsContract.Data.DATA1, username );
builder.withValue(ContactsContract.Data.DATA2, "Musical Compatibility" );
builder.withValue(ContactsContract.Data.DATA3, "Your musical compatibility is " + tastes[tasteIdx]);
operationList.add(builder.build());
builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
builder.withValue(ContactsContract.Data.RAW_CONTACT_ID, rawContactId);
builder.withValue(ContactsContract.Data.MIMETYPE, "vnd.android.cursor.item/vnd.fm.last.android.tasteometer");
builder.withValue(ContactsContract.Data.DATA1, username );
builder.withValue(ContactsContract.Data.DATA2, "Common Artists" );
builder.withValue(ContactsContract.Data.DATA3, artists);
operationList.add(builder.build());
builder = ContentProviderOperation.newUpdate(ContactsContract.RawContacts.CONTENT_URI);
builder.withSelection(ContactsContract.RawContacts.CONTACT_ID + " = '" + rawContactId + "'", null);
builder.withValue(TasteTimestampColumn, String.valueOf(System.currentTimeMillis()));
operationList.add(builder.build());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private static void deleteContact(Context context, long rawContactId) {
Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId).buildUpon().appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build();
ContentProviderClient client = context.getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
try {
client.delete(uri, null, null);
} catch (RemoteException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
client.release();
}
private static class SyncEntry {
public Long raw_id = 0L;
public String photo_url = null;
public Long photo_timestamp = null;
public Long taste_timestamp = null;
}
private static void performSync(Context context, Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult)
throws OperationCanceledException {
HashMap<String, SyncEntry> localContacts = new HashMap<String, SyncEntry>();
ArrayList<String> lastfmFriends = new ArrayList<String>();
mContentResolver = context.getContentResolver();
//If our app has requested a full sync, we're going to delete all our local contacts and start over
boolean is_full_sync = PreferenceManager.getDefaultSharedPreferences(LastFMApplication.getInstance()).getBoolean("do_full_sync", false);
//If our schema is out-of-date, do a fresh sync
if(PreferenceManager.getDefaultSharedPreferences(LastFMApplication.getInstance()).getInt("sync_schema", 0) < syncSchema)
is_full_sync = true;
// Load the local Last.fm contacts
Uri rawContactUri = RawContacts.CONTENT_URI.buildUpon().appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name).appendQueryParameter(
RawContacts.ACCOUNT_TYPE, account.type).build();
Cursor c1 = mContentResolver.query(rawContactUri, new String[] { BaseColumns._ID, UsernameColumn, PhotoUrlColumn, PhotoTimestampColumn, TasteTimestampColumn }, null, null, null);
while (c1 != null && c1.moveToNext()) {
if(is_full_sync) {
deleteContact(context, c1.getLong(0));
} else {
SyncEntry entry = new SyncEntry();
entry.raw_id = c1.getLong(0);
entry.photo_url = c1.getString(2);
entry.photo_timestamp = c1.getLong(3);
entry.taste_timestamp = c1.getLong(4);
localContacts.put(c1.getString(1), entry);
}
}
Editor editor = PreferenceManager.getDefaultSharedPreferences(LastFMApplication.getInstance()).edit();
editor.remove("do_full_sync");
editor.putInt("sync_schema", syncSchema);
editor.commit();
LastFmServer server = AndroidLastFmServerFactory.getServer();
ArrayList<User> friends = null;
try {
friends = new ArrayList<User>();
Friends f = server.getFriends(account.name, null, "1024");
for (User user : f.getFriends()) {
friends.add(user);
}
User self = server.getUserInfo(account.name, LastFMApplication.getInstance().session.getKey());
friends.add(self);
for (User user : friends) {
if (!localContacts.containsKey(user.getName())) {
long id = addContact(account, user.getRealName(), user.getName());
if(id != -1) {
SyncEntry entry = new SyncEntry();
entry.raw_id = id;
localContacts.put(user.getName(), entry);
}
}
}
} catch (WSError e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
return;
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
return;
}
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
for (User user : friends) {
String username = user.getName();
lastfmFriends.add(username);
if (localContacts.containsKey(username)) {
SyncEntry entry = localContacts.get(username);
if (entry.photo_timestamp == null || System.currentTimeMillis() > (entry.photo_timestamp + 604800000L)) {
String url = null;
if(user.getImages().length > 0)
url = user.getURLforImageSize("extralarge");
if(entry.photo_url != url)
updateContactPhoto(operationList, entry.raw_id, url);
}
try {
Track[] tracks = null;
tracks = server.getUserRecentTracks(username, "true", 1);
if (tracks.length > 0) {
updateContactStatus(operationList, entry.raw_id, tracks[0]);
}
} catch (Exception e) {
e.printStackTrace();
} catch (WSError e) {
e.printStackTrace();
}
try {
if (!account.name.equals(username) && (entry.taste_timestamp == null || System.currentTimeMillis() > (entry.taste_timestamp + 2628000000L))) {
Tasteometer taste;
taste = server.tasteometerCompare(account.name, username, 3);
if(Integer.decode(Build.VERSION.SDK) <= 10)
updateTasteometer(operationList, entry.raw_id, username, taste);
}
} catch (Exception e) {
e.printStackTrace();
} catch (WSError e) {
e.printStackTrace();
}
updateContactName(operationList, entry.raw_id, user.getRealName(), username);
}
if(operationList.size() >= 50) {
try {
mContentResolver.applyBatch(ContactsContract.AUTHORITY, operationList);
} catch (Exception e) {
e.printStackTrace();
}
operationList.clear();
}
}
if(operationList.size() > 0) {
try {
mContentResolver.applyBatch(ContactsContract.AUTHORITY, operationList);
} catch (Exception e) {
e.printStackTrace();
}
}
Iterator<String> i = localContacts.keySet().iterator();
while(i.hasNext()) {
String user = i.next();
if(!lastfmFriends.contains(user))
deleteContact(context, localContacts.get(user).raw_id);
}
}
}