com.oxplot.contactphotosync.AssignContactPhotoActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.oxplot.contactphotosync.AssignContactPhotoActivity.java

Source

/**
 * AssignContactPhotoActivity.java - Assign photos to contacts of a
 *                                   specific Google account.
 * 
 * Copyright (C) 2012 Mansour <mansour@oxplot.com>
 * All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
    
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
    
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 */
package com.oxplot.contactphotosync;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import android.accounts.Account;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningAppProcessInfo;
import android.app.ProgressDialog;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.Intent;
import android.content.OperationApplicationException;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.provider.MediaStore.Images.Media;
import android.support.v4.app.NavUtils;
import android.support.v4.app.TaskStackBuilder;
import android.util.Log;
import android.util.SparseArray;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView.MultiChoiceModeListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

/**
 * Provides a similar interface to android contacts app allowing user to assign
 * photos to contacts. The reason for re-implementing this UI is to work around
 * the android API which limits certain aspects of dimension and quality of the
 * saved photo. In our app with root access, we can save any image file we wish
 * as contact a photo.
 */
public class AssignContactPhotoActivity extends Activity {

    /**
     * Debugging tag
     */
    private static final String TAG = "AssignContactPhoto";

    /**
     * JPEG quality of thumbnails saved to app's disk cache.
     */
    private static final int THUMB_QUALITY = 90;

    /**
     * Request code for starting activity to pick an image file.
     */
    private static final int REQ_CODE_PICK_IMAGE = 88;

    /**
     * Request code for starting the crop activity.
     */
    private static final int REQ_CODE_CROP_IMAGE = 99;

    /**
     * Maximum number of thumbnails kept in memory cache above which the cache is
     * completely flushed.
     */
    private static final int THUMB_MEM_CACHE_LIMIT = 100;

    /**
     * Gmail group id for "My Contacts" group which contains all the contacts that
     * have been specifically added by the user as opposed to those that are
     * automatically added by Gmail.
     */
    private static final String MY_CONTACTS_GROUP = "6";

    /**
     * Directory name under application's cache directory for storing contact
     * thumbnails. The thumbnails are used to speed up the viewing of contact
     * photos once the app is first resumed.
     */
    private static final String DISK_CACHE_DIR = "thumbcache";

    /**
     * Google account type.
     */
    private static final String ACCOUNT_TYPE = "com.google";

    /**
     * Contact photo data authority. It is used here to enable sync for our app
     * through the UI.
     */
    private static final String CONTACT_PHOTO_AUTHORITY = "com.oxplot.contactphotos";

    /**
     * Contact data authority. It is used here to disable contact sync while the
     * UI is running to prevent contact sync which may be interrupted by the
     * repetitive killing of Contacts Provider.
     */
    private static final String CONTACTS_AUTHORITY = "com.android.contacts";

    /**
     * Placeholder used to indicate that thumbnail for a contact is not modified
     * and hence should not be updated on the UI.
     */
    private Drawable unchangedThumb;

    /**
     * Account name (aka email address) of the Google account for which the
     * contacts are shown.
     */
    private String account;

    /**
     * Main UI list element on the screen.
     */
    private ListView contactList;

    /**
     * A text message shown when there are no contacts for the current account.
     */
    private TextView emptyList;

    /**
     * Progress bar shown while contacts are loading.
     */
    private ProgressBar loadingProgress;

    /**
     * Task that loads list of contacts for the current account.
     */
    private LoadContactsTask contactsLoader;

    /**
     * In memory cache of thumbnails used to speed up rendering contact photos in
     * the list.
     */
    private SparseArray<Drawable> thumbMemCache;

    /**
     * Async tasks currently running (mostly thumbnails retrievers).
     */
    private Set<AsyncTask<?, ?, ?>> asyncTasks;

    /**
     * The default thumbnail for when a contact is lacking a photo.
     */
    private Drawable defaultThumb;

    /**
     * Raw contact ID of a contact for which a new photo is being picked. This is
     * set after a contact is selected and is used in StoreImageTask after
     * CropPhotoActivity has successfully finished.
     */
    private int pickedRawContact;

    /**
     * Saves the photo that has been cropped by CropPhotoActivity as contact
     * photo.
     */
    private StoreImageTask storeImageTask;

    /**
     * Indicates if Google contact sync was ticked in preferences prior to
     * resumption of this activity.
     */
    private boolean contactsSyncAuto;

    /**
     * Indicates if Google contact photo sync was ticked in preferences prior to
     * resumption of this activity.
     */
    private boolean contactPhotoSyncAuto;

    /**
     * Width and height of a thumbnail.
     */
    private int thumbSize;

    /**
     * Temporary location for saving cropped photos. This location is turned into
     * a URI and passed to CropPhotoActivity.
     */
    private File cropTemp;

    @Override
    public void onCreate(Bundle savedState) {
        super.onCreate(savedState);

        thumbSize = getResources().getInteger(R.integer.config_list_thumb_size);

        if (savedState != null) {
            String cropTempPath = savedState.getString("crop_temp");
            if (cropTempPath != null)
                cropTemp = new File(cropTempPath);
            pickedRawContact = savedState.getInt("picked_raw_contact", 0);
        }

        // Initialize cache directory

        new File(getCacheDir(), DISK_CACHE_DIR).mkdir();

        unchangedThumb = new BitmapDrawable(getResources(), Bitmap.createBitmap(1, 1, Config.ALPHA_8));

        setContentView(R.layout.activity_assign_contact_photo);
        contactList = (ListView) findViewById(R.id.contactList);
        emptyList = (TextView) findViewById(R.id.empty);
        loadingProgress = (ProgressBar) findViewById(R.id.loading);

        contactList.setEmptyView(loadingProgress);
        contactList.setAdapter(new ContactAdapter());
        contactList.setDividerHeight(1);
        contactList.setMultiChoiceModeListener(new ContactListMultiChoiceModeListener());
        contactList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);

        getActionBar().setDisplayHomeAsUpEnabled(true);
        account = getIntent().getStringExtra("account");
        setTitle(account);

        thumbMemCache = new SparseArray<Drawable>();
        asyncTasks = new HashSet<AsyncTask<?, ?, ?>>();
        defaultThumb = getResources().getDrawable(R.drawable.new_picture);

        contactList.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> arg0, View view, int position, long id) {

                pickedRawContact = ((Contact) contactList.getItemAtPosition(position)).rawContactId;
                Intent intent = new Intent();

                // We only really accept PNG and JPEG but the activities registered for
                // the intended action only accept the generic form of the mime type. We
                // will check for our constraints after the image is picked.

                intent.setType("image/*");
                intent.setAction(Intent.ACTION_GET_CONTENT);
                startActivityForResult(
                        Intent.createChooser(intent, getResources().getString(R.string.select_picture)),
                        REQ_CODE_PICK_IMAGE);

            }
        });

    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        if (cropTemp != null)
            outState.putString("crop_temp", cropTemp.getAbsolutePath());
        outState.putInt("picked_raw_contact", pickedRawContact);
    }

    /**
     * Stores the given {@link Drawable} for raw contact with <code>id</code> to
     * in-memory cache. If the cache is already full (ie
     * {@link THUMB_MEM_CACHE_LIMIT} number of elements or more), the cache is
     * flushed completely and <code>d</code> added.
     * 
     * @param id
     *          Raw contact Id
     * @param d
     *          Thumbnail to store
     */
    private void putToThumbMemCache(int id, Drawable d) {
        // XXX very naive way of doing this
        if (thumbMemCache.size() >= THUMB_MEM_CACHE_LIMIT)
            thumbMemCache.clear();
        thumbMemCache.put(id, d);
    }

    /**
     * Removes a thumbnail from disk cache.
     * 
     * @param id
     *          Raw contact Id
     */
    private void removeDiskCache(int id) {
        new File(new File(getCacheDir(), DISK_CACHE_DIR), "" + id).delete();
    }

    /**
     * Stores <code>bitmap</code> as thumbnail for the given contact with raw
     * contact <code>id</code> in app's disk cache.
     * 
     * @param id
     *          Raw contact id.
     * @param bitmap
     *          Thumbnail
     */
    private void writeDiskCache(int id, Bitmap bitmap) {
        File file = new File(new File(getCacheDir(), DISK_CACHE_DIR), "" + id);
        FileOutputStream stream = null;
        try {
            stream = new FileOutputStream(file);
            bitmap.compress(CompressFormat.JPEG, THUMB_QUALITY, stream);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (stream != null)
                try {
                    stream.close();
                } catch (IOException e) {
                }
        }
    }

    /**
     * Loads thumbnail from disk cache for the given contact.
     * 
     * @param id
     *          Raw contact Id
     * @return Thumbnail otherwise <code>null</code> if no thumbnail exists for
     *         the contact or if an error occurs while loading.
     */
    private Bitmap readDiskCache(int id) {
        File file = new File(new File(getCacheDir(), DISK_CACHE_DIR), "" + id);
        FileInputStream stream = null;
        Bitmap result = null;
        try {
            stream = new FileInputStream(file);
            result = BitmapFactory.decodeStream(stream);
        } catch (IOException e) {
        } finally {
            if (stream != null)
                try {
                    stream.close();
                } catch (IOException e) {
                }
        }
        return result;
    }

    protected void onActivityResult(int requestCode, int resultCode, Intent imageReturnedIntent) {
        super.onActivityResult(requestCode, resultCode, imageReturnedIntent);

        switch (requestCode) {
        case REQ_CODE_CROP_IMAGE:

            // The photo is cropped. Kick off the store image task to save it to the
            // appropriate contact.

            if (resultCode == RESULT_OK) {
                storeImageTask = new StoreImageTask();
                storeImageTask.execute();
            } else {
                cropTemp.delete();
            }
            break;

        case REQ_CODE_PICK_IMAGE:

            if (resultCode != RESULT_OK)
                return;

            // The image is picked by the user. Query its MIME type.

            Uri selectedImage = imageReturnedIntent.getData();

            Cursor cursor = getContentResolver().query(selectedImage, new String[] { Media.MIME_TYPE }, null, null,
                    null);

            if (cursor == null) {
                Toast.makeText(this, getResources().getString(R.string.something_went_wrong), Toast.LENGTH_LONG)
                        .show();
                return;
            }

            String mimeType = "";

            try {
                cursor.moveToFirst();
                mimeType = cursor.getString(cursor.getColumnIndex(Media.MIME_TYPE));
            } finally {
                cursor.close();
            }

            // Currently, we only accept PNG and JPEG images. Puke on anything else.

            if (!"image/jpeg".equals(mimeType) && !"image/png".equals(mimeType)) {
                Toast.makeText(this, getResources().getString(R.string.only_image_allowed), Toast.LENGTH_LONG)
                        .show();
                return;
            }

            // Create a temporary file to pass to CropPhotoActivity for saving the
            // cropped photo.

            try {
                cropTemp = File.createTempFile("croptemp-", "", getCacheDir());
            } catch (IOException e) {
                Toast.makeText(this, getResources().getString(R.string.something_went_wrong), Toast.LENGTH_LONG)
                        .show();
                return;
            }

            // Fire up the cropper.

            Intent intent = new Intent(this, CropPhotoActivity.class);
            intent.setData(selectedImage);
            intent.putExtra("wratio", 1.0f);
            intent.putExtra("hratio", 1.0f);
            intent.putExtra("maxwidth", getResources().getInteger(R.integer.config_max_photo_dim));
            intent.putExtra("maxheight", getResources().getInteger(R.integer.config_max_photo_dim));
            // intent.putExtra("quality", 0);
            intent.putExtra("out", Uri.fromFile(cropTemp));
            startActivityForResult(intent, REQ_CODE_CROP_IMAGE);

        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_assign_contact_photo, menu);
        return true;
    }

    @SuppressWarnings("unchecked")
    @Override
    public boolean onMenuItemSelected(int featureId, MenuItem item) {
        switch (item.getItemId()) {

        case android.R.id.home:
            Intent upIntent = new Intent(this, SelectAccountActivity.class);
            if (NavUtils.shouldUpRecreateTask(this, upIntent)) {
                TaskStackBuilder.create(this).addNextIntent(upIntent).startActivities();
            } else {
                NavUtils.navigateUpTo(this, upIntent);
            }
            return true;

        case R.id.menu_sync_now:

            // This is a convenient way of enabling and running the contact photo
            // sync.

            Account a = new Account(account, ACCOUNT_TYPE);
            ContentResolver.setSyncAutomatically(a, CONTACT_PHOTO_AUTHORITY, true);
            ContentResolver.requestSync(a, CONTACT_PHOTO_AUTHORITY, new Bundle());
            Toast.makeText(this, getResources().getString(R.string.sync_requested), Toast.LENGTH_LONG).show();
            break;

        case R.id.menu_download_all:
        case R.id.menu_upload_all:
            new DownloadUploadTask(item.getItemId() == R.id.menu_download_all ? DownloadUploadTask.TYPE_DOWNLOAD
                    : DownloadUploadTask.TYPE_UPLOAD)
                            .execute(((ContactAdapter) contactList.getAdapter()).getBackingList());

            break;

        case R.id.menu_refresh:

            // XXX This is ugly and hackish. This of course doesn't stop us from being
            // lazy and using it here.

            onPause();
            onResume();
            break;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    protected void onDestroy() {
        if (storeImageTask != null)
            storeImageTask.cancel(true);
        super.onDestroy();
    }

    @Override
    protected void onPause() {

        // XXX experimentally, we're gonna disable contacts sync so it doesn't
        // interfere with our evil root plans - and here we restore it
        // Account a = new Account(account, ACCOUNT_TYPE);
        // ContentResolver.setSyncAutomatically(a, CONTACTS_AUTHORITY,
        // contactsSyncAuto);
        // ContentResolver.setSyncAutomatically(a, CONTACT_PHOTO_AUTHORITY,
        // contactPhotoSyncAuto);

        thumbMemCache.clear();
        if (contactsLoader != null)
            contactsLoader.cancel(false);
        for (AsyncTask<?, ?, ?> lt : asyncTasks)
            lt.cancel(false);
        asyncTasks.clear();
        super.onPause();
    }

    @Override
    protected void onResume() {

        super.onResume();
        contactsLoader = new LoadContactsTask();
        contactsLoader.execute(account);

        // XXX experimentally, we're gonna disable contacts sync so it doesn't
        // interfere with our evil root plans
        // Account a = new Account(account, ACCOUNT_TYPE);
        // contactsSyncAuto = ContentResolver.getSyncAutomatically(a,
        // CONTACTS_AUTHORITY);
        // contactPhotoSyncAuto = ContentResolver.getSyncAutomatically(a,
        // CONTACT_PHOTO_AUTHORITY);
        // ContentResolver.setSyncAutomatically(a, CONTACTS_AUTHORITY, false);
        // ContentResolver.setSyncAutomatically(a, CONTACT_PHOTO_AUTHORITY, false);
    }

    /**
     * Loads a single thumbnail on a background thread.
     */
    private class LoadThumbTask extends AsyncTask<Integer, Void, Drawable> {

        /**
         * Raw contact ID of the contact to load the thumbnail for.
         */
        private int rawContactId;

        @Override
        protected Drawable doInBackground(Integer... params) {
            AssetFileDescriptor fd = null;
            InputStream is = null;
            BitmapFactory.Options opts;

            rawContactId = params[0];
            BitmapDrawable result = null;
            Uri rawContactPhotoUri = Uri.withAppendedPath(
                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
                    RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
            try {

                // Get the bounds for later resampling

                fd = getContentResolver().openAssetFileDescriptor(rawContactPhotoUri, "r");
                is = fd.createInputStream();
                opts = new Options();
                opts.inJustDecodeBounds = true;
                BitmapFactory.decodeStream(is, null, opts);
                is.close();
                fd.close();

                opts.inSampleSize = opts.outHeight / thumbSize;
                opts.inSampleSize = opts.inSampleSize < 1 ? 1 : opts.inSampleSize;
                opts.inJustDecodeBounds = false;
                fd = getContentResolver().openAssetFileDescriptor(rawContactPhotoUri, "r");
                is = fd.createInputStream();

                Bitmap bitmap = BitmapFactory.decodeStream(is, null, opts);
                if (bitmap == null)
                    return unchangedThumb;

                result = new BitmapDrawable(getResources(), bitmap);

                return result;

            } catch (FileNotFoundException e) {
                return null;
            } catch (IOException e) {
                return null;
            } finally {
                if (is != null)
                    try {
                        is.close();
                    } catch (IOException e) {
                    }
                if (fd != null)
                    try {
                        fd.close();
                    } catch (IOException e) {
                    }
            }
        }

        @Override
        protected void onPostExecute(Drawable result) {
            asyncTasks.remove(this);
            if (result == null) {
                putToThumbMemCache(rawContactId, defaultThumb);
                removeDiskCache(rawContactId);
            } else if (result != unchangedThumb) {
                putToThumbMemCache(rawContactId, result);
                writeDiskCache(rawContactId, ((BitmapDrawable) result).getBitmap());
            }
            ((ContactAdapter) contactList.getAdapter()).notifyDataSetChanged();
        }
    }

    private class LoadContactsTask extends AsyncTask<String, Void, List<Contact>> {

        @Override
        protected List<Contact> doInBackground(String... params) {
            Uri groupsUri = ContactsContract.Groups.CONTENT_URI.buildUpon()
                    .appendQueryParameter(RawContacts.ACCOUNT_NAME, params[0])
                    .appendQueryParameter(RawContacts.ACCOUNT_TYPE, ACCOUNT_TYPE).build();

            Cursor cursor = getContentResolver().query(groupsUri, null, null, null, null);
            if (cursor == null)
                return null;

            int myContactGroupId = -1;

            try {
                for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {

                    String sourceId = cursor.getString(cursor.getColumnIndex(ContactsContract.Groups.SOURCE_ID));
                    if (MY_CONTACTS_GROUP.equals(sourceId)) {
                        myContactGroupId = cursor.getInt(cursor.getColumnIndex(ContactsContract.Groups._ID));
                        break;
                    }
                }
            } finally {
                cursor.close();
            }

            if (myContactGroupId >= 0) {
                Uri contactsUri = ContactsContract.Data.CONTENT_URI.buildUpon()
                        .appendQueryParameter(RawContacts.ACCOUNT_NAME, params[0])
                        .appendQueryParameter(RawContacts.ACCOUNT_TYPE, ACCOUNT_TYPE).build();
                cursor = getContentResolver().query(contactsUri,
                        new String[] { GroupMembership.DISPLAY_NAME, GroupMembership.RAW_CONTACT_ID },
                        GroupMembership.GROUP_ROW_ID + " = " + myContactGroupId, null,
                        "lower(" + GroupMembership.DISPLAY_NAME + ")");

                if (cursor == null)
                    return null;

                ArrayList<Contact> contacts = new ArrayList<Contact>();
                try {
                    if (!cursor.moveToFirst())
                        return contacts;
                    do {
                        Contact c = new Contact();
                        c.rawContactId = cursor.getInt(cursor.getColumnIndex(GroupMembership.RAW_CONTACT_ID));
                        c.displayName = cursor.getString(cursor.getColumnIndex(Data.DISPLAY_NAME));
                        contacts.add(c);
                        if (isCancelled())
                            return null;
                    } while (cursor.moveToNext());
                } finally {
                    cursor.close();
                }

                return contacts;
            } else {
                return null;
            }
        }

        @Override
        protected void onPostExecute(List<Contact> result) {
            if (result != null) {
                ContactAdapter adapter = (ContactAdapter) contactList.getAdapter();
                adapter.refresh(result);
            }
            // FIXME we don't have to do this every time!
            loadingProgress.setVisibility(View.GONE);
            contactList.setEmptyView(emptyList);
        }

    }

    private static class Contact {
        public int rawContactId;
        public String displayName;
    }

    private class ContactAdapter extends BaseAdapter {

        private final int selectedColor = Color.parseColor("#8833b5e5");
        private final int unselectedColor = Color.TRANSPARENT;

        private ArrayList<Contact> items = new ArrayList<Contact>();

        public List<Contact> getBackingList() {
            return Collections.unmodifiableList(items);
        }

        public void refresh(List<Contact> newList) {
            items.clear();
            items.addAll(newList);
            notifyDataSetChanged();
        }

        @Override
        public int getCount() {
            return items.size();
        }

        @Override
        public Object getItem(int position) {
            return items.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            boolean checked = contactList.isItemChecked(position);
            Contact c = items.get(position);

            Drawable thumb = thumbMemCache.get(c.rawContactId);
            if (thumb == null) {
                asyncTasks.add((LoadThumbTask) new LoadThumbTask().execute(c.rawContactId));
                Bitmap fromDisk = readDiskCache(c.rawContactId);
                if (fromDisk != null)
                    putToThumbMemCache(c.rawContactId, new BitmapDrawable(getResources(), fromDisk));
                else
                    putToThumbMemCache(c.rawContactId, defaultThumb);
            }
            thumb = thumbMemCache.get(c.rawContactId);

            View topView = convertView != null ? convertView
                    : getLayoutInflater().inflate(R.layout.contact_row, null);

            ((TextView) topView.findViewById(R.id.name)).setText(c.displayName);
            ((ImageView) topView.findViewById(R.id.photo)).setBackgroundDrawable(thumb);
            topView.setBackgroundColor(checked ? selectedColor : unselectedColor);
            return topView;
        }
    }

    private class DownloadUploadTask extends AsyncTask<List<Contact>, Void, Integer> {

        public static final int TYPE_DOWNLOAD = 0;
        public static final int TYPE_UPLOAD = 1;
        private static final int BATCH_SIZE = 50;
        private ProgressDialog dialog;
        private int type;

        public DownloadUploadTask(int type) {
            super();
            this.type = type;
            dialog = new ProgressDialog(AssignContactPhotoActivity.this);
            dialog.setTitle(getResources().getString(
                    type == TYPE_DOWNLOAD ? R.string.queuing_for_download : R.string.queuing_for_upload));

            dialog.setIndeterminate(true);
            dialog.setCancelable(true);
            dialog.setOnCancelListener(new OnCancelListener() {
                @Override
                public void onCancel(DialogInterface dialog) {
                    cancel(true);
                }
            });
            dialog.show();
        }

        @Override
        protected Integer doInBackground(List<Contact>... arg0) {
            List<Contact> contacts = arg0[0];
            ContentValues updateVals = new ContentValues();

            if (type == TYPE_DOWNLOAD)
                updateVals.put(RawContacts.SYNC4, SyncAdapter.OVERRIDE_TAG + "|");
            else if (type == TYPE_UPLOAD)
                updateVals.put(RawContacts.SYNC4, "|" + SyncAdapter.OVERRIDE_TAG);

            for (int i = 0; i < contacts.size(); i += BATCH_SIZE) {
                StringBuffer inVals = new StringBuffer();
                int maxIndex = Math.min(BATCH_SIZE + i, contacts.size()) - i;
                for (int j = 0; j < maxIndex; j++)
                    inVals.append(contacts.get(i + j).rawContactId + ",");
                inVals.deleteCharAt(inVals.length() - 1);
                String selectionClause = RawContacts._ID + " IN (" + inVals + ")";
                getContentResolver().update(RawContacts.CONTENT_URI, updateVals, selectionClause, new String[] {});
                if (isCancelled())
                    return 0;
            }

            return contacts.size();
        }

        @Override
        protected void onCancelled() {
            dialog.dismiss();
        }

        @Override
        protected void onPostExecute(Integer result) {
            dialog.dismiss();
            Toast.makeText(AssignContactPhotoActivity.this,
                    String.format(getResources().getString(
                            type == TYPE_DOWNLOAD ? R.string.queued_for_download : R.string.queued_for_upload),
                            result),
                    Toast.LENGTH_LONG).show();
        }
    }

    private class RemovePhotoTask extends AsyncTask<List<Contact>, Void, Integer> {

        private static final int BATCH_SIZE = 50;
        private ProgressDialog dialog;
        private List<Contact> contacts;

        public RemovePhotoTask() {
            super();
            dialog = new ProgressDialog(AssignContactPhotoActivity.this);
            dialog.setTitle(getResources().getString(R.string.removing_photos));

            dialog.setIndeterminate(true);
            dialog.setCancelable(true);
            dialog.setOnCancelListener(new OnCancelListener() {
                @Override
                public void onCancel(DialogInterface dialog) {
                    cancel(true);
                }
            });
            dialog.show();
        }

        @Override
        protected Integer doInBackground(List<Contact>... arg0) {
            contacts = arg0[0];

            for (int i = 0; i < contacts.size(); i += BATCH_SIZE) {
                StringBuffer inVals = new StringBuffer();
                int maxIndex = Math.min(BATCH_SIZE + i, contacts.size()) - i;
                for (int j = 0; j < maxIndex; j++)
                    inVals.append(contacts.get(i + j).rawContactId + ",");
                inVals.deleteCharAt(inVals.length() - 1);
                String selectionClause = GroupMembership.RAW_CONTACT_ID + " IN (" + inVals + ")";

                ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();

                ops.add(ContentProviderOperation
                        .newUpdate(
                                Data.CONTENT_URI.buildUpon().appendQueryParameter(RawContacts.ACCOUNT_NAME, account)
                                        .appendQueryParameter(RawContacts.ACCOUNT_TYPE, ACCOUNT_TYPE).build())
                        .withSelection(selectionClause, null).withValue(Photo.PHOTO, null).build());

                try {
                    getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
                } catch (RemoteException e) {
                } catch (OperationApplicationException e) {
                }

                if (isCancelled())
                    return 0;
            }

            return contacts.size();
        }

        @Override
        protected void onCancelled() {
            dialog.dismiss();
        }

        @Override
        protected void onPostExecute(Integer result) {
            dialog.dismiss();
            Toast.makeText(AssignContactPhotoActivity.this,
                    String.format(getResources().getString(R.string.removed_photos), result), Toast.LENGTH_LONG)
                    .show();
            for (Contact c : contacts)
                thumbMemCache.remove(c.rawContactId);
            ((ContactAdapter) contactList.getAdapter()).notifyDataSetChanged();
        }
    }

    private class ContactListMultiChoiceModeListener implements MultiChoiceModeListener {

        private HashSet<Integer> selectedPos;

        private List<Contact> getSelectedContacts() {
            ArrayList<Contact> selectedContacts = new ArrayList<Contact>();
            List<Contact> allContacts = ((ContactAdapter) contactList.getAdapter()).getBackingList();
            for (int p : selectedPos)
                selectedContacts.add(allContacts.get(p));
            return selectedContacts;
        }

        @SuppressWarnings("unchecked")
        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            switch (item.getItemId()) {

            case R.id.menu_upload_photo:
            case R.id.menu_download_photo:
                new DownloadUploadTask(
                        item.getItemId() == R.id.menu_download_photo ? DownloadUploadTask.TYPE_DOWNLOAD
                                : DownloadUploadTask.TYPE_UPLOAD).execute(getSelectedContacts());
                break;

            case R.id.menu_remove_photo:
                new RemovePhotoTask().execute(getSelectedContacts());
                break;

            }
            mode.finish();
            return true;
        }

        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            selectedPos = new HashSet<Integer>();
            mode.setSubtitle(R.string.selected);
            mode.getMenuInflater().inflate(R.menu.action_contact_list, menu);
            return true;
        }

        @Override
        public void onDestroyActionMode(ActionMode mode) {

        }

        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            setTitle(mode);
            return true;
        }

        @Override
        public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
            if (checked)
                selectedPos.add(position);
            else
                selectedPos.remove(position);
            setTitle(mode);
        }

        private void setTitle(ActionMode mode) {
            int count = contactList.getCheckedItemCount();
            mode.setTitle(count + " " + getResources().getString(count > 1 ? R.string.contacts : R.string.contact));
            ((ContactAdapter) contactList.getAdapter()).notifyDataSetChanged();
        }

    }

    private class StoreImageTask extends AsyncTask<Void, Void, Integer> {

        private static final String PHOTO_DIR = "/files/photos";
        private static final String CONTACT_PROVIDER = "com.android.providers.contacts";
        private static final int WAIT_TIME_DB = 5000;
        private static final int WAIT_TIME_INT = 50;
        public static final int RESULT_SUCCESS = 0;
        public static final int RESULT_NO_ROOT = 1;
        public static final int RESULT_IO_ERROR = 2;
        public static final int RESULT_CANCELLED = 3;

        private ProgressDialog dialog;

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            dialog = new ProgressDialog(AssignContactPhotoActivity.this);
            dialog.setIndeterminate(true);
            dialog.setMessage(getResources().getString(R.string.saving_in_progress));
            dialog.setCancelable(false);
            dialog.show();
        }

        @Override
        protected Integer doInBackground(Void... params) {

            byte[] buffer = new byte[4096];
            int bytesRead;
            AssetFileDescriptor fdout = null;
            InputStream is = null;
            OutputStream os = null;

            try {

                // Delete the current picture

                ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();

                ops.add(ContentProviderOperation
                        .newUpdate(
                                Data.CONTENT_URI.buildUpon().appendQueryParameter(RawContacts.ACCOUNT_NAME, account)
                                        .appendQueryParameter(RawContacts.ACCOUNT_TYPE, ACCOUNT_TYPE).build())
                        .withSelection(GroupMembership.RAW_CONTACT_ID + " = " + pickedRawContact, null)
                        .withValue(Photo.PHOTO, null).build());

                try {
                    getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
                } catch (RemoteException e1) {
                } catch (OperationApplicationException e1) {
                }

                // Store the image using android API as to update its database

                is = new FileInputStream(cropTemp);

                Uri rawContactPhotoUri = Uri.withAppendedPath(
                        ContentUris.withAppendedId(RawContacts.CONTENT_URI, pickedRawContact),
                        RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
                fdout = getContentResolver().openAssetFileDescriptor(rawContactPhotoUri, "w");
                os = fdout.createOutputStream();

                bytesRead = is.read(buffer);
                while (bytesRead >= 0) {
                    os.write(buffer, 0, bytesRead);
                    bytesRead = is.read(buffer);
                    if (isCancelled())
                        return RESULT_CANCELLED;
                }

                os.close();
                fdout.close();
                is.close();

                // Wait until its file ID is available

                int fileId = -1;
                Uri contactsUri = ContactsContract.Data.CONTENT_URI.buildUpon()
                        .appendQueryParameter(RawContacts.ACCOUNT_NAME, account)
                        .appendQueryParameter(RawContacts.ACCOUNT_TYPE, ACCOUNT_TYPE).build();

                int retryTime = 0;
                for (; retryTime < WAIT_TIME_DB; retryTime += WAIT_TIME_INT) {

                    Cursor cursor = getContentResolver().query(contactsUri,
                            new String[] { Photo.PHOTO_FILE_ID, GroupMembership.RAW_CONTACT_ID },
                            GroupMembership.RAW_CONTACT_ID + " = ?", new String[] { pickedRawContact + "" }, null);

                    try {
                        if (cursor.moveToFirst()) {

                            int colIndex = cursor.getColumnIndex(Photo.PHOTO_FILE_ID);
                            if (!cursor.isNull(colIndex)) {
                                fileId = cursor.getInt(colIndex);
                                break;
                            }
                        }
                    } finally {
                        cursor.close();
                    }

                    if (isCancelled())
                        return RESULT_CANCELLED;
                }

                if (fileId < 0) {
                    Log.e(TAG, "File ID didn't show up in db after saving");
                    return RESULT_IO_ERROR;
                }

                // Wait until the actual file is available

                boolean fileAvailable = false;
                for (; retryTime < WAIT_TIME_DB; retryTime += WAIT_TIME_INT) {
                    try {
                        fdout = getContentResolver().openAssetFileDescriptor(rawContactPhotoUri, "r");
                        is = fdout.createInputStream();
                        is.close();
                        fdout.close();
                        fileAvailable = true;
                        break;
                    } catch (FileNotFoundException e) {
                    } finally {
                        try {
                            is.close();
                        } catch (IOException e) {
                        }
                        try {
                            fdout.close();
                        } catch (IOException e) {
                        }
                    }

                    if (isCancelled())
                        return RESULT_CANCELLED;
                }

                if (!fileAvailable)
                    return RESULT_IO_ERROR;

                // Atomically replace the image file

                if (!rootReplaceImage(cropTemp.getAbsolutePath(), fileId))
                    return RESULT_NO_ROOT;

                return RESULT_SUCCESS;

            } catch (InterruptedException e) {
                return RESULT_IO_ERROR;
            } catch (IOException e) {
                e.printStackTrace();
                return RESULT_IO_ERROR;
            } finally {
                try {
                    if (is != null)
                        is.close();
                } catch (IOException e) {
                }
                try {
                    if (os != null)
                        os.close();
                } catch (IOException e) {
                }
                try {
                    if (fdout != null)
                        fdout.close();
                } catch (IOException e) {
                }
            }
        }

        private boolean rootReplaceImage(String src, int fileId) throws InterruptedException, IOException {

            int uid;
            String dstDir;
            try {
                PackageManager pm = getPackageManager();
                PackageInfo pi = pm.getPackageInfo(CONTACT_PROVIDER, 0);
                uid = pi.applicationInfo.uid;
                dstDir = pi.applicationInfo.dataDir + PHOTO_DIR;
            } catch (NameNotFoundException e) {
                return false;
            }

            // Find the PID of contact provider

            String killCommand = "";
            ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
            for (RunningAppProcessInfo proc : am.getRunningAppProcesses())
                for (String p : proc.pkgList)
                    if (CONTACT_PROVIDER.equals(p)) {
                        killCommand = "kill " + proc.pid + "\n";
                        break;
                    }

            // Modify the permission of our tmp file and move it over to the correct
            // location + restart contact storage service

            if (!Util.runRoot("chown " + uid + ":" + uid + " " + src + "\nchmod 600 " + src + "\nmv " + src + " "
                    + dstDir + "/" + fileId + "\n" + killCommand))
                return false;

            return true;

        }

        @Override
        protected void onPostExecute(Integer result) {
            try {
                dialog.dismiss();
            } catch (Exception e) {
            }
            switch (result) {
            case RESULT_SUCCESS:
                break;
            case RESULT_NO_ROOT:
                Toast.makeText(AssignContactPhotoActivity.this, getResources().getString(R.string.need_to_be_root),
                        Toast.LENGTH_LONG).show();
                break;
            case RESULT_IO_ERROR:
                Toast.makeText(AssignContactPhotoActivity.this,
                        getResources().getString(R.string.something_went_wrong), Toast.LENGTH_LONG).show();
                break;
            }
            removeDiskCache(pickedRawContact);
            thumbMemCache.remove(pickedRawContact);
        }

        @Override
        protected void onCancelled() {
            try {
                dialog.cancel();
            } catch (Exception e) {
            }
            Toast.makeText(AssignContactPhotoActivity.this, getResources().getString(R.string.saving_cancelled),
                    Toast.LENGTH_LONG).show();
            removeDiskCache(pickedRawContact);
            thumbMemCache.remove(pickedRawContact);
        }

    }

}