/*
* Copyright 2012 The Stanford MobiSocial Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mobisocial.musubi.service;
import gnu.trove.iterator.TLongIterator;
import gnu.trove.list.array.TLongArrayList;
import gnu.trove.map.hash.TLongObjectHashMap;
import gnu.trove.procedure.TLongProcedure;
import gnu.trove.set.hash.TLongHashSet;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import mobisocial.crypto.IBHashedIdentity.Authority;
import mobisocial.crypto.IBIdentity;
import mobisocial.musubi.App;
import mobisocial.musubi.BootstrapActivity;
import mobisocial.musubi.R;
import mobisocial.musubi.model.MFeed;
import mobisocial.musubi.model.MIdentity;
import mobisocial.musubi.model.MMyAccount;
import mobisocial.musubi.model.helpers.ContactDataVersionManager;
import mobisocial.musubi.model.helpers.DatabaseManager;
import mobisocial.musubi.model.helpers.FeedManager;
import mobisocial.musubi.model.helpers.IdentitiesManager;
import mobisocial.musubi.model.helpers.MyAccountManager;
import mobisocial.musubi.model.helpers.SQLClauseHelper;
import mobisocial.musubi.util.IdentityCache;
import mobisocial.musubi.util.Util;
import org.apache.commons.io.IOUtils;
import org.javatuples.Pair;
import android.accounts.Account;
import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
import android.os.SystemClock;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds;
import android.util.Log;
public class AddressBookUpdateHandler extends ContentObserver {
private static final int BATCH_SIZE = 50;
public static int sAddressBookTotal = 0;
public static int sAddressBookPosition = 0;
//at most twice / minute
private static final int ONCE_PER_PERIOD = 30 * 1000;
private static final String TAG = "AddressBookUpdateHandler";
private static final boolean DBG = false;
private final Context mContext;
private final IdentityCache mContactThumbnailCache;
HandlerThread mThread;
int mChangeCount = 0;
private String mAccountType;
private static final String NAME_OR_OTHER_SELECTION =
ContactsContract.Data.MIMETYPE + "='" + CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE + "' OR " ;
private static final String FACEBOOK_MIMETYPE = "vnd.android.cursor.item/vnd.facebook.profile";
private static final String TWITTER_MIMETYPE = "vnd.android.cursor.item/vnd.twitter.profile";
private static final String BASE_ACCOUNT_TYPES_SELECTION =
ContactsContract.Data.MIMETYPE + "='" + CommonDataKinds.Email.CONTENT_ITEM_TYPE + "' ";
private static final String FACEBOOK_ACCOUNT_TYPES_SELECTION =
" OR " + ContactsContract.Data.MIMETYPE + "='" + FACEBOOK_MIMETYPE + "' ";
private static final String TWITTER_ACCOUNT_TYPES_SELECTION =
" OR " + ContactsContract.Data.MIMETYPE + "='" + TWITTER_MIMETYPE + "' ";
private static final String PHONE_ACCOUNT_TYPES_SELECTION =
" OR " + ContactsContract.Data.MIMETYPE + "='" + CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "' ";
private long mLastRun;
private boolean mScheduled;
private long mSleepTime = 0;
static final int SLEEP_SCALE = 14;
//Note that if you change these, you will need to clear the contact data version
//table and reset the sync state table in the upgrade process.
private final static boolean SYNC_EMAIL = true; //must always be set, code will fail if you change this to false (because of query building logic)
private final static boolean SYNC_PHONE = false;
private final static boolean SYNC_TWITTER = false;
private final static boolean SYNC_FACEBOOK = true;
public AddressBookUpdateHandler(Context context, SQLiteOpenHelper dbh, HandlerThread thread, ContentResolver resolver) {
super(new Handler(thread.getLooper()));
mThread = thread;
mContext = context.getApplicationContext();
mContactThumbnailCache = App.getContactCache(context);
mAccountType = mContext.getString(R.string.account_type);
resolver.registerContentObserver(MusubiService.FORCE_RESCAN_CONTACTS, false, new ContentObserver(new Handler(thread.getLooper())) {
public void onChange(boolean selfChange) {
mLastRun = -1;
AddressBookUpdateHandler.this.dispatchChange(false);
}
});
dispatchChange(false);
}
static final Pattern getEmailPattern() {
return Pattern.compile("\\b[A-Z0-9._%-]+@[A-Z0-9.-]+\\.[A-Z]{2,4}\\b", Pattern.CASE_INSENSITIVE);
}
static final Pattern getNumberPattern() {
return Pattern.compile("[0-9]+");
}
@Override
public void onChange(boolean selfChange) {
final DatabaseManager dbManager = new DatabaseManager(mContext);
if (!dbManager.getIdentitiesManager().hasConnectedAccounts()) {
Log.w(TAG, "no connected accounts, skipping friend import");
return;
}
//a new meta contact appears (and the previous ones disappear) if the user merges
//or if a new entry is added, we can detect the ones that have changed by
//this condition
long highestContactIdAlreadySeen = dbManager.getContactDataVersionManager().getMaxContactIdSeen();
//a new data item corresponds with a new contact, but its possible
//that a users just adds a new contact method to an existing contact
//and we need to detect that
long highestDataIdAlreadySeen = dbManager.getContactDataVersionManager().getMaxDataIdSeen();
// BJD -- this didn't end up being faster once all import features were added.
/*if (highestContactIdAlreadySeen == -1) {
importFullAddressBook(mContext);
return;
}*/
long now = System.currentTimeMillis();
if(mLastRun + ONCE_PER_PERIOD > now) {
//wake up when the period expires
if(!mScheduled) {
new Handler(mThread.getLooper()).postDelayed(new Runnable() {
@Override
public void run() {
mScheduled = false;
dispatchChange(false);
}
}, ONCE_PER_PERIOD - (now - mLastRun) + 1);
}
mScheduled = true;
//skip this update
return;
}
Log.i(TAG, "waking up to handle contact changes...");
boolean identityAdded = false, profileDataChanged = false;
Date start = new Date();
assert(SYNC_EMAIL);
String account_type_selection = getAccountSelectionString();
Cursor c = mContext.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
new String[] {
ContactsContract.Data._ID,
ContactsContract.Data.DATA_VERSION,
ContactsContract.Data.CONTACT_ID
},
"(" +
ContactsContract.Data.DATA_VERSION + ">0 OR " + //maybe updated
ContactsContract.Data.CONTACT_ID + ">? OR " + //definitely new or merged
ContactsContract.Data._ID + ">? " + //definitely added a data item
") AND (" +
ContactsContract.RawContacts.ACCOUNT_TYPE + "<>'" + mAccountType + "'" +
") AND (" +
NAME_OR_OTHER_SELECTION + account_type_selection +
")", // All known contacts.
new String[] { String.valueOf(highestContactIdAlreadySeen), String.valueOf(highestDataIdAlreadySeen) },
null
);
if (c == null) {
Log.e(TAG, "no valid cursor", new Throwable());
mContext.getContentResolver().notifyChange(MusubiService.ADDRESS_BOOK_SCANNED, this);
return;
}
HashMap<Pair<String,String>, MMyAccount> account_mapping = new HashMap<Pair<String,String>, MMyAccount>();
int max_changes = c.getCount();
TLongArrayList raw_data_ids = new TLongArrayList(max_changes);
TLongArrayList versions = new TLongArrayList(max_changes);
long new_max_data_id = highestDataIdAlreadySeen;
long new_max_contact_id = highestContactIdAlreadySeen;
TLongHashSet potentially_changed = new TLongHashSet();
try {
//the cursor points to a list of raw contact data items that may have changed
//the items will include a type specific field that we are interested in updating
//it is possible that multiple data item entries mention the same identifier
//so we build a list of contacts to update and then perform synchronization
//by refreshing given that we know the top level contact id.
if (DBG) Log.d(TAG, "Scanning " + c.getCount() + " contacts...");
while(c.moveToNext()) {
if (DBG) Log.v(TAG, "check for updates of contact " + c.getLong(0));
long raw_data_id = c.getLong(0);
long version = c.getLong(1);
long contact_id = c.getLong(2);
//if the contact was split or merged, then we get a higher contact id
//so if we have a higher id, data version doesnt really matter
if(contact_id <= highestContactIdAlreadySeen) {
//the data associated with this contact may not be dirty
//we just can't do the join against our table because thise
//api is implmented over the content provider
if(dbManager.getContactDataVersionManager().getVersion(raw_data_id) == version)
continue;
} else {
new_max_contact_id = Math.max(new_max_contact_id, contact_id);
}
raw_data_ids.add(raw_data_id);
versions.add(version);
potentially_changed.add(contact_id);
new_max_data_id = Math.max(new_max_data_id, raw_data_id);
}
if (DBG) Log.d(TAG, "Finished iterating over " + c.getCount() + " contacts for " + potentially_changed.size() + " candidates.");
} finally {
c.close();
}
if(potentially_changed.size() == 0) {
Log.w(TAG, "possible bug, woke up to update contacts, but no change was detected; there are extra wakes so it could be ok");
}
final SQLiteDatabase db = dbManager.getDatabase();
Pattern emailPattern = getEmailPattern();
Pattern numberPattern = getNumberPattern();
//slice it up so we don't use too much system resource on keeping a lot of state in memory
int total = potentially_changed.size();
sAddressBookTotal = total;
sAddressBookPosition = 0;
final TLongArrayList slice_of_changed = new TLongArrayList(BATCH_SIZE);
final StringBuilder to_fetch = new StringBuilder();
final HashMap<Pair<String, String>, TLongHashSet> ids_for_account =
new HashMap<Pair<String,String>, TLongHashSet>();
final TLongObjectHashMap<String> names = new TLongObjectHashMap<String>();
TLongIterator it = potentially_changed.iterator();
for(int i = 0; i < total && it.hasNext();) {
sAddressBookPosition = i;
if (BootstrapActivity.isBootstrapped()) {
try {
Thread.sleep(mSleepTime * SLEEP_SCALE);
} catch(InterruptedException e) {}
}
slice_of_changed.clear();
ids_for_account.clear();
names.clear();
int max = i + BATCH_SIZE;
for(; i < max && it.hasNext(); ++i) {
slice_of_changed.add(it.next());
}
if(DBG) Log.v(TAG, "looking up names ");
to_fetch.setLength(0);
to_fetch.append(ContactsContract.Contacts._ID + " IN ");
SQLClauseHelper.appendArray(to_fetch, slice_of_changed.iterator());
//lookup the fields we care about from a user profile perspective
c = mContext.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI,
new String[] {
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
},
to_fetch.toString(),
null,
null
);
try {
while(c.moveToNext()) {
long id = c.getLong(0);
String name = c.getString(1);
if(name == null)
continue;
//reject names that are just the email address or are just a number
//the default for android is just to propagate this as the name
//if there is no name
if(emailPattern.matcher(name).matches() || numberPattern.matcher(name).matches())
continue;
names.put(id, name);
}
} finally {
c.close();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
db.beginTransactionNonExclusive();
} else {
db.beginTransaction();
}
long before = SystemClock.elapsedRealtime();
SliceUpdater updater = new SliceUpdater(dbManager, slice_of_changed, ids_for_account, names, account_type_selection);
long after = SystemClock.elapsedRealtime();
mSleepTime = (mSleepTime + after - before) / 2;
slice_of_changed.forEach(updater);
profileDataChanged |= updater.profileDataChanged;
identityAdded |= updater.identityAdded;
db.setTransactionSuccessful();
db.endTransaction();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
db.beginTransactionNonExclusive();
} else {
db.beginTransaction();
}
//add all detected members to account feed
for(Entry<Pair<String, String>, TLongHashSet> e : ids_for_account.entrySet()) {
Pair<String, String> k = e.getKey();
TLongHashSet v = e.getValue();
MMyAccount cached_account = account_mapping.get(k);
if(cached_account == null) {
cached_account = lookupOrCreateAccount(dbManager, k.getValue0(), k.getValue1());
prepareAccountWhitelistFeed(dbManager.getMyAccountManager(), dbManager.getFeedManager(), cached_account);
account_mapping.put(k, cached_account);
}
final MMyAccount account = cached_account;
v.forEach(new TLongProcedure() {
@Override
public boolean execute(long id) {
dbManager.getFeedManager().ensureFeedMember(account.feedId_, id);
db.yieldIfContendedSafely(75);
return true;
}
});
}
db.setTransactionSuccessful();
db.endTransaction();
}
sAddressBookTotal = sAddressBookPosition = 0;
//TODO: handle deleted
//for all android data ids in our table, check if they still exist in the
//contacts table, probably in batches of 100 or something. if they don't
//null them out. this is annoyingly non-differential.
//TODO: adding friend should update accepted feed status, however,
//if a crashe happens for whatever reason, then its possible that this may need to
//be run for identities which actually exist in the db. so this update code
//needs to do the feed accepted status change for all users that were touched
//by the profile update process
//update the version ids so we can be faster on subsequent runs
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
db.beginTransactionNonExclusive();
} else {
db.beginTransaction();
}
int changed_data_rows = raw_data_ids.size();
for(int i = 0; i < changed_data_rows; ++i) {
dbManager.getContactDataVersionManager().setVersion(raw_data_ids.get(i), versions.get(i));
}
db.setTransactionSuccessful();
db.endTransaction();
dbManager.getContactDataVersionManager().setMaxDataIdSeen(new_max_data_id);
dbManager.getContactDataVersionManager().setMaxContactIdSeen(new_max_contact_id);
ContentResolver resolver = mContext.getContentResolver();
Date end = new Date();
double time = end.getTime() - start.getTime();
time /= 1000;
Log.w(TAG, "update address book " + mChangeCount++ + " took " + time + " seconds");
if (identityAdded) {
//wake up the profile push
resolver.notifyChange(MusubiService.WHITELIST_APPENDED, this);
}
if (profileDataChanged) {
//refresh the ui...
resolver.notifyChange(MusubiService.PRIMARY_CONTENT_CHANGED, this);
}
if (identityAdded || profileDataChanged) {
//update the our musubi address book as needed.
String accountName = mContext.getString(R.string.account_name);
String accountType = mContext.getString(R.string.account_type);
Account account = new Account(accountName, accountType);
ContentResolver.requestSync(account, ContactsContract.AUTHORITY, new Bundle());
}
dbManager.close();
mLastRun = new Date().getTime();
resolver.notifyChange(MusubiService.ADDRESS_BOOK_SCANNED, this);
}
private static MMyAccount lookupOrCreateAccount(DatabaseManager dbManager, String accountName,
String accountType) {
IBIdentity ibid;
//TODO: this needs to support handling other account types better
ibid = new IBIdentity(Authority.Email, accountName, 0);
MMyAccount cached_account = dbManager.getMyAccountManager()
.lookupAccount(accountName, accountType);
if (cached_account == null) {
cached_account = new MMyAccount();
cached_account.accountName_ = accountName;
cached_account.accountType_ = accountType;
MIdentity existingId = dbManager.getIdentitiesManager()
.getIdentityForIBHashedIdentity(ibid);
if (existingId != null) {
cached_account.identityId_ = existingId.id_;
}
dbManager.getMyAccountManager().insertAccount(cached_account);
}
return cached_account;
}
private static void prepareAccountWhitelistFeed(MyAccountManager am,
FeedManager fm, MMyAccount account) {
if(account.feedId_ == null) {
MFeed feed = new MFeed();
feed.accepted_ = false; //not visible
feed.type_ = MFeed.FeedType.ASYMMETRIC;
feed.name_ = MFeed.LOCAL_WHITELIST_FEED_NAME;
fm.insertFeed(feed);
account.feedId_ = feed.id_;
am.updateAccount(account);
}
}
private static String getAccountSelectionString() {
String account_type_selection = BASE_ACCOUNT_TYPES_SELECTION;
if(SYNC_FACEBOOK)
account_type_selection += FACEBOOK_ACCOUNT_TYPES_SELECTION;
if(SYNC_PHONE)
account_type_selection += PHONE_ACCOUNT_TYPES_SELECTION;
if(SYNC_TWITTER)
account_type_selection += TWITTER_ACCOUNT_TYPES_SELECTION;
return account_type_selection;
}
class SliceUpdater implements TLongProcedure {
public boolean profileDataChanged;
TLongObjectHashMap<String> names;
TLongArrayList slice_of_changed;
HashMap<Pair<String, String>, TLongHashSet> ids_for_account;
public boolean identityAdded;
private final DatabaseManager mDatabaseManager;
int i = 0;
// Member variables used to avoid allocations across calls to execute()
private final String[] mAccountColumns;
private final String mAccountSelection;
private final String[] mAccountSelectionArgs;
private SliceUpdater(DatabaseManager dbManager, TLongArrayList slice_of_changed,
HashMap<Pair<String, String>, TLongHashSet> ids_for_account,
TLongObjectHashMap<String> names, String accountSelection) {
mDatabaseManager = dbManager;
this.ids_for_account = ids_for_account;
this.slice_of_changed = slice_of_changed;
this.names = names;
mAccountColumns = new String[] {
ContactsContract.Data.DATA1,
ContactsContract.RawContacts.ACCOUNT_NAME,
ContactsContract.RawContacts.ACCOUNT_TYPE,
ContactsContract.Data.MIMETYPE,
};
mAccountSelection = ContactsContract.Data.CONTACT_ID + "=?" + " AND (" +
accountSelection + ")";
mAccountSelectionArgs = new String[1];
}
@Override
public boolean execute(long contact_id) {
//for all types of identity
//- ensure the row exists
//- ensure the linked android id equals the value of this contact
//- update the profile fields
mAccountSelectionArgs[0] = String.valueOf(contact_id);
Cursor c = mContext.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
mAccountColumns, mAccountSelection, mAccountSelectionArgs, null);
try {
while(c.moveToNext()) {
String type = c.getString(3);
String principal = c.getString(0);
String accountName = c.getString(1);
String accountType = c.getString(2);
if(accountName == null) {
accountName = "null-account-name";
}
if(accountType == null) {
accountType = "null-account-type";
}
IBIdentity id = ibIdentityForData(type, principal);
if (id == null) {
continue;
}
if(DBG) Log.v(TAG, "updating contact " + contact_id);
//lookup the fields we care about from a user profile perspective
String display_name = names.get(contact_id);
byte[] photo_thumbnail = null;
Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contact_id);
InputStream is = ContactsContract.Contacts.openContactPhotoInputStream(mContext.getContentResolver(), uri);
if (is != null) {
if(DBG) Log.v(TAG, "importing photo for " + display_name);
try {
photo_thumbnail = IOUtils.toByteArray(is);
} catch (IOException e) {
Log.e(TAG, "photo thumbnail failed to serialize", e);
} finally {
try {
is.close();
} catch(IOException e) {}
}
}
MIdentity ident = ensureIdentity(contact_id, display_name, photo_thumbnail, id, mDatabaseManager);
Pair<String, String> k = Pair.with(accountName, accountType);
TLongHashSet ids = ids_for_account.get(k);
if(ids == null) {
ids = new TLongHashSet();
ids_for_account.put(k, ids);
}
ids.add(ident.id_);
mDatabaseManager.getDatabase().yieldIfContendedSafely(100);
}
} finally {
c.close();
}
return true;
}
MIdentity ensureIdentity(long contact_id, String display_name, byte[] photo_thumbnail,
IBIdentity id, DatabaseManager dbManager) {
MIdentity ident = dbManager.getIdentitiesManager().getIdentityForIBHashedIdentity(id);
boolean changed = false;
boolean insert = false;
boolean picture_changed = false;
if(ident == null) {
ident = new MIdentity();
insert = true;
//stuff that lets us reach them
ident.type_ = id.authority_;
ident.principal_ = id.principal_;
ident.principalHash_ = id.hashed_;
ident.principalShortHash_ = Util.shortHash(id.hashed_);
//stuff that makes them pretty
ident.name_ = display_name;
ident.thumbnail_ = photo_thumbnail;
}
//This is a little weird because there may be several contacts that update this one
//so its possible for it to go through a sequence of changes before it settles on
//one. We could defer all the updates until we knew for sure there was a change...
//but this seems okay until we look much more closely later
//if the identity is new, there couldnt possibly any feeds to accept
boolean accept_feeds = false;
//the main strategy here is to update the field we use for display/queries
//if the musubi version hasnt been populated
//TODO: in the future, maybe take the newest across all services or something
//like that?
if(!ident.whitelisted_) {
changed = true;
//dont' change the blocked flag here, because it could only have
//been set through explicit user interaction
ident.whitelisted_ = true;
accept_feeds = true;
identityAdded = true;
}
if(ident.androidAggregatedContactId_ == null || contact_id != ident.androidAggregatedContactId_ || ident.androidAggregatedContactId_ != contact_id) {
ident.androidAggregatedContactId_ = contact_id;
changed = true;
}
if(display_name != null && (ident.name_ == null || !ident.name_.equals(display_name))) {
changed = true;
ident.name_ = display_name;
}
//TODO: is there a way to detect if the thumbnail actually changed?
if(photo_thumbnail != null && (ident.thumbnail_ == null || !Arrays.equals(ident.thumbnail_, photo_thumbnail))) {
picture_changed = true;
ident.thumbnail_ = photo_thumbnail;
}
if(insert) {
dbManager.getIdentitiesManager().insertIdentity(ident);
} else if(picture_changed || changed) {
if(picture_changed) {
dbManager.getIdentitiesManager().updateThumbnail(ident);
mContactThumbnailCache.invalidate(ident.id_);
}
if(changed) {
dbManager.getIdentitiesManager().updateIdentity(ident);
}
profileDataChanged = true;
}
if(accept_feeds) {
dbManager.getFeedManager().acceptFeedsFromMember(mContext, ident.id_);
}
return ident;
}
}
public static AddressBookUpdateHandler newInstance(Context context, SQLiteOpenHelper dbh, ContentResolver resolver) {
HandlerThread thread = new HandlerThread("AddressBookUpdateThread");
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
thread.start();
AddressBookUpdateHandler abuh = new AddressBookUpdateHandler(context, dbh, thread, resolver);
return abuh;
}
static IBIdentity ibIdentityForData(String type, String principal) {
if(type.equals(CommonDataKinds.Email.CONTENT_ITEM_TYPE)) {
if(!SYNC_EMAIL)
return null;
//TODO: canonicalizing emails like gmail? e.g.
//. isn't really considered part of the address,
//+after either
//TODO: filter out this data item if it looks like a
//mailing list or common corporate sending address
return new IBIdentity(Authority.Email, principal, 0);
} else if(type.equals(CommonDataKinds.Phone.CONTENT_ITEM_TYPE)){
if(!SYNC_PHONE)
return null;
//TODO: phone number/sms support for server
//TODO: phone number must be canonicalized
return new IBIdentity(Authority.PhoneNumber, principal, 0);
} else if(type.equals(FACEBOOK_MIMETYPE)){
if(!SYNC_FACEBOOK)
return null;
return new IBIdentity(Authority.Facebook, principal, 0);
} else if(type.equals(SYNC_TWITTER)){
if(!SYNC_TWITTER)
return null;
//TODO: twitter support
//TODO: sync doesnt really work on phone for the twitter app for me (TJ)
return new IBIdentity(Authority.Twitter, principal, 0);
} else {
return null;
}
}
public static class AddressBookImportTask extends AsyncTask<Void, String, Void> {
final String NOTICE = "\n\nYour privacy is important. Musubi never uploads your contacts from your device.";
final Context mContext;
boolean mNeedsFriends = true;
ProgressDialog mDialog;
public AddressBookImportTask(Context context) {
mContext = context;
}
@Override
protected void onPreExecute() {
/*mDialog = new ProgressDialog(mContext);
mDialog.setTitle("Preparing friend list...");
mDialog.setIndeterminate(true);
mDialog.setCancelable(true);
mDialog.show();*/
}
@Override
protected Void doInBackground(Void... params) {
publishProgress("Scanning address book for friends.");
ContentResolver resolver = mContext.getContentResolver();
Uri friendPoint = MusubiService.ADDRESS_BOOK_SCANNED;
ContentObserver friends = new ContentObserver(new Handler(
mContext.getMainLooper())) {
@Override
public void onChange(boolean selfChange) {
mNeedsFriends = false;
mContext.getContentResolver().unregisterContentObserver(this);
}
};
resolver.registerContentObserver(friendPoint, false, friends);
resolver.notifyChange(MusubiService.REQUEST_ADDRESS_BOOK_SCAN, null);
while (mNeedsFriends) {
int contacts = AddressBookUpdateHandler.sAddressBookTotal
- AddressBookUpdateHandler.sAddressBookPosition;
if (contacts > 0) {
publishProgress("Adding " + contacts
+ " friends from address book...");
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
return null;
}
@Override
protected void onProgressUpdate(String... values) {
//mDialog.setMessage(values[0] + NOTICE);
//Log.d(TAG, "-- " + values[0]);
}
@Override
protected void onPostExecute(Void result) {
// XXX hack to reconnect accounts.
mContext.getContentResolver().notifyChange(MusubiService.AUTH_TOKEN_REFRESH, null);
//mDialog.dismiss();
}
}
public static void importFullAddressBook(Context context) {
Log.d(TAG, "doing full import");
SQLiteOpenHelper db = App.getDatabaseSource(context);
DatabaseManager dbm = new DatabaseManager(context);
IdentitiesManager idm = dbm.getIdentitiesManager();
FeedManager fm = dbm.getFeedManager();
MyAccountManager am = dbm.getMyAccountManager();
long startTime = System.currentTimeMillis();
String musubiAccountType = context.getString(R.string.account_type);
long maxDataId = -1;
long maxContactId = -1;
assert(SYNC_EMAIL);
String account_type_selection = getAccountSelectionString();
Cursor c = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
new String[] {
ContactsContract.Data.CONTACT_ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Data._ID,
ContactsContract.Data.DATA_VERSION,
ContactsContract.Data.DATA1,
ContactsContract.Data.MIMETYPE,
ContactsContract.RawContacts.ACCOUNT_NAME,
ContactsContract.RawContacts.ACCOUNT_TYPE
},
"(" +
ContactsContract.RawContacts.ACCOUNT_TYPE + "<>'" + musubiAccountType + "'" +
") AND (" +
NAME_OR_OTHER_SELECTION + account_type_selection +
")", // All known contacts.
null, null
);
if (c == null) {
Log.e(TAG, "no valid cursor", new Throwable());
return;
}
sAddressBookTotal = c.getCount();
sAddressBookPosition = 0;
Log.d(TAG, "Scanning contacts...");
final Map<String, MMyAccount> myAccounts = new HashMap<String, MMyAccount>();
final Pattern emailPattern = getEmailPattern();
final Pattern numberPattern = getNumberPattern();
while (c.moveToNext()) {
sAddressBookPosition++;
String identityType = c.getString(5);
String identityPrincipal = c.getString(4);
long contactId = c.getLong(0);
long dataId = c.getLong(2);
String displayName = c.getString(1);
byte[] thumbnail = null;
String accountName = c.getString(6);
String accountType = c.getString(7);
if(accountName == null) {
accountName = "null-account-name";
}
if(accountType == null) {
accountType = "null-account-type";
}
String accountKey = accountName + "-" + accountType;
MMyAccount myAccount = myAccounts.get(accountKey);
if (myAccount == null) {
myAccount = lookupOrCreateAccount(dbm, accountName, accountType);
prepareAccountWhitelistFeed(am, fm, myAccount);
myAccounts.put(accountKey, myAccount);
}
if (displayName == null
|| emailPattern.matcher(displayName).matches()
|| numberPattern.matcher(displayName).matches()) {
continue;
}
IBIdentity ibid = ibIdentityForData(identityType, identityPrincipal);
if (ibid == null) {
//TODO: better selection
//Log.d(TAG, "skipping " + displayName + " // " + identityPrincipal);
continue;
}
Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId);
InputStream is = ContactsContract.Contacts.openContactPhotoInputStream(context.getContentResolver(), uri);
if (is != null) {
//Log.d(TAG, "importing photo for " + displayName);
try {
thumbnail = IOUtils.toByteArray(is);
} catch (IOException e) {
thumbnail = null;
//Log.e(TAG, "photo thumbnail failed to serialize", e);
} finally {
try {
is.close();
} catch(IOException e) {}
}
} else {
thumbnail = null;
}
MIdentity ident = addIdentity(context, idm, contactId, displayName, thumbnail, ibid);
if (ident != null) {
fm.ensureFeedMember(myAccount.feedId_, ident.id_);
fm.acceptFeedsFromMember(context, ident.id_);
}
maxDataId = Math.max(maxDataId, dataId);
maxContactId = Math.max(maxContactId, contactId);
}
c.close();
long timeTaken = System.currentTimeMillis() - startTime;
ContactDataVersionManager cdvm = new ContactDataVersionManager(db);
cdvm.setMaxDataIdSeen(maxDataId);
cdvm.setMaxContactIdSeen(maxContactId);
Log.d(TAG, "full import took " + timeTaken / 1000 + " secs");
context.getContentResolver().notifyChange(MusubiService.ADDRESS_BOOK_SCANNED, null);
}
/**
* Returns the newly created identity or null if no identity was created.
*/
static MIdentity addIdentity(Context context, IdentitiesManager idm,
long contactId, String displayName, byte[] photoThumbnail, IBIdentity id) {
// TODO: in memory lookup for full import?
MIdentity ident = idm.getIdentityForIBHashedIdentity(id);
if(ident != null) {
return null;
}
ident = new MIdentity();
ident.whitelisted_ = true;
ident.type_ = id.authority_;
ident.principal_ = id.principal_;
ident.principalHash_ = id.hashed_;
ident.principalShortHash_ = Util.shortHash(id.hashed_);
//stuff that makes them pretty
ident.name_ = displayName;
ident.thumbnail_ = photoThumbnail;
ident.androidAggregatedContactId_ = contactId;
idm.insertIdentity(ident);
return ident;
}
}