public class AutoSmsDbHelper extends SQLiteOpenHelper {

    private static final String TAG = "AutoSmsDbHelper";

    private static AutoSmsDbHelper AutoSmsDbHelper;

    private static final String DATABASE_NAME = "SmsScheduler.db";
    private static final int DATABASE_VERSION = 1;

    public static final String TABLE_SMS = "sms";

    public static final String COLUMN_TIMESTAMP_CREATED = "datetimeCreated";
    public static final String COLUMN_TIMESTAMP_SCHEDULED = "datetimeScheduled";
    public static final String COLUMN_RECIPIENT_PHONE_NUMBER = "recipientPhoneNumber";
    public static final String COLUMN_RECIPIENT_LOOKUP = "recipientLookup";
    public static final String COLUMN_MESSAGE = "message";
    public static final String COLUMN_STATUS = "status";
    public static final String COLUMN_RESULT = "result";
    public static final String COLUMN_SUBSCRIPTION_ID = "subscriptionId";
    public static final String COLUMN_RECURRING_MODE = "recurringMode";

    public AutoSmsDbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    public AutoSmsDbHelper(Context context, SQLiteDatabase.CursorFactory factory) {
        super(context, DATABASE_NAME, factory, DATABASE_VERSION);
    }

    static public AutoSmsDbHelper getDbHelper(Context context) {
        if (null == AutoSmsDbHelper) {
            AutoSmsDbHelper = new AutoSmsDbHelper(context);
        }
        return AutoSmsDbHelper;
    }

    static public void closeDbHelper() {
        if (AutoSmsDbHelper != null) {
            AutoSmsDbHelper.close();
        }
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL("CREATE TABLE " + TABLE_SMS +
                "(" +
                COLUMN_TIMESTAMP_CREATED + " BIGINTEGER PRIMARY KEY," +
                COLUMN_TIMESTAMP_SCHEDULED + " BIGINTEGER," +
                COLUMN_RECIPIENT_PHONE_NUMBER + " TEXT," +
                COLUMN_RECIPIENT_LOOKUP + " TEXT," +
                COLUMN_MESSAGE + " TEXT," +
                COLUMN_STATUS + " TEXT," +
                COLUMN_RESULT + " TEXT," +
                COLUMN_SUBSCRIPTION_ID + " INTEGER," +
                COLUMN_RECURRING_MODE + " TEXT" +
                ")"
        );
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        if (newVersion <= oldVersion) {
            Log.i(getClass().getName(), "newVersion <= oldVersion");
            return;
        }
    }

    private ContentValues constructContentValues(AutoSms sms) {
        ContentValues values = new ContentValues();
        values.put(COLUMN_TIMESTAMP_SCHEDULED, sms.getDateScheduled().getTime());
        values.put(COLUMN_RECIPIENT_LOOKUP, sms.getRecipientLookup());
        values.put(COLUMN_RECIPIENT_PHONE_NUMBER, sms.getRecipientPhoneNumber());
        values.put(COLUMN_MESSAGE, sms.getMessage());
        values.put(COLUMN_STATUS, sms.getStatus().name());
        values.put(COLUMN_RESULT, sms.getResult());
        values.put(COLUMN_SUBSCRIPTION_ID, sms.getSubscriptionId());
        values.put(COLUMN_RECURRING_MODE, sms.getRecurringMode());
        return values;
    }

    public void insert(AutoSms sms) {
        ContentValues values = constructContentValues(sms);
        long timestampCreated = System.currentTimeMillis();
        sms.setDateCreated(new Date(timestampCreated));
        values.put(COLUMN_TIMESTAMP_CREATED, timestampCreated);
        AutoSmsDbHelper.getWritableDatabase().insert(TABLE_SMS, null, values);
        Log.d(TAG, "Add auto-sms " + sms);
    }

    public void update(AutoSms sms) {
        ContentValues values = constructContentValues(sms);
        String whereClause = COLUMN_TIMESTAMP_CREATED + "=?";
        String[] whereArgs = new String[] {String.valueOf(sms.getDateCreated().getTime())};
        AutoSmsDbHelper.getWritableDatabase().update(
                TABLE_SMS,
                values,
                whereClause,
                whereArgs);
        Log.d(TAG, "Update auto-sms " + sms);
    }

    public AutoSms getAutoSmsById(long timestampCreated) {
        Cursor cursor = AutoSmsDbHelper.getReadableDatabase().query(
                false,
                TABLE_SMS,
                new String[]{"*", COLUMN_TIMESTAMP_CREATED + " AS _id"},
                COLUMN_TIMESTAMP_CREATED + "=?",
                new String[]{Long.toString(timestampCreated)},
                null,
                null,
                null,
                "1"
        );
        if (cursor != null) {
            List<AutoSms> results = getListAutoSms(cursor);
            cursor.close();
            if (results.size() > 0) {
                return results.get(0);
            }
        }
        return null;
    }

    public List<AutoSms> getListAutoSmsByLookupKeyAndStatus(String lookupKey, AutoSms.Status status) {
        Cursor cursor =  AutoSmsDbHelper.getReadableDatabase().query(
                TABLE_SMS,
                new String[] { "*", COLUMN_TIMESTAMP_CREATED + " AS _id" },
                COLUMN_STATUS + "=?" + " AND " + COLUMN_RECIPIENT_LOOKUP + "=?",
                new String[] {status.name(), lookupKey},
                null,
                null,
                COLUMN_TIMESTAMP_CREATED + " DESC");
        if (cursor != null) {
            List<AutoSms> results = getListAutoSms(cursor);
            cursor.close();
            return results;
        }
        return null;
    }

    public List<AutoSms> getListAutoSmsByStatus(AutoSms.Status status) {
        Cursor cursor =  AutoSmsDbHelper.getReadableDatabase().query(
                TABLE_SMS,
                new String[] { "*", COLUMN_TIMESTAMP_CREATED + " AS _id" },
                COLUMN_STATUS + "=?",
                new String[] {status.name()},
                null,
                null,
                COLUMN_TIMESTAMP_CREATED + " DESC");
        if (cursor != null) {
            List<AutoSms> results = getListAutoSms(cursor);
            cursor.close();
            return results;
        }
        return null;
    }

    public void deleteAllByLookupKey(String lookupKey) {
        String selection = COLUMN_RECIPIENT_LOOKUP + "=?";
        String[] selectionArgs = new String[] {lookupKey};
        AutoSmsDbHelper.getReadableDatabase().delete(TABLE_SMS, selection, selectionArgs);
    }

    public void deleteById(Long timestampCreated) {
        String selection = COLUMN_TIMESTAMP_CREATED + "=?";
        String[] selectionArgs = new String[] {timestampCreated.toString()};
        AutoSmsDbHelper.getReadableDatabase().delete(TABLE_SMS, selection, selectionArgs);
    }

    private List<AutoSms> getListAutoSms(Cursor cursor) {
        ArrayList<AutoSms> result = new ArrayList<>();
        int indexTimestampCreated = cursor.getColumnIndex(COLUMN_TIMESTAMP_CREATED);
        int indexTimestampScheduled = cursor.getColumnIndex(COLUMN_TIMESTAMP_SCHEDULED);
        int indexRecipientNumber = cursor.getColumnIndex(COLUMN_RECIPIENT_PHONE_NUMBER);
        int indexRecipientLookup = cursor.getColumnIndex(COLUMN_RECIPIENT_LOOKUP);
        int indexMessage = cursor.getColumnIndex(COLUMN_MESSAGE);
        int indexStatus = cursor.getColumnIndex(COLUMN_STATUS);
        int indexResult = cursor.getColumnIndex(COLUMN_RESULT);
        int indexSubscriptionId = cursor.getColumnIndex(COLUMN_SUBSCRIPTION_ID);
        int indexRecurringMode = cursor.getColumnIndex(COLUMN_RECURRING_MODE);
        AutoSms object;
        while (cursor.moveToNext()) {
            object = new AutoSms();
            object.setDateCreated(new Date(cursor.getLong(indexTimestampCreated)));
            object.setDateScheduled(new Date(cursor.getLong(indexTimestampScheduled)));
            object.setRecipientPhoneNumber(cursor.getString(indexRecipientNumber));
            object.setRecipientLookup(cursor.getString(indexRecipientLookup));
            object.setMessage(cursor.getString(indexMessage));
            object.setStatus(AutoSms.Status.valueOf(cursor.getString(indexStatus)));
            object.setResult(cursor.getString(indexResult));
            object.setSubscriptionId(cursor.getInt(indexSubscriptionId));
            object.setRecurringMode(cursor.getString(indexRecurringMode));
            result.add(object);
        }
        return result;
    }
} public class AutoSms implements Parcelable {

    public static final String ERROR_UNKNOWN = "UNKNOWN";
    public static final String ERROR_GENERIC = "GENERIC";
    public static final String ERROR_NO_SERVICE = "NO_SERVICE";
    public static final String ERROR_NULL_PDU = "NULL_PDU";
    public static final String ERROR_RADIO_OFF = "RADIO_OFF";

    private Date dateCreated;
    private Date dateScheduled;
    private String recipientPhoneNumber;
    private String recipientLookup;
    private String message;
    private Status status = Status.PENDING;
    private int subscriptionId;
    private String recurringMode = CalendarResolver.RECURRING_NO;

    private String result = "";

    public AutoSms() {

    }

    public Date getDateCreated() {
        return dateCreated;
    }

    public void setDateCreated(Date dateCreated) {
        this.dateCreated = dateCreated;
    }

    public Date getDateScheduled() {
        return dateScheduled;
    }

    public void setDateScheduled(Date dateScheduled) {
        this.dateScheduled = dateScheduled;
    }

    public String getRecipientPhoneNumber() {
        return recipientPhoneNumber;
    }

    public void setRecipientPhoneNumber(String recipientPhoneNumber) {
        this.recipientPhoneNumber = recipientPhoneNumber;
    }

    public String getRecipientLookup() {
        return recipientLookup;
    }

    public void setRecipientLookup(String recipientLookup) {
        this.recipientLookup = recipientLookup;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public Status getStatus() {
        return status;
    }

    public void setStatus(Status status) {
        this.status = status;
    }

    public String getResult() {
        return result;
    }

    public void setResult(String result) {
        this.result = result;
    }

    public int getSubscriptionId() {
        return subscriptionId;
    }

    public void setSubscriptionId(int subscriptionId) {
        this.subscriptionId = subscriptionId;
    }

    public String getRecurringMode() {
        return recurringMode;
    }

    public void setRecurringMode(String recurringMode) {
        this.recurringMode = recurringMode;
    }

    public AutoSms(Parcel in) {
        dateCreated = (Date) in.readSerializable();
        dateScheduled = (Date) in.readSerializable();
        recipientPhoneNumber = in.readString();
        recipientLookup = in.readString();
        message = in.readString();
        status = Status.valueOf(in.readString());
        result = in.readString();
        subscriptionId = in.readInt();
        recurringMode = in.readString();
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeSerializable(dateCreated);
        dest.writeSerializable(dateScheduled);
        dest.writeString(recipientPhoneNumber);
        dest.writeString(recipientLookup);
        dest.writeString(message);
        dest.writeString(status.name());
        dest.writeString(result);
        dest.writeInt(subscriptionId);
        dest.writeString(recurringMode);
    }

    public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
        public AutoSms createFromParcel(Parcel in) {
            return new AutoSms(in);
        }

        public AutoSms[] newArray(int size) {
            return new AutoSms[size];
        }
    };

    @Override
    public String toString() {
        return "AutoSms{" +
                "dateCreated=" + dateCreated +
                ", dateScheduled=" + dateScheduled +
                ", recipientPhoneNumber='" + recipientPhoneNumber + '\'' +
                ", recipientLookup='" + recipientLookup + '\'' +
                ", message='" + message + '\'' +
                ", status=" + status +
                ", subscriptionId=" + subscriptionId +
                ", recurringMode='" + recurringMode + '\'' +
                ", result='" + result + '\'' +
                '}';
    }

    public enum Status {
        PENDING,
        SEND,
        DELIVERED,
        FAILED
    }
} public class AutoMessageAdapter extends AbstractReminderAdapter<AutoMessage, AutoMessageViewHolder> {

    private static final String TAG = "AutoMessageAdapter";

    public AutoMessageAdapter(Context context, DateUnknownYear anniversary) {
        super(context, anniversary);
    }

    @Override
    public void addDefaultItem() {
        int[] defaultTime = PreferencesManager.getDefaultTime(context);
        addReminder(new AutoMessage(anniversary.getDate(), defaultTime[0], defaultTime[1], 0));
    }

    @Override
    public AutoMessageViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_list_auto_messages, parent, false);
        return new AutoMessageViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(AutoMessageViewHolder holder, int position) {
        super.onBindViewHolder(holder, position);
        AutoMessage currentAutoMessage = listReminders.get(position);
        // TODO assign content of message
        holder.messageContent.setText(currentAutoMessage.getContent());
        holder.messageContent.addTextChangedListener(new OnTextChanged(currentAutoMessage, holder.saveButton));

        holder.saveButton.setVisibility(View.GONE);
        holder.saveButton.setOnClickListener(new OnClickSaveButton(currentAutoMessage));
    }

    private class OnTextChanged implements TextWatcher {

        private AutoMessage autoMessage;
        private View textView;

        private OnTextChanged(AutoMessage autoMessage, View textView) {
            this.autoMessage = autoMessage;
            this.textView = textView;
        }

        @Override
        public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}

        @Override
        public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
            autoMessage.setContent(charSequence.toString());
        }
        @Override
        public void afterTextChanged(Editable editable) {
            textView.setVisibility(View.VISIBLE);
        }
    }

    /**
     * Class for manage insert listener of select reminder
     */
    private class OnClickSaveButton implements View.OnClickListener {

        private AutoMessage autoMessage;

        OnClickSaveButton(AutoMessage autoMessage) {
            this.autoMessage = autoMessage;
        }

        @Override
        public void onClick(View view) {
            int position = listReminders.indexOf(autoMessage);
            // Notify observable
            for(ReminderDataObserver<AutoMessage> observer : reminderDataObservers) {
                observer.onReminderUpdated(autoMessage);
            }
            notifyItemChanged(position);
        }
    }
} public class MenuAdapter extends RecyclerView.Adapter<MenuViewHolder> {

    private Context context;
    private MenuContact menuFactory;

    public MenuAdapter(Context context,
                       MenuContact menuFactory) {
        this.context = context;
        this.menuFactory = menuFactory;
    }

    @Override
    public int getItemViewType(int position) {
        return menuFactory.getMenu(position).getState().ordinal();
    }

    @Override
    public MenuViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemListBuddyView;
        if(viewType == MenuAction.STATE.ACTIVE.ordinal())
            itemListBuddyView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_list_menu, parent, false);
        else if(viewType == MenuAction.STATE.INACTIVE.ordinal())
            itemListBuddyView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_list_menu_inactive, parent, false);
        else
            itemListBuddyView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_list_menu_not_available, parent, false);

        return new MenuViewHolder(itemListBuddyView);
    }

    @Override
    public void onBindViewHolder(MenuViewHolder holder, int position) {
        MenuAction menuAction = menuFactory.getMenu(position);
        holder.image.setImageResource(menuAction.getImageId());
        holder.title.setText(context.getString(menuAction.getTitleId()));
        holder.container.setOnClickListener(new BufferMenuClickListener(menuAction, position));
    }

    @Override
    public int getItemCount() {
        return menuFactory.getMenuCount();
    }

    public void setMenuContact(MenuContact menuContact) {
        this.menuFactory = menuContact;
    }

    /**
     * Class manager for add contact and view in listener
     */
    private class BufferMenuClickListener implements View.OnClickListener {

        private MenuAction menuAction;
        private int position;

        BufferMenuClickListener(MenuAction menuAction, int position) {
            this.menuAction = menuAction;
            this.position = position;
        }

        @Override
        public void onClick(View view) {
            menuAction.doAction(menuAction, position);
        }
    }
} public class ReminderCalendarNotificationsAdapter extends AbstractReminderAdapter<Reminder, ReminderViewHolder> {

    public ReminderCalendarNotificationsAdapter(Context context, DateUnknownYear anniversary) {
        super(context, anniversary);
    }

    @Override
    public void addDefaultItem() {
        int[] defaultTime = PreferencesManager.getDefaultTime(context);
        addReminder(new Reminder(anniversary.getDate(), defaultTime[0], defaultTime[1], 0));
    }

    @Override
    public ReminderViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_list_reminder_notifs, parent, false);
        return new ReminderViewHolder(itemView);
    }
} public class AutoSmsDatabaseObserver implements AbstractReminderAdapter.ReminderDataObserver<AutoMessage> {

    private final static String TAG = "AutoSmsDatabaseObserver";
    private Contact contact;
    private Context context;

    public AutoSmsDatabaseObserver(Contact contact, Context context) {
        this.contact = contact;
        this.context = context;
    }

    @Override
    public void onReminderAdded(AutoMessage autoMessage) {
        AutoSms autoSms = ProxyAutoMessage.getAutoSms(contact, autoMessage);
        AutoSmsDbHelper.getDbHelper(context).insert(autoSms);
        autoMessage.setId(autoSms.getDateCreated().getTime());
        boolean autoSmsReminder = PreferencesManager.isAutoSmsRemindersActive(context);
        new Scheduler(context).schedule(autoSms, autoSmsReminder);
        Log.d(TAG, "Message added and scheduled " + autoSms);
    }

    @Override
    public void onRemindersAdded(List<AutoMessage> autoMessages) {

    }

    @Override
    public void onReminderUpdated(AutoMessage autoMessage) {
        Log.d(TAG, "Content : " + autoMessage);
        AutoSms autoSms = ProxyAutoMessage.getAutoSms(contact, autoMessage);
        AutoSmsDbHelper.getDbHelper(context).update(autoSms);
        boolean autoSmsReminder = PreferencesManager.isAutoSmsRemindersActive(context);
        new Scheduler(context).schedule(autoSms, autoSmsReminder);
        Log.d(TAG, "Message updated and scheduled " + autoSms);
    }

    @Override
    public void onRemindersUpdated(List<AutoMessage> autoMessages) {

    }

    @Override
    public void onReminderDeleted(AutoMessage autoMessage) {
        AutoSms autoSms = ProxyAutoMessage.getAutoSms(contact, autoMessage);
        long id = autoSms.getDateCreated().getTime();
        AutoSmsDbHelper.getDbHelper(context).deleteById(id);
        new Scheduler(context).unschedule(id);
        Log.d(TAG, "Message remove and unscheduled " + autoSms);
    }

    @Override
    public void onRemindersDeleted(List<AutoMessage> reminders) {

    }
} public class AutoSmsToastObserver implements AbstractReminderAdapter.ReminderDataObserver<AutoMessage> {

    private Context context;

    public AutoSmsToastObserver(Context context) {
        this.context = context;
    }

    @Override
    public void onReminderAdded(AutoMessage reminder) {
        Toast.makeText(context, context.getString(R.string.auto_sms_added, reminder.getDate()), Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onRemindersAdded(List<AutoMessage> reminders) {

    }

    @Override
    public void onReminderUpdated(AutoMessage reminder) {
        Toast.makeText(context, context.getString(R.string.auto_sms_updated, reminder.getDate()), Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onRemindersUpdated(List<AutoMessage> reminders) {

    }

    @Override
    public void onReminderDeleted(AutoMessage reminder) {
        Toast.makeText(context, context.getString(R.string.auto_sms_deleted), Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onRemindersDeleted(List<AutoMessage> reminders) {

    }
} public class ReminderCalendarProviderObserver implements AbstractReminderAdapter.ReminderDataObserver<Reminder> {
    
    private Context context;
    private CalendarEvent baseEvent;
    private List<CalendarEvent> afterEvents;
    private ContentResolver contentResolver;
    private ArrayList<ContentProviderOperation> ops;
    
    public ReminderCalendarProviderObserver(Context context, Contact contact, CalendarEvent baseEvent) {
        this.context = context;
        try {
            this.afterEvents = EventLoader.getEventsSavedOrCreateNewsForEachYearAfterNextEvent(context, contact);
        } catch (EventLoader.EventException e) {
            Log.e(getClass().getSimpleName(), "Unable to getAutoSmsById next events. " + e.getLocalizedMessage());
            this.afterEvents = new ArrayList<>();
        }
        this.baseEvent = baseEvent;
        this.contentResolver = context.getContentResolver();
        this.ops = new ArrayList<>();
    }
    
    @Override
    public void onReminderAdded(Reminder reminder) {
        ops.add(ReminderProvider.insert(context, baseEvent.getId(), reminder));
        //TODO Add id to reminder
        applyBatch();
    }

    @Override
    public void onRemindersAdded(List<Reminder> reminders) {
        //TODO Add id to reminder
        for(Reminder reminder : reminders) {
            ops.add(ReminderProvider.insert(context, baseEvent.getId(), reminder));
        }
        applyBatch();
    }

    @Override
    public void onReminderUpdated(Reminder reminder) {
        // TODO with link
        ops.add(ReminderProvider.update(context, baseEvent.getId(), reminder));
        for(CalendarEvent afterEvent : afterEvents) {
            //ops.add(ReminderProvider.updateWithUnknownId(context, afterEvent.getId(), reminder, newMinutes));
        }
        applyBatch();
    }

    @Override
    public void onRemindersUpdated(List<Reminder> reminders) {
        for(Reminder reminder : reminders) {
            ops.add(ReminderProvider.update(context, baseEvent.getId(), reminder));
            for(CalendarEvent afterEvent : afterEvents) {
                // TODO with links
                //ops.add(ReminderProvider.update(context, afterEvent.getId(), reminder));
            }
        }
        applyBatch();
    }

    @Override
    public void onReminderDeleted(Reminder reminder) {
        ops.add(ReminderProvider.delete(context, baseEvent.getId(), reminder));
        for(CalendarEvent afterEvent : afterEvents) {
            ops.add(ReminderProvider.deleteWithUnknownId(context, afterEvent.getId(), reminder));
        }
        applyBatch();
    }

    @Override
    public void onRemindersDeleted(List<Reminder> reminders) {
        // TODO deleteById
        for(Reminder reminder : reminders) {
            ops.add(ReminderProvider.delete(context, baseEvent.getId(), reminder));
            for(CalendarEvent afterEvent : afterEvents) {
                ops.add(ReminderProvider.deleteWithUnknownId(context, afterEvent.getId(), reminder));
            }
        }
        applyBatch();
    }

    /**
     * Apply operations
     */
    private void applyBatch() {
        try {
            ContentProviderResult[] contentProviderResults =
                    contentResolver.applyBatch(CalendarContract.AUTHORITY, ops);
            for(ContentProviderResult result : contentProviderResults)
                if(result.uri != null)
                    Log.d(this.getClass().getSimpleName(), result.uri.toString());
        } catch (RemoteException|OperationApplicationException e) {
            Log.e(this.getClass().getSimpleName(), e.getMessage());
        } finally {
            ops.clear();
        }
    }

} public class ReminderToastObserver implements AbstractReminderAdapter.ReminderDataObserver<Reminder> {

    private Context context;

    public ReminderToastObserver(Context context) {
        this.context = context;
    }

    @Override
    public void onReminderAdded(Reminder reminder) {
        Toast.makeText(context, context.getString(R.string.reminder_added, reminder.getDate()), Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onRemindersAdded(List<Reminder> reminders) {

    }

    @Override
    public void onReminderUpdated(Reminder reminder) {
        Toast.makeText(context, context.getString(R.string.reminder_updated, reminder.getDate()), Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onRemindersUpdated(List<Reminder> reminders) {

    }

    @Override
    public void onReminderDeleted(Reminder reminder) {
        Toast.makeText(context, context.getString(R.string.reminder_deleted), Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onRemindersDeleted(List<Reminder> reminders) {

    }
} public class AnimationViewCircle {

    private View view;
    private int startPointX;
    private int startPointY;
    private int duration;
    private boolean customRadius;
    private float radius;
    private Interpolator interpolator;
    private Animator.AnimatorListener animatorListener;

    private AnimationViewCircle(View view) {
        this.view = view;

        customRadius = false;
        startPoint(0, 0);
        duration(320);
        interpolator(new AccelerateDecelerateInterpolator());

        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                hide();
            }
        });
    }

    public AnimationViewCircle animate() {
        if(view.getVisibility() == View.VISIBLE) {
            hide();
        } else {
            show();
        }
        return this;
    }

    public void hide() {
        // Android native animator
        Animator animatorRevealMenu =
                ViewAnimationUtils.createCircularReveal(view, startPointX, startPointY, radius, 0);
        animatorRevealMenu.setInterpolator(interpolator);
        animatorRevealMenu.setDuration(duration);
        animatorRevealMenu.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {
                if(animatorListener != null)
                    animatorListener.onAnimationStart(animator);
            }

            @Override
            public void onAnimationEnd(Animator animator) {
                view.setVisibility(View.GONE);
                if(animatorListener != null)
                    animatorListener.onAnimationEnd(animator);
            }

            @Override
            public void onAnimationCancel(Animator animator) {
                if(animatorListener != null)
                    animatorListener.onAnimationCancel(animator);
            }

            @Override
            public void onAnimationRepeat(Animator animator) {
                if(animatorListener != null)
                    animatorListener.onAnimationRepeat(animator);
            }
        });
        animatorRevealMenu.start();
    }

    public void show() {
        // Android native animator
        Animator animatorRevealMenu =
                ViewAnimationUtils.createCircularReveal(view, startPointX, startPointY, 0, radius);
        animatorRevealMenu.setInterpolator(interpolator);
        animatorRevealMenu.setDuration(duration);
        animatorRevealMenu.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {
                if(animatorListener != null)
                    animatorListener.onAnimationStart(animator);
                view.setVisibility(View.VISIBLE);
            }

            @Override
            public void onAnimationEnd(Animator animator) {
                if(animatorListener != null)
                    animatorListener.onAnimationEnd(animator);
            }

            @Override
            public void onAnimationCancel(Animator animator) {
                if(animatorListener != null)
                    animatorListener.onAnimationCancel(animator);
            }

            @Override
            public void onAnimationRepeat(Animator animator) {
                if(animatorListener != null)
                    animatorListener.onAnimationRepeat(animator);
            }
        });
        animatorRevealMenu.start();
    }

    public static AnimationViewCircle build(View view) {
        return new AnimationViewCircle(view);
    }

    public AnimationViewCircle startPoint(int startPointX, int startPointY) {
        this.startPointX = startPointX;
        this.startPointY = startPointY;
        if(!customRadius) {
            int dx = Math.max(startPointX, view.getWidth() - startPointX);
            int dy = Math.max(startPointY, view.getHeight() - startPointY);
            this.radius = (float) Math.hypot(dx, dy);
        }
        return this;
    }

    public AnimationViewCircle duration(int duration) {
        this.duration = duration;
        return this;
    }

    public AnimationViewCircle radius(float radius) {
        this.customRadius = true;
        this.radius = radius;
        return this;
    }

    public AnimationViewCircle interpolator(Interpolator interpolator) {
        this.interpolator = interpolator;
        return this;
    }

    public AnimationViewCircle setAnimatorListener(Animator.AnimatorListener animatorListener) {
        this.animatorListener = animatorListener;
        return this;
    }
} public class CalendarEvent implements Parcelable {

    public static final long ID_UNDEFINED = -1;

    private long id;
    private Date dateStart;
    private Date dateStop;
    private boolean allDay;
    private String title;
    private String description;
    private List<Reminder> reminders;

    public static String getEventTitleFromContact(Context context, Contact contact) {
        if (contact.hasBirthday()) {
            if(!contact.getBirthday().containsYear())
                return context.getString(R.string.event_title_without_year, contact.getName());
            else
                return context.getString(R.string.event_title, contact.getName(), contact.getAgeToNextBirthday());
        }
        return "";
    }

    public static CalendarEvent buildDefaultEventFromContactToSave(Context context, Contact contact) {
        CalendarEvent event = new CalendarEvent(getEventTitleFromContact(context, contact),
                contact.getNextBirthday(), true);
        int[] defaultTime = PreferencesManager.getDefaultTime(context);
        event.addReminder(
                new Reminder(event.getDate(), defaultTime[0], defaultTime[1]));

        Log.e(TAG, event.toString());
        return event;
    }

    public CalendarEvent(String title, Date date) {
        this(title, date, date);
    }

    public CalendarEvent(String title, Date date, boolean allDay) {
        this(title, date, date);
        setAllDay(allDay);
    }

    public CalendarEvent(String title, Date dateStart, Date dateStop) {
        this.id = ID_UNDEFINED;
        this.dateStart = dateStart;
        this.dateStop = dateStop;
        this.allDay = false;
        this.title = title;
        this.reminders = new ArrayList<>();
    }

    /**
     * Create a copy of event
     * @param another Base event
     */
    public CalendarEvent(CalendarEvent another) {
        this.id = ID_UNDEFINED;
        this.dateStart = another.dateStart;
        this.dateStop = another.dateStop;
        setAllDay(another.allDay);
        this.title = another.title;
        this.reminders = new ArrayList<>();
        for(Reminder reminder : another.reminders) {
            reminders.add(new Reminder(reminder));
        }
    }

    private CalendarEvent(Parcel in) {
        id = in.readLong();
        dateStart = (Date) in.readSerializable();
        dateStop = (Date) in.readSerializable();
        allDay = in.readByte() != 0;
        title = in.readString();
        reminders = in.readArrayList(Reminder.class.getClassLoader());
    }

    public boolean hasId() {
        return id != ID_UNDEFINED;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Date getDate() {
        return dateStart;
    }

    /**
     * Get the year from date of event
     * @return
     */
    public int getYear() {
        return new DateTime(getDate()).getYear();
    }

    /**
     * Set the year to the date of event
     */
    public void setYear(int year) {
        int secondsBetweenStartAndStop = Seconds.secondsBetween(
                new DateTime(dateStart),
                new DateTime(dateStop))
                .getSeconds();
        dateStart = new DateTime(dateStart).withYear(year).toDate();
        dateStop = new DateTime(dateStart).plusSeconds(secondsBetweenStartAndStop).toDate();
    }

    public Date getDateStart() {
        return dateStart;
    }

    public void setDateStart(Date dateStart) {
        this.dateStart = dateStart;
    }

    public Date getDateStop() {
        return dateStop;
    }

    public void setDateStop(Date dateStop) {
        this.dateStop = dateStop;
    }

    public boolean isAllDay() {
        return allDay;
    }

    public void setAllDay(boolean allDay) {
        this.allDay = allDay;
        if(allDay) {
            dateStart = new DateTime(dateStart)
                    .withHourOfDay(0)
                    .withMinuteOfHour(0)
                    .withSecondOfMinute(0)
                    .withMillisOfSecond(0)
                    .toDate();
            dateStop = new DateTime(dateStart)
                    .plusDays(1)
                    .toDate();
        }
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public void addReminder(Reminder reminder) {
        this.reminders.add(reminder);
    }

    public void addReminders(List<Reminder> reminders) {
        this.reminders.addAll(reminders);
    }

    public List<Reminder> getReminders() {
        return reminders;
    }

    public void deleteAllReminders() {
        reminders.clear();
    }

    @Override
    public String toString() {
        return "CalendarEvent{" +
                "id=" + id +
                ", dateStart=" + dateStart +
                ", dateStop=" + dateStop +
                ", allDay=" + allDay +
                ", title='" + title + '\'' +
                ", description='" + description + '\'' +
                ", reminders=" + reminders +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        CalendarEvent that = (CalendarEvent) o;

        if (allDay != that.allDay) return false;
        if (dateStart != null ? !dateStart.equals(that.dateStart) : that.dateStart != null)
            return false;
        if (dateStop != null ? !dateStop.equals(that.dateStop) : that.dateStop != null)
            return false;
        if (title != null ? !title.equals(that.title) : that.title != null) return false;
        return description != null ? description.equals(that.description) : that.description == null;

    }

    @Override
    public int hashCode() {
        int result = dateStart != null ? dateStart.hashCode() : 0;
        result = 31 * result + (dateStop != null ? dateStop.hashCode() : 0);
        result = 31 * result + (allDay ? 1 : 0);
        result = 31 * result + (title != null ? title.hashCode() : 0);
        result = 31 * result + (description != null ? description.hashCode() : 0);
        return result;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel parcel, int i) {
        parcel.writeLong(id);
        parcel.writeSerializable(dateStart);
        parcel.writeSerializable(dateStop);
        parcel.writeByte((byte) (allDay ? 1 : 0));
        parcel.writeString(title);
        parcel.writeList(reminders);
    }

    public static final Parcelable.Creator<CalendarEvent> CREATOR = new Parcelable.Creator<CalendarEvent>() {
        public CalendarEvent createFromParcel(Parcel in) {
            return new CalendarEvent(in);
        }

        public CalendarEvent[] newArray(int size) {
            return new CalendarEvent[size];
        }
    };
} public class Contact implements Parcelable{

    public static final long ID_UNDEFINED = -1;

    private long id;
    private String lookupKey;
    private long rawId;
    private long dataAnniversaryId;
    private String name;
    private Uri imageThumbnailUri;
    private Uri imageUri;
    private DateUnknownYear birthday;
    private List<PhoneNumber> phoneNumbers;

    public Contact(long id, String lookupKey, String name) {
        this(id, lookupKey, ID_UNDEFINED, name, null);
    }

    public Contact(long id, String lookupKey, long rawId, String name) {
        this(id, lookupKey, rawId, name, null);
    }

    public Contact(long id, String lookupKey, long rawId, String name, DateUnknownYear birthday) {
        this.id = id;
        this.lookupKey = lookupKey;
        this.rawId = rawId;
        this.name = name;
        this.imageThumbnailUri = null;
        this.imageUri = null;
        setBirthday(birthday);
    }

    public Contact(String name) {
        this(ID_UNDEFINED, "", ID_UNDEFINED, name, null);
    }

    public Contact(String name, DateUnknownYear birthday) {
        this(ID_UNDEFINED, "", ID_UNDEFINED, name, birthday);
    }

    private Contact(Parcel in) {
        id = in.readLong();
        lookupKey = in.readString();
        rawId = in.readLong();
        dataAnniversaryId = in.readLong();
        name = in.readString();
        imageThumbnailUri = in.readParcelable(Uri.class.getClassLoader());
        imageUri = in.readParcelable(Uri.class.getClassLoader());
        setBirthday((DateUnknownYear) in.readParcelable(DateUnknownYear.class.getClassLoader()));
        phoneNumbers = in.readArrayList(PhoneNumber.class.getClassLoader());
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getLookUpKey() {
        return lookupKey;
    }

    public void setLookUpKey(String lookupKey) {
        this.lookupKey = lookupKey;
    }

    public long getRawId() {
        return rawId;
    }

    public void setRawId(long rawId) {
        this.rawId = rawId;
    }

    public long getDataAnniversaryId() {
        return dataAnniversaryId;
    }

    public void setDataAnniversaryId(long dataAnniversaryId) {
        this.dataAnniversaryId = dataAnniversaryId;
    }

    public Uri getUri() {
        return ContactsContract.Contacts.getLookupUri(id, lookupKey);
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Uri getImageThumbnailUri() {
        return imageThumbnailUri;
    }

    public void setImageThumbnailUri(Uri imageUri) {
        this.imageThumbnailUri = imageUri;
    }

    public Uri getImageUri() {
        return imageUri;
    }

    public void setImageUri(Uri imageUri) {
        this.imageUri = imageUri;
    }

    public boolean hasBirthday() {
        return birthday!=null;
    }

    public DateUnknownYear getBirthday() {
        return birthday;
    }

    public void setBirthday(DateUnknownYear date) {
        this.birthday = date;
    }

    public boolean isPhoneNumberInit() {
        return phoneNumbers != null;
    }

    public boolean containsPhoneNumber() throws PhoneNumberNotInitializedException {
        if (phoneNumbers == null)
            throw new PhoneNumberNotInitializedException();
        return !phoneNumbers.isEmpty();
    }

    public PhoneNumber getMainPhoneNumber() throws PhoneNumberNotInitializedException,NoPhoneNumberException {
        if (phoneNumbers == null)
            throw new PhoneNumberNotInitializedException();
        if (phoneNumbers.isEmpty())
            throw new NoPhoneNumberException();
        return phoneNumbers.get(0);
    }

    public List<PhoneNumber> getPhoneNumbers() throws PhoneNumberNotInitializedException {
        if (phoneNumbers == null)
            throw new PhoneNumberNotInitializedException();
        return phoneNumbers;
    }

    public void setNoPhoneNumber() {
        if(this.phoneNumbers == null)
            this.phoneNumbers = new ArrayList<>();
        else
            this.phoneNumbers.clear();
    }

    public void setPhoneNumber(PhoneNumber phoneNumber) {
        setNoPhoneNumber();
        this.phoneNumbers.add(phoneNumber);
    }


    public void setPhoneNumbers(List<PhoneNumber> phoneNumbers) {
        setNoPhoneNumber();
        this.phoneNumbers = phoneNumbers;
    }



    public boolean containsImage() {
        return imageUri!=null;
    }

    /**
     * Get number of years always > 0 between the birthday and today <br />
     * WARNING : if the year is unknown, return -1
     * @return
     */
    public int getAge() {
        if(birthday.containsYear())
            return Math.abs(birthday.getDeltaYears());
        else
            return -1;
    }

    /**
     * Get years old of contact to next birthday
     * @return Next years old of contact
     */
    public int getAgeToNextBirthday() {
        if(birthday.containsYear())
            if(birthday.nextAnniversaryIsToday())
                return getAge();
            else
                return getAge() +1;
        else
            return -1;
    }

    /**
     * Return number of days between today and the birthday
     * @return Number of days left
     */
    public int getBirthdayDaysRemaining() {
        return birthday.getDeltaDaysInAYear();
    }

    /**
     * Gets the birthday not yet passed
     * @return Next birthday in the year
     */
    public Date getNextBirthday() {
        return birthday.getNextAnniversary();
    }

    /**
     * Gets the birthday not yet passed without hour, minute, second and millisecond
     * @return Next birthday in the year
     */
    public Date getNextBirthdayWithoutHour() {
        return birthday.getNextAnniversaryWithoutHour();
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel parcel, int i) {
        parcel.writeLong(id);
        parcel.writeString(lookupKey);
        parcel.writeLong(rawId);
        parcel.writeLong(dataAnniversaryId);
        parcel.writeString(name);
        parcel.writeParcelable(imageThumbnailUri, i);
        parcel.writeParcelable(imageUri, i);
        parcel.writeParcelable(birthday, i);
        parcel.writeList(phoneNumbers);
    }

    public static final Parcelable.Creator<Contact> CREATOR = new Parcelable.Creator<Contact>() {
        public Contact createFromParcel(Parcel in) {
            return new Contact(in);
        }

        public Contact[] newArray(int size) {
            return new Contact[size];
        }
    };

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Contact contact = (Contact) o;

        if(rawId != -1 && rawId == contact.rawId) return true;

        if (id == -1 || id != contact.id) return false;
        return lookupKey != null ? lookupKey.equals(contact.lookupKey) : contact.lookupKey == null;
    }

    @Override
    public int hashCode() {
        int result = (int) (id ^ (id >>> 32));
        result = 31 * result + (lookupKey != null ? lookupKey.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "Contact{" +
                "id='" + id + '\'' +
                "lookupKey='" + lookupKey + '\'' +
                ", name='" + name + '\'' +
                ", birthday=" + birthday +
                '}';
    }
} public class DateUnknownYear implements Parcelable {

    private static final String TAG = "DateUnknownYear";

    private static String WITH_YEAR_FORMAT_DEFAULT = "yyyy-MM-dd";
    private static String WITHOUT_YEAR_FORMAT_DEFAULT = "--MM-dd";
    private static int YEAR_UNKNOWN_DEFAULT = 1700;

    private boolean containsYear;
    private Date date;

    /**
     * Construct date with unknown year indication, contains year by default
     * @param date date to store
     */
    public DateUnknownYear(Date date) {
        this.containsYear = true;
        setDate(date);
    }

    /**
     * Construct date with unknown year indication
     * @param date date to store
     * @param containsYear true if year is known, false elsewhere
     */
    public DateUnknownYear(Date date, boolean containsYear) {
        this.containsYear = containsYear;
        setDate(date);
    }

    private DateUnknownYear(Parcel in) {
        this.containsYear = in.readByte() != 0;
        setDate((Date) in.readSerializable());
    }

    /**
     * @return Return the default unknown date
     */
    public static DateUnknownYear getDefault() {
        return new DateUnknownYear(new Date(0), false);
    }

    /**
     * Determines whether the year is present in the date
     * @return true is is present
     */
    public boolean containsYear() {
        return containsYear;
    }

    /**
     * Determines whether the year is present or not.
     * @param containsYear true if the year is present
     */
    public void setContainsYear(boolean containsYear) {
        this.containsYear = containsYear;
    }

    /**
     * @return The corresponding date, <br />
     * Caution: Hours, minutes and seconds are random, the year is random if not determined.
     */
    public Date getDate() {
        return date;
    }

    /**
     * Get the date with a specific year
     * @param year Year
     * @return Date with year
     */
    public Date getDateWithYear(int year) {
        return getDateWithYear(this.date, year);
    }

    /**
     * Get the date with a specific year
     * @param date Date
     * @param year Year
     * @return Date with year
     */
    public static Date getDateWithYear(Date date, int year) {
        return new DateTime(date).withYear(year).toDate();
    }

    /**
     * Assign a new date, only day, month and year are used.
     * @param date The new date
     */
    public void setDate(Date date) {
        this.date = date;
        Calendar calendar = GregorianCalendar.getInstance();
        calendar.setTime(date);
        int year = calendar.get(Calendar.YEAR);
        if(year == YEAR_UNKNOWN_DEFAULT)
            setContainsYear(false);
    }

    /**
     * Return number of days between today and the next date in a year
     * @param date date for calculate delta
     * @return Number of days always >= 0
     */
    public static int daysBetweenTodayAnd(Date date) {
        MonthDay monthDayNow = MonthDay.now();
        MonthDay monthDayOfNextDate = MonthDay.fromDateFields(date);
        if(monthDayNow.isEqual(monthDayOfNextDate))
            return 0;
        if(monthDayNow.isBefore(monthDayOfNextDate))
            return Days.daysBetween(monthDayNow, monthDayOfNextDate).getDays();
        else {
            DateTime dateTimeNow = DateTime.now();
            DateTime dateTimeOfNextDate = new DateTime(date).withYear(dateTimeNow.getYear()).plusYears(1);
            return Days.daysBetween(DateTime.now(), dateTimeOfNextDate).getDays();
        }
    }

    /**
     * Return number of days between today and the date (this) in a year<br />
     * @return Number of days always >= 0
     */
    public int getDeltaDaysInAYear() {
        return daysBetweenTodayAnd(this.date);
    }

    /**
     * If next Anniversary of date is today
     * @return True if it's the anniversary
     */
    public boolean nextAnniversaryIsToday() {
        return getDeltaDaysInAYear() == 0;
    }

    /**
     * Gets the anniversary not yet passed
     * @return Next anniversary in the year
     */
    // TODO TEST
    public Date getNextAnniversary() {
        return getNextAnniversary(this.date);
    }

    /**
     * Gets the anniversary not yet passed without hour, minute, second and millisecond
     * @return Next anniversary in the year
     */
    // TODO TEST
    public Date getNextAnniversaryWithoutHour() {
        DateTime dateWithoutHour = new DateTime(getNextAnniversary(this.date));
        dateWithoutHour = dateWithoutHour.withHourOfDay(0)
                .withMinuteOfHour(0)
                .withSecondOfMinute(0)
                .withMillisOfSecond(0);
        return dateWithoutHour.toDate();
    }

    /**
     * Gets the anniversary date not yet passed
     * @param date Date without year
     * @return Next anniversary in the year
     */
    public static Date getNextAnniversary(DateUnknownYear date) {
        return getNextAnniversary(date.getDate());
    }

    /**
     * Gets the anniversary date not yet passed (with delta of 1 day)
     * @param date Date
     * @return Next anniversary in the year
     */
    public static Date getNextAnniversary(Date date) {
        DateTime dateTimeNow = DateTime.now();
        MonthDay monthDayNow = MonthDay.now();
        MonthDay monthDayOfNextDate = MonthDay.fromDateFields(date);
        if(monthDayNow.isEqual(monthDayOfNextDate)) {
            DateTime inputDate = new DateTime(date);
            return dateTimeNow
                    .withHourOfDay(inputDate.getHourOfDay())
                    .withMinuteOfHour(inputDate.getMinuteOfHour())
                    .withSecondOfMinute(inputDate.getSecondOfMinute())
                    .withMillisOfSecond(inputDate.getMillisOfSecond()).toDate();
        } if(monthDayNow.isBefore(monthDayOfNextDate))
            return new DateTime(date).withYear(dateTimeNow.getYear()).toDate();
        else {
            DateTime dateTimeOfNextDate = new DateTime(date).withYear(dateTimeNow.getYear()).plusYears(1);
            return dateTimeOfNextDate.toDate();
        }
    }

    /**
     * Number of years between a date and today
     * @param date Date: Year for calculate number
     * @return int: Number of days
     */
    public static int yearsBetweenTodayAnd(Date date) {
        return Years.yearsBetween(DateTime.now(), new DateTime(date)).getYears();
    }

    /**
     * Number of years between the date (this) and today
     * @return int: Number of days
     */
    public int getDeltaYears() {
        return yearsBetweenTodayAnd(this.date);
    }

    /**
     * String for backup in a database
     * @return The string for backup
     */
    public String toBackupString() {
        if (!containsYear()){
            SimpleDateFormat datePattern = new SimpleDateFormat(WITHOUT_YEAR_FORMAT_DEFAULT, Locale.getDefault());
            return datePattern.format(date);
        } else {
            SimpleDateFormat datePattern = new SimpleDateFormat(WITH_YEAR_FORMAT_DEFAULT, Locale.getDefault());
            return datePattern.format(date);
        }
    }

    @Override
    public String toString() {
        int dateFormat = DateFormat.MEDIUM;
        if(containsYear()) {
            DateFormat df = DateFormat.getDateInstance(dateFormat, Locale.getDefault());
            return df.format(date);
        } else {
            return toStringMonthAndDay(dateFormat);
        }
    }

    /**
     * @return Formatted string only see the year, must be used with {@link DateUnknownYear#containsYear()} <br />
     * WARNING : If the date does not contain a year, returns a random number.
     */
    public String toStringYear() {
        SimpleDateFormat yearSimpleDateFormat = new SimpleDateFormat("yyyy", Locale.getDefault());
        return yearSimpleDateFormat.format(date);
    }

    /**
     * Formatted string only see the month and the day
     * @param dateFormat can be {@link DateFormat#SHORT}, {@link DateFormat#MEDIUM} or {@link DateFormat#LONG}
     * @return Formatted string
     */
    public String toStringMonthAndDay(int dateFormat) {
        SimpleDateFormat sdf = (SimpleDateFormat) DateFormat.getDateInstance(dateFormat);
        sdf.applyPattern(sdf.toPattern().replaceAll(
                "([^\\p{Alpha}']|('[\\p{Alpha}]+'))*y+([^\\p{Alpha}']|('[\\p{Alpha}]+'))*",
                ""));
        return sdf.format(date);
    }

    /**
     * Try to parse input with SimpleDateFormat
     *
     * @param format SimpleDateFormat
     * @param withYear When true the age will be not displayed in brackets
     * @return Date object if successful, otherwise null
     */
    private static DateUnknownYear parseStringWithSimpleDateFormat(String input, String format,
                                                        boolean withYear) {
        SimpleDateFormat dateFormat = new SimpleDateFormat(format, Locale.getDefault());
        dateFormat.setTimeZone(TimeZone.getDefault());
        try {
            DateUnknownYear dateUnknownYear = DateUnknownYear.getDefault();
            Date parsedDate = dateFormat.parse(input);
            dateUnknownYear.setContainsYear(withYear);
            dateUnknownYear.setDate(parsedDate);
            return dateUnknownYear;
        } catch (ParseException e) {
            return null;
        }
    }

    /**
     * The date format in the contact events is not standardized! This method will try to parse it
     * trying different date formats.
     * <p/>
     * See also: http://dmfs.org/carddav/?date_format
     *
     * @return eventDate as Date object
     */
    public static DateUnknownYear stringToDate(String eventDateString) {
        DateUnknownYear date;
        if (eventDateString != null) {
            // yyyy-MM-dd, Most used format!
            date = parseStringWithSimpleDateFormat(eventDateString, "yy-MM-dd", true);
            if (date == null) {
                date = parseStringWithSimpleDateFormat(eventDateString, "yyyy-MM-dd", false);
            }
            // --MM-dd, Most used format without year!
            if (date == null) {
                date = parseStringWithSimpleDateFormat(eventDateString, "--MM-dd", false);
            }
            if (date == null) {
                date = parseStringWithSimpleDateFormat(eventDateString, "MM-dd", false);
            }
            // yyyyMMdd, HTC Desire
            if (date == null) {
                if (eventDateString.length() == 8) {
                    date = parseStringWithSimpleDateFormat(eventDateString, "yyyyMMdd", true);
                }
            }
            // Unix timestamp, Some Motorola devices
            if (date == null) {
                Log.d(TAG, "Trying to parse Event Date String " + eventDateString
                        + " as a unix timestamp!");
                try {
                    Date rawDate = new Date(Long.parseLong(eventDateString));
                } catch (NumberFormatException e) {
                    Log.d(TAG, "Parsing failed!");
                }
            }
            // dd.MM.yyyy
            if (date == null) {
                date = parseStringWithSimpleDateFormat(eventDateString, "dd.MM.yyyy", true);
            }
            // yyyy.MM.dd
            if (date == null) {
                date = parseStringWithSimpleDateFormat(eventDateString, "yyyy.MM.dd", true);
            }
            // MM/dd/yyyy, Used by Facebook
            if (date == null) {
                date = parseStringWithSimpleDateFormat(eventDateString, "MM/dd/yyyy", true);
            }
            //MM/dd, Used by Facebook
            if (date == null) {
                date = parseStringWithSimpleDateFormat(eventDateString, "MM/dd", false);
            }
            /* Return */
            if (date != null) {
                Log.d(TAG, "Event Date String " + eventDateString + " was parsed as "
                        + date.toString());
                return date;
            } else {
                Log.e(TAG, "Event Date String " + eventDateString
                        + " could NOT be parsed! returning null!");
                return null;
            }
        } else {
            Log.d(TAG, "Event Date String is null!");
            return null;
        }
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel parcel, int i) {
        parcel.writeByte((byte) (containsYear ? 1 : 0));
        parcel.writeSerializable(date);
    }

    public static final Parcelable.Creator<DateUnknownYear> CREATOR = new Parcelable.Creator<DateUnknownYear>() {
        public DateUnknownYear createFromParcel(Parcel in) {
            return new DateUnknownYear(in);
        }

        public DateUnknownYear[] newArray(int size) {
            return new DateUnknownYear[size];
        }
    };
} public class PhoneNumber implements Parcelable {

    private String number;
    private int type;

    public PhoneNumber(String number, int type) {
        this.number = number;
        this.type = type;
    }

    private PhoneNumber(Parcel in) {
        number = in.readString();
        type = in.readInt();
    }

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    public int getType() {
        return type;
    }

    public void setType(int type) {
        this.type = type;
    }

    @Override
    public String toString() {
        return "PhoneNumber{" +
                "number='" + number + '\'' +
                ", type=" + type +
                '}';
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel parcel, int i) {
        parcel.writeString(number);
        parcel.writeInt(type);
    }

    public static final Parcelable.Creator<PhoneNumber> CREATOR = new Parcelable.Creator<PhoneNumber>() {
        public PhoneNumber createFromParcel(Parcel in) {
            return new PhoneNumber(in);
        }

        public PhoneNumber[] newArray(int size) {
            return new PhoneNumber[size];
        }
    };
} public class ProxyAutoMessage {

    private static final String TAG = "ProxyAutoMessage";

    public static AutoMessage getFromAutoSms(Date anniversary, AutoSms autoSms) {
        DateTime dateTimeAnniversary = new DateTime(anniversary);

        DateTime dateTimeScheduled = new DateTime(autoSms.getDateScheduled());
        dateTimeScheduled = new DateTime(DateUnknownYear.getNextAnniversary(dateTimeScheduled.toDate()));

        int minutes = Minutes.minutesBetween(dateTimeScheduled, dateTimeAnniversary).getMinutes();

        AutoMessage autoMessage = new AutoMessage(anniversary, minutes);
        autoMessage.setId(autoSms.getDateCreated().getTime());
        autoMessage.setContent(autoSms.getMessage());
        return autoMessage;
    }

    public static AutoSms getAutoSms(Contact contact, AutoMessage autoMessage) {
        AutoSms autoSms = new AutoSms();
        if (contact.getLookUpKey().equals("")) {
            Log.e(TAG, "Unknown lookup key of contact");
        } else {
            autoSms.setRecipientLookup(contact.getLookUpKey());
        }
        try {
            autoSms.setRecipientPhoneNumber(contact.getMainPhoneNumber().getNumber());
            autoSms.setMessage(autoMessage.getContent());
            if(autoMessage.getId() != AutoMessage.ID_UNDEFINED)
                autoSms.setDateCreated(new Date(autoMessage.getId()));
            else
                autoSms.setDateCreated(new Date());
            autoSms.setDateScheduled(autoMessage.getDate());
            autoSms.setStatus(AutoSms.Status.PENDING);
        } catch (PhoneNumberNotInitializedException |NoPhoneNumberException e) {
            Log.e(TAG, "Unable to create Auto-SMS, phone number can't be open");
        }
        //TODO Proxy
        return autoSms;
    }

    public static List<AutoMessage> getFromAutoSmsList(Date anniversary, List<AutoSms> autoSmsList) {
        List<AutoMessage> autoMessages = new ArrayList<>();
        for(AutoSms autoSms : autoSmsList) {
            autoMessages.add(getFromAutoSms(anniversary, autoSms));
        }
        return autoMessages;
    }
} public class Reminder implements Parcelable{
    public static final long ID_UNDEFINED = -1;

    private long id;
    protected Date dateEvent;
    protected int hourOfDay;
    protected int minuteOfHour;
    protected int daysBefore;

    /**
     * Create default auto message
     */
    public Reminder(Date dateEvent, int hourOfDay, int minuteOfHour, int deltaDay) {
        this.id = ID_UNDEFINED;
        this.dateEvent = dateEvent;
        this.dateEvent = new DateTime(this.dateEvent)
                .withHourOfDay(0)
                .withMinuteOfHour(0)
                .withSecondOfMinute(0)
                .withMillisOfSecond(0)
                .toDate();
        this.hourOfDay = hourOfDay;
        this.minuteOfHour = minuteOfHour;
        this.daysBefore = deltaDay;
    }

    /**
     * Create default auto message for date of anniversary
     */
    public Reminder(Date dateEvent, int hourOfDay, int minuteOfHour) {
        this(dateEvent, hourOfDay, minuteOfHour, 0);
    }

    /**
     * Create default auto message for date of anniversary
     */
    public Reminder(Date dateEvent, int minuteBeforeEvent) {
        this.id = ID_UNDEFINED;
        this.dateEvent = dateEvent;
        this.dateEvent = new DateTime(this.dateEvent)
                .withHourOfDay(0)
                .withMinuteOfHour(0)
                .withSecondOfMinute(0)
                .withMillisOfSecond(0)
                .toDate();
        DateTime dateReminder = new DateTime(dateEvent).minusMinutes(minuteBeforeEvent);
        this.hourOfDay = dateReminder.getHourOfDay();
        this.minuteOfHour = dateReminder.getMinuteOfHour();
        this.daysBefore = Days.daysBetween(dateReminder, new DateTime(dateEvent)).getDays();
        if(minuteBeforeEvent > 0)
            this.daysBefore++;
    }

    /**
     * Create a copy of Reminder
     * @param another
     */
    public Reminder(Reminder another) {
        this.id = ID_UNDEFINED;
        this.dateEvent = another.dateEvent;
        this.hourOfDay = another.hourOfDay;
        this.minuteOfHour = another.minuteOfHour;
        this.daysBefore = another.daysBefore;
    }

    public Reminder(Parcel in) {
        this.id = in.readLong();
        this.dateEvent = new Date(in.readLong());
        this.hourOfDay = in.readInt();
        this.minuteOfHour = in.readInt();
        this.daysBefore = in.readInt();
    }

    public boolean hasId() {
        return id != ID_UNDEFINED;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public Date getDate() {
        return new DateTime(dateEvent)
                .withMinuteOfHour(minuteOfHour)
                .withHourOfDay(hourOfDay)
                .minusDays(daysBefore)
                .toDate();
    }

    public void setDateEvent(Date dateEvent) {
        this.dateEvent = dateEvent;
    }

    public int getMinutesBeforeEvent() {
        return Minutes.minutesBetween(new DateTime(getDate()), new DateTime(dateEvent)).getMinutes();
    }

    public int getDeltaDay() {
        return daysBefore;
    }

    public void setDeltaDay(int deltaDay) {
        this.daysBefore = deltaDay;
    }

    public int getHourOfDay() {
        return hourOfDay;
    }

    public void setHourOfDay(int hour) {
        this.hourOfDay = hour;
    }

    public int getMinuteOfHour() {
        return minuteOfHour;
    }

    public void setMinuteOfHour(int minute) {
        this.minuteOfHour = minute;
    }

    @Override
    public String toString() {
        return "Reminder{" +
                "id=" + id +
                ", date=" + getDate() +
                '}';
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel parcel, int i) {
        parcel.writeLong(id);
        parcel.writeLong(dateEvent.getTime());
        parcel.writeInt(hourOfDay);
        parcel.writeInt(minuteOfHour);
        parcel.writeInt(daysBefore);
    }

    public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
        public Reminder createFromParcel(Parcel in) {
            return new Reminder(in);
        }

        public Reminder[] newArray(int size) {
            return new Reminder[size];
        }
    };
} public class NoPhoneNumberException extends Exception {

    public NoPhoneNumberException() {
        super("No Phone Number for this contact");
    }
} public class PhoneNumberNotInitializedException extends Exception {

    public PhoneNumberNotInitializedException() {
        super("Phone number has not been initialized, use the 'setPhoneNumbers' or 'setPhoneNumber' method of contact");
    }
} public interface ActionContactMenu {
    void doActionMenu(MenuAction menuAction);
} public abstract class MenuAction {

    private int titleId;
    private int imageId;
    private STATE state;

    private ActionContactMenu actionContactMenu;

    public MenuAction(int titleId, int imageId) {
        this(titleId, imageId, STATE.ACTIVE);
    }

    public MenuAction(int titleId, int imageId, STATE state) {
        this.titleId = titleId;
        this.imageId = imageId;
        this.state = state;
    }

    public abstract int getItemId();

    public int getTitleId() {
        return titleId;
    }

    public int getImageId() {
        return imageId;
    }

    public STATE getState() {
        return state;
    }

    public void setActionContactMenu(ActionContactMenu actionContactMenu) {
        this.actionContactMenu = actionContactMenu;
    }

    public void doAction(MenuAction menuAction, int position) {
        if(actionContactMenu != null)
            actionContactMenu.doActionMenu(menuAction);
    }

    public enum STATE {
        ACTIVE,
        INACTIVE,
        NOT_AVAILABLE
    }
} public class MenuActionAutoMessage extends MenuAction{

    public static final int ITEM_ID = 1684968;

    public MenuActionAutoMessage(STATE state) {
        super(R.string.auto_message_title,
                R.drawable.ic_text_clock_white_24dp,
                state);
    }

    public MenuActionAutoMessage() {
        this(STATE.ACTIVE);
    }

    @Override
    public int getItemId() {
        return ITEM_ID;
    }
} public class MenuActionCalendar extends MenuAction{

    public static final int ITEM_ID = 1684954;

    public MenuActionCalendar() {
        super(R.string.calendar_title, R.drawable.ic_event_note_white_24dp);
    }

    @Override
    public int getItemId() {
        return ITEM_ID;
    }
} public class MenuActionCall extends MenuAction{

    public static final int ITEM_ID = 1567967;

    public MenuActionCall() {
        super(R.string.call_title,
                R.drawable.ic_call_white_24dp);
    }

    @Override
    public int getItemId() {
        return ITEM_ID;
    }
} public class MenuActionGift extends MenuAction{

    public static final int ITEM_ID = 2561957;

    public MenuActionGift(STATE state) {
        super(R.string.gift_title,
                R.drawable.ic_gift_white_24dp,
                state);
    }

    public MenuActionGift() {
        this(STATE.ACTIVE);
    }

    @Override
    public int getItemId() {
        return ITEM_ID;
    }
} public class MenuActionMessage extends MenuAction{

    public static final int ITEM_ID = 1667954;

    public MenuActionMessage() {
        super(R.string.message_title,
                R.drawable.ic_message_white_24dp);
    }

    @Override
    public int getItemId() {
        return ITEM_ID;
    }
} public class MenuActionReminder extends MenuAction{

    public static final int ITEM_ID = 1646954;

    public MenuActionReminder(STATE state) {
        super(R.string.reminder_title,
                R.drawable.ic_alarm_white_24dp,
                state);
    }

    public MenuActionReminder() {
        this(STATE.ACTIVE);
    }

    @Override
    public int getItemId() {
        return ITEM_ID;
    }
} public abstract class MenuContact {

    protected List<MenuAction> listMenuAction;

    protected MenuContact() {
        listMenuAction = new ArrayList<>();
    }

    public void setActionContactMenu(ActionContactMenu actionContactMenu) {
        for (MenuAction menuAction : listMenuAction) {
            menuAction.setActionContactMenu(actionContactMenu);
        }
    }

    public List<MenuAction> createMenu() {
        return listMenuAction;
    }

    public MenuAction getMenu(int position) {
        try {
            return listMenuAction.get(position);
        } catch(IndexOutOfBoundsException e) {
            return null;
        }
    }

    public int getMenuCount() {
        return listMenuAction.size();
    }
} public class MenuContactCreator {

    private MenuContact menuFactory;

    public static MenuContact emptyMenuContact() {
        return new MenuContact() {
            @Override
            public void setActionContactMenu(ActionContactMenu actionContactMenu) {
                super.setActionContactMenu(actionContactMenu);
            }
        };
    }

    public MenuContactCreator(Context context, boolean asPhoneNumber) {
        // List for menu, depend of variant of app
        if(!BuildConfig.FULL_VERSION)
            menuFactory = new MenuFactoryFree(context, asPhoneNumber);
        else {
            menuFactory = new MenuFactoryPro(context, asPhoneNumber);
        }
    }

    public MenuContact create() {
        return menuFactory;
    }
} public class PreferencesManager {

    public static final String PATTERN_REMINDER_PREF = "^\\d{1,2}+(#\\d{1,2})*$";

    /**
     * Return true if custom calendar is active, false elsewhere
     * @param context Context to call
     * @return true id active
     */
    public static boolean isCustomCalendarActive(Context context) {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        //TODO false for free and new
        // true for free and old
        // true for pro and old
        // true for pro and new with dialog for duplication
        return prefs.getBoolean(context.getString(R.string.pref_create_calendar_key),
                Boolean.getBoolean(context.getString(R.string.pref_create_calendar_default)));
    }

    /**
     * Return the color of custom calendar
     * @param context Context to call
     * @return Color
     */
    public static @ColorInt int getCustomCalendarColor(Context context) {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        return prefs.getInt(context.getString(R.string.pref_calendar_color_key),
                ContextCompat.getColor(context, R.color.pref_calendar_color_default));
    }

    /**
     * Get default days in preferences
     * @param context Context to call
     * @return Array of days
     */
    public static int[] getDefaultDays(Context context) {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        String prefNotificationsDay = prefs.getString(context.getString(R.string.pref_reminders_days_key),
                context.getString(R.string.pref_reminders_days_default));
        Pattern p = Pattern.compile(PATTERN_REMINDER_PREF);
        int[] days;
        Matcher m = p.matcher(prefNotificationsDay);
        if(m.matches()) {
            String[] splitString = prefNotificationsDay.split("#");
            days = new int[splitString.length];
            for(int i=0; i<splitString.length; i++) {
                days[i] = Integer.parseInt(splitString[i]);
            }
            return days;
        }
        // 0 if preference can't match
        days = new int[1];
        days[0] = 0;
        return days;
    }

    /**
     * Get default time in preferences
     * @param context Context to call
     * @return Array of 2 elements, [0] for hours, [1] for minutes
     */
    public static int[] getDefaultTime(Context context) {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        String prefNotificationsTime = prefs.getString(context.getString(R.string.pref_reminders_time_key),
                context.getString(R.string.pref_reminders_time_default));
        int[] time = new int[2];
        time[0] = TimePreference.parseHour(prefNotificationsTime);
        time[1] = TimePreference.parseMinute(prefNotificationsTime);
        return time;
    }

    /**
     * Get default contactSort for sort the list of buddies
     * @param context Context to call
     * @return the sort
     */
    public static ContactSort getDefaultContactSort(Context context) {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        String prefContactSort = prefs.getString(context.getString(R.string.pref_contacts_sort_list_key),
                context.getString(R.string.pref_contacts_sort_list_value_default));
        String prefContactOrder = prefs.getString(context.getString(R.string.pref_contacts_order_list_key),
                context.getString(R.string.pref_contacts_order_list_value_default));
        return ContactSort.findContactSortByResourceValueString(
                context.getResources(),
                prefContactSort,
                prefContactOrder);
    }

    /**
     * Return true if daemons for powerfull notification or auto send message is active
     * @param context Context to call
     * @return Daemons active
     */
    public static boolean isDaemonsActive(Context context) {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        return prefs.getBoolean(context.getString(R.string.pref_special_service_key), false);
    }

    /**
     * Return true if reminders for auto-SMS is active
     * @param context Context to call
     * @return Reminders active
     */
    public static boolean isAutoSmsRemindersActive(Context context) {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        return prefs.getBoolean(context.getString(R.string.pref_auto_sms_reminders_key), false);
    }

    /**
     * Return true if buttons for inactive features are hidden
     * @param context Context to call
     * @return Hidden buttons
     */
    public static boolean isButtonsForInactiveFeaturesHidden(Context context) {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        return prefs.getBoolean(context.getString(R.string.pref_hide_inactive_features_key), false);
    }
} public abstract class ContactLoader extends AbstractLoader {

    private static final String TAG = "ContactLoader";

    protected ContactSort contactSort;
    private LoaderContactCallbacks loaderContactCallback;

    public ContactLoader(Context context) {
        super(context);
    }

    /**
     * Get RawContactId from ContactId
     * @param context context to call
     * @param contactId Id key of ContractsContract.Contacts
     * @return Id of RawContact
     */
    public static long getRawContactId(Context context, long contactId) {
        long rawContactId = -1;
        Cursor cursor = context.getContentResolver().query(
                ContactsContract.RawContacts.CONTENT_URI,
                new String[]{ContactsContract.RawContacts._ID},
                ContactsContract.RawContacts.CONTACT_ID + "=?",
                new String[]{String.valueOf(contactId)}, null);
        if(cursor != null) {
            if (cursor.moveToFirst()) {
                rawContactId = cursor.getLong(0);
            }
            cursor.close();
        }
        return rawContactId;
    }

    /**
     * Assign RAW_CONTACT_ID to contact in parameter with CONTACT_ID,
     * If CONTACT_ID is undefined, RAW_CONTACT_ID is set to undefined
     * @param context Context to call
     * @param contact Contact to modify
     * @return first RAW_CONTACT_ID assign
     */
    public static long assignRawContactIdToContact(Context context, Contact contact) {
        long rawId = Contact.ID_UNDEFINED;
        if(contact.getId() != Contact.ID_UNDEFINED)
            rawId = getRawContactId(context, contact.getId());
        contact.setRawId(rawId);
        return rawId;
    }

    /**
     * Return Contact from URI
     * @param context Context to call
     * @param contactData Contact URI
     * @return Contact from content resolver or null if not fund
     */
    public static Contact getContactFromURI(Context context, Uri contactData) {
        Contact contact = null;
        Cursor cursor =  context.getContentResolver().query(contactData, null, null, null, null);
        if (cursor != null) {
            if (cursor.moveToFirst()) {
                contact = new Contact(
                        cursor.getLong(cursor.getColumnIndex(ContactsContract.Data.CONTACT_ID)),
                        cursor.getString(cursor.getColumnIndex(ContactsContract.Data.LOOKUP_KEY)),
                        cursor.getLong(cursor.getColumnIndex(ContactsContract.Data.RAW_CONTACT_ID)),
                        cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DISPLAY_NAME)));
            }
            cursor.close();
        }
        return contact;
    }

    /**
     * Get List of contacts with birthdays in callback when loading is finished
     * @param context Context to call
     * @param loaderManager Loader Manager to init loader
     * @param contactsCallbacks Callback to retrieve contacts
     */
    public static void getAllContacts(final Context context, LoaderManager loaderManager,
                                               final ContactsCallbacks contactsCallbacks) {
        ContactBirthdayLoader contactBirthdayLoader = new ContactBirthdayLoader(context);
        contactBirthdayLoader.setLoaderContactCallback(new LoaderContactCallbacks() {
            @Override
            public void onContactLoadFinished(Loader<Cursor> loader, Cursor cursor) {
                contactsCallbacks.onContactsLoadFinished(getContactsFromCursor(cursor));
            }

            @Override
            public void onContactLoaderReset(Loader<Cursor> loader) {}
        });
        loaderManager.initLoader(0, null, contactBirthdayLoader);
    }

    public static List<Contact> getAllContacts(Context context) {
        ContactBirthdayLoader contactBirthdayLoader = new ContactBirthdayLoader(context);
        Cursor cursor = context.getContentResolver().query(
                contactBirthdayLoader.uri,
                contactBirthdayLoader.projection,
                contactBirthdayLoader.selection,
                contactBirthdayLoader.selectionArgs,
                contactBirthdayLoader.sortOrder);
        return getContactsFromCursor(cursor);
    }

    public static List<Contact> getContactsFromCursor(Cursor cursor) {
        List<Contact> contactList = new ArrayList<>();
        if(cursor != null) {
            // TODO getAutoSmsById only first for each contact
            while (cursor.moveToNext()) {
                int eventLookupKeyColumn = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Event.LOOKUP_KEY);
                int displayNameColumn = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
                int eventDateColumn = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Event.START_DATE);

                Contact contact = new Contact(cursor.getString(displayNameColumn));
                contact.setBirthday(DateUnknownYear.stringToDate(cursor.getString(eventDateColumn)));
                contact.setLookUpKey(cursor.getString(eventLookupKeyColumn));
                contactList.add(contact);
            }
            cursor.close();
        }
        return contactList;
    }

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        if(contactSort != null && contactSort.getOrderByQuery() != null) {
            sortOrder = contactSort.getOrderByQuery();
        }
        // Starts the query
        return new CursorLoader(
                context,
                uri,
                projection,
                selection,
                selectionArgs,
                sortOrder
        );
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
        if(loaderContactCallback != null)
            loaderContactCallback.onContactLoadFinished(loader, cursor);
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        if(loaderContactCallback != null)
            loaderContactCallback.onContactLoaderReset(loader);
    }

    /**
     * Assign listener for callback
     * @param loaderContactCallback Contact callback
     */
    public void setLoaderContactCallback(LoaderContactCallbacks loaderContactCallback) {
        this.loaderContactCallback = loaderContactCallback;
    }

    /**
     * Get sort of contact (combined ASC, DESC and list sort)
     * @return ContactSort
     */
    public ContactSort getContactSort() {
        return contactSort;
    }

    /**
     * Loader who getAutoSmsById all contacts
     */
    public static class ContactBaseLoader extends ContactLoader {

        public ContactBaseLoader(Context context) {
            super(context);

            uri = ContactsContract.Contacts.CONTENT_URI;
            projection = new String[]{
                    ContactsContract.Contacts._ID,
                    ContactsContract.Contacts.LOOKUP_KEY,
                    ContactsContract.Contacts.DISPLAY_NAME_PRIMARY,
                    ContactsContract.Contacts.PHOTO_THUMBNAIL_URI,
                    ContactsContract.Contacts.PHOTO_URI
            };
            selection = null;
            selectionArgs = null;
            sortOrder = null;

            contactSort = ContactSort.CONTACT_SORT_BY_NAME;
        }
    }

    /**
     * Loader who getAutoSmsById contacts with birthdays
     */
    public static class ContactBirthdayLoader extends ContactLoader {

        public ContactBirthdayLoader(Context context) {
            super(context);

            uri = ContactsContract.Data.CONTENT_URI;
            projection = new String[]{
                    ContactsContract.Contacts._ID,
                    ContactsContract.Contacts.LOOKUP_KEY,
                    ContactsContract.Contacts.DISPLAY_NAME_PRIMARY,
                    ContactsContract.Contacts.PHOTO_THUMBNAIL_URI,
                    ContactsContract.Contacts.PHOTO_URI,
                    ContactsContract.Contacts.Data._ID,
                    ContactsContract.CommonDataKinds.Event.START_DATE,
                    ContactsContract.CommonDataKinds.Event.TYPE
            };
            selection =
                    ContactsContract.Data.MIMETYPE + "= ? AND (" +
                            ContactsContract.CommonDataKinds.Event.TYPE + "=" +
                            ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY +
                            " ) ";
            selectionArgs = new String[] {
                    ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE
            };

            contactSort = PreferencesManager.getDefaultContactSort(context);
        }
    }

    /**
     * Interface for callback methods of LoaderContact
     */
    public interface LoaderContactCallbacks {
        void onContactLoadFinished(Loader<android.database.Cursor> loader, android.database.Cursor cursor);
        void onContactLoaderReset(Loader<Cursor> loader);
    }

    /**
     * Interface for callback methods of LoaderContact
     */
    public interface ContactsCallbacks {
        void onContactsLoadFinished(List<Contact> contacts);
    }
} public class ContactPhoneNumberLoader extends AbstractLoader {

    private CallbackActionPhoneNumber callbackActionPhoneNumber;

    public ContactPhoneNumberLoader(Context context, Contact contact) {
        super(context);

        uri = ContactsContract.Data.CONTENT_URI;
        projection = new String[] {
                ContactsContract.Contacts._ID,
                ContactsContract.Data.CONTACT_ID,
                ContactsContract.Data.LOOKUP_KEY,
                ContactsContract.CommonDataKinds.Phone.NUMBER,
                ContactsContract.CommonDataKinds.Phone.TYPE};
        selection = ContactsContract.Data.MIMETYPE + " = ? AND "
                + ContactsContract.Data.LOOKUP_KEY + " = '" + contact.getLookUpKey() + "'";
        selectionArgs = new String[] {
                ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE};

    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
        List<PhoneNumber> phoneNumbers = new ArrayList<>();
        if(cursor != null && !cursor.isClosed()) {
            cursor.moveToFirst();
            while (!cursor.isAfterLast()) {
                String number = cursor.getString(cursor.getColumnIndex(
                        ContactsContract.CommonDataKinds.Phone.NUMBER));
                int type = cursor.getInt(cursor.getColumnIndex(
                        ContactsContract.CommonDataKinds.Phone.TYPE));
                phoneNumbers.add(new PhoneNumber(number, type));
                cursor.moveToNext();
            }
            // TODO try catch with id
            cursor.close();
        }
        if(callbackActionPhoneNumber != null)
            callbackActionPhoneNumber.afterActionPhoneNumberInDatabase(phoneNumbers);
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {

    }

    public void setCallbackActionPhoneNumber(CallbackActionPhoneNumber callbackActionPhoneNumber) {
        this.callbackActionPhoneNumber = callbackActionPhoneNumber;
    }

    /**
     * Callback for do action after getAutoSmsById phone numbers of contact in database
     */
    public interface CallbackActionPhoneNumber {
        void afterActionPhoneNumberInDatabase(List<PhoneNumber> phoneNumberList);
    }
} public abstract class ContactProvider extends AsyncTask<Void, Void, Exception> {

    protected DateUnknownYear birthday;
    protected Activity context;

    protected CallbackActionBirthday callbackActionBirthday;
    protected CallbackActionBirthday.Action action;

    public ContactProvider(Activity context, DateUnknownYear birthday) {
        this.birthday = birthday;
        this.context = context;
        this.action = CallbackActionBirthday.Action.UNDEFINED;
    }

    @Override
    protected void onPostExecute(Exception exception) {
        if(exception != null)
            Log.e(getClass().getSimpleName(), "Error in contact provider : " + exception.getMessage());
        if(callbackActionBirthday != null)
            callbackActionBirthday.afterActionBirthdayInDatabase(birthday, action, exception);
    }

    public CallbackActionBirthday getCallbackActionBirthday() {
        return callbackActionBirthday;
    }

    public void setCallbackActionBirthday(CallbackActionBirthday callbackActionBirthday) {
        this.callbackActionBirthday = callbackActionBirthday;
    }

    /**
     * Callback for do action after insert/update/deleteById birthday of contact in database
     */
    public interface CallbackActionBirthday {

        enum Action {
            UNDEFINED,
            ADD,
            UPDATE,
            REMOVE
        }

        void afterActionBirthdayInDatabase(DateUnknownYear birthday, Action action, Exception exception);
    }

    /**
     * AsyncTask who store the birthday in database for a specific contact
     */
    public static class AddBirthdayContactProvider extends ContactProvider {

        private long rawContactId;

        public AddBirthdayContactProvider(Activity context, long rawContactId, DateUnknownYear birthday) {
            super(context, birthday);
            this.rawContactId = rawContactId;
            this.action = CallbackActionBirthday.Action.ADD;
        }

        @Override
        protected Exception doInBackground(Void... params) {
            try {
                ArrayList<ContentProviderOperation> ops = new ArrayList<>();
                ContentProviderOperation.Builder contentBuilder =  ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
                        .withValue(ContactsContract.Data.RAW_CONTACT_ID, rawContactId)
                        .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE)
                        .withValue(ContactsContract.CommonDataKinds.Event.START_DATE, birthday.toBackupString())
                        .withValue(ContactsContract.CommonDataKinds.Event.TYPE, ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY);
                Log.d(getClass().getSimpleName(), "Add birthday " + birthday);
                ops.add(contentBuilder.build());
                ContentProviderResult[] results = context.getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
                if(results[0] == null)
                    throw new Exception("Unable to add new birthday");
            } catch(Exception e) {
                return e;
            }
            return null;
        }
    }

    /**
     * AsyncTask who store the birthday in database for a specific contact
     */
    public static class RemoveBirthdayContactProvider extends ContactProvider {

        protected long dataId;

        public RemoveBirthdayContactProvider(Activity context, long dataId, DateUnknownYear birthday) {
            super(context, birthday);
            this.dataId = dataId;
            this.action = CallbackActionBirthday.Action.REMOVE;
        }

        @Override
        protected Exception doInBackground(Void... params) {
            try {
                ArrayList<ContentProviderOperation> ops = new ArrayList<>();
                ContentProviderOperation.Builder contentBuilder =  ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI)
                        .withSelection(
                                ContactsContract.Data._ID + " =? AND " +
                                ContactsContract.Data.MIMETYPE + " =? AND " +
                                ContactsContract.CommonDataKinds.Event.START_DATE + " =? AND " +
                                ContactsContract.CommonDataKinds.Event.TYPE + " =?"
                                , new String[]{String.valueOf(dataId),
                                    ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE,
                                    birthday.toBackupString(),
                                    String.valueOf(ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY)});
                Log.d(getClass().getSimpleName(), "Delete birthday " + birthday);
                ops.add(contentBuilder.build());
                context.getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
            } catch(Exception e) {
                return e;
            }
            return null;
        }
    }

    /**
     * Created by joker on 24/04/17.
     */
    public static class UpdateBirthdayContactProvider extends ContactProvider {

        private long dataId;
        private DateUnknownYear oldBirthday;

        public UpdateBirthdayContactProvider(Activity context, long dataId, DateUnknownYear oldBirthday, DateUnknownYear newBirthday) {
            super(context, newBirthday);
            this.dataId = dataId;
            this.oldBirthday = oldBirthday;
            this.action = CallbackActionBirthday.Action.UPDATE;
        }

        @Override
        protected Exception doInBackground(Void... voids) {
            try {
                ArrayList<ContentProviderOperation> ops = new ArrayList<>();
                ContentProviderOperation.Builder contentBuilder =  ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI)
                        .withSelection(ContactsContract.Data._ID + " =? AND " +
                                        ContactsContract.Data.MIMETYPE + " =? AND " +
                                        ContactsContract.CommonDataKinds.Event.START_DATE + " =? AND " +
                                        ContactsContract.CommonDataKinds.Event.TYPE + " =?"
                                , new String[]{String.valueOf(dataId),
                                        ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE,
                                        oldBirthday.toBackupString(),
                                        String.valueOf(ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY)})
                        .withValue(ContactsContract.CommonDataKinds.Event.START_DATE, birthday.toBackupString());
                Log.d(getClass().getSimpleName(), "Update birthday " + oldBirthday + " to " + birthday);
                ops.add(contentBuilder.build());
                ContentProviderResult[] results = context.getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
                if(results[0].count == 0)
                    return new Exception("Unable to update birthday");
            } catch(Exception e) {
                return e;
            }
            return null;
        }
    }
} public class EventLoader {

    private final static String TAG = "EventLoader";

    /**
     * Return new event from contact or null if not found
     * @param context Context to call
     * @param contact Contact associated with event
     * @return Next event in the year or null if not fund
     */
    private synchronized static CalendarEvent getNextEventFromContact(Context context, Contact contact) throws EventException {
        Long[] eventTimes = new Long[1];
        eventTimes[0] = new DateTime(contact.getNextBirthday())
                .withHourOfDay(0)
                .withMinuteOfHour(0)
                .withSecondOfMinute(0)
                .withMillisOfSecond(0)
                .withZoneRetainFields(DateTimeZone.UTC)
                .toDateTime().toDate().getTime();
        List<CalendarEvent> calendarEvents = getEventsFromContact(context, contact, eventTimes);
        if(calendarEvents.isEmpty())
            throw new EventException("Unable to getAutoSmsById next event from contact : " + contact.toString());
        else {
            CalendarEvent event = calendarEvents.get(0);
            Log.d(TAG, "Get next event " + event + " from contact " + contact);
            return event;
        }
    }

    /**
     * Return each event from contact
     * @param context Context to call
     * @param contact Contact associated with events
     * @param years List of event's years
     * @return Events for each year
     */
    private synchronized static List<CalendarEvent> getEventsFromContactWithYears(Context context, Contact contact, List<Integer> years) {
        Long[] eventTimes = new Long[years.size()];
        for(int i = 0; i < years.size(); i++) {
            int year = years.get(i);
            long eventTime = new DateTime(contact.getBirthday().getDateWithYear(year))
                    .withHourOfDay(0)
                    .withMinuteOfHour(0)
                    .withSecondOfMinute(0)
                    .withMillisOfSecond(0)
                    .withZoneRetainFields(DateTimeZone.UTC)
                    .toDateTime().toDate().getTime();
            eventTimes[i] = eventTime;
        }
        List<CalendarEvent> events = getEventsFromContact(context, contact, eventTimes);
        Log.d(TAG, "Get events (" + events.size() + ") from contact " + contact + " with year " + years);
        return events;
    }

    /**
     * Utility class for getAutoSmsById all events from contact with list of StartTime
     * @param context Context to call
     * @param contact Contact link
     * @param eventTimes List of events' StartTime
     * @return List of events
     */
    private synchronized static List<CalendarEvent> getEventsFromContact(Context context, Contact contact, Long[] eventTimes) {
        /* Two ways
            - Get events days of anniversary and filter with name (use for the first time)
            - Create links Event-Contact in custom table (may have bugs if event remove manually from calendar)
        */
        List<CalendarEvent> calendarEvents = new ArrayList<>();

        if(contact.hasBirthday()) {
            String[] projection = new String[] {
                    CalendarContract.Events._ID,
                    CalendarContract.Events.TITLE,
                    CalendarContract.Events.DESCRIPTION,
                    CalendarContract.Events.DTSTART,
                    CalendarContract.Events.DTEND,
                    CalendarContract.Events.EVENT_TIMEZONE,
                    CalendarContract.Events.ALL_DAY};
            /*
             * Get newt event who have an all day in the day of the event with name of contact in title
             */
            String where = CalendarContract.Events.DTSTART + " IN " + String.valueOf(QueryTool.getString(eventTimes)) +
                    " AND " + CalendarContract.Events.TITLE + " LIKE ?";
            String[] whereParam = {"%" + contact.getName() + "%"};
            // TODO better retrieve

            ContentResolver contentResolver = context.getContentResolver();
            Cursor cursor = contentResolver.query(
                    CalendarLoader.getBirthdayAdapterUri(context, CalendarContract.Events.CONTENT_URI),
                    projection,
                    where,
                    whereParam,
                    null);
            if(cursor != null) {
                cursor.moveToFirst();
                while (!cursor.isAfterLast()) {
                    CalendarEvent calendarEvent;
                    long id = cursor.getLong(cursor.getColumnIndex(CalendarContract.Events._ID));
                    String title = cursor.getString(cursor.getColumnIndex(CalendarContract.Events.TITLE));
                    String description = cursor.getString(cursor.getColumnIndex(CalendarContract.Events.DESCRIPTION));
                    boolean allDay = cursor.getInt(cursor.getColumnIndex(CalendarContract.Events.ALL_DAY)) > 0;
                    if(allDay) {
                        Date dateStart = new DateTime(
                                cursor.getLong(cursor.getColumnIndex(CalendarContract.Events.DTSTART)),
                                DateTimeZone.forID(cursor.getString(cursor.getColumnIndex(CalendarContract.Events.EVENT_TIMEZONE))))
                                .withZone(DateTimeZone.getDefault())
                                .toDate();
                        calendarEvent = new CalendarEvent(title, dateStart, true);
                    } else {
                        Date dateStart = new DateTime(
                                cursor.getLong(cursor.getColumnIndex(CalendarContract.Events.DTSTART)),
                                DateTimeZone.forID(cursor.getString(cursor.getColumnIndex(CalendarContract.Events.EVENT_TIMEZONE))))
                                .withZone(DateTimeZone.getDefault())
                                .toDate();
                        Date dateEnd = new DateTime(
                                cursor.getLong(cursor.getColumnIndex(CalendarContract.Events.DTEND)),
                                DateTimeZone.forID(cursor.getString(cursor.getColumnIndex(CalendarContract.Events.EVENT_TIMEZONE))))
                                .withZone(DateTimeZone.getDefault())
                                .toDate();
                        calendarEvent = new CalendarEvent(title, dateStart, dateEnd);
                    }
                    calendarEvent.setDescription(description);
                    calendarEvent.setId(id);
                    calendarEvents.add(calendarEvent);
                    cursor.moveToNext();
                }
                cursor.close();
            }
        }
        return calendarEvents;
    }

    /**
     * Get the next event or create a new event if not exists
     * @param context Context to call
     * @param contact Contact link
     * @return The next event
     * @throws EventException If event can't be getAutoSmsById after creation
     */
    public synchronized static CalendarEvent getNextEventOrCreateNewFromContact(Context context, Contact contact) throws EventException {
        try {
            return getNextEventFromContact(context, contact);
        } catch (EventException e) {
            // If next event do not exists, create all events missing (end of 5 years)
            // TODO Replace by saveEventIfNotExistsFromContactWithBirthday
            saveEventsIfNotExistsFromAllContactWithBirthday(context);
            return getNextEventFromContact(context, contact);
        }
    }

    public synchronized static List<CalendarEvent> getEventsSavedOrCreateNewsForEachYearAfterNextEvent(Context context, Contact contact) throws EventException {
        Log.d(TAG, "Retrieve events saved for each year after next event or create them");
        List<CalendarEvent> eventsSaved = new ArrayList<>();
        CalendarEvent eventToUpdate = getNextEventOrCreateNewFromContact(context, contact);
        // Update events for each year
        EventWithoutYear eventWithoutYear = new EventWithoutYear(eventToUpdate);
        List<CalendarEvent> eventsAfterNeeded = eventWithoutYear.getEventsAfterThisYear();
        List<CalendarEvent> eventsAfterSaved = getEventsFromContactWithYears(
                context, contact, eventWithoutYear.getListOfYearsForEventsAfterThisYear());

        for (CalendarEvent event : eventsAfterNeeded) {
            if (eventsAfterSaved.contains(event)) {
                // For getAutoSmsById id
                event = eventsAfterSaved.get(eventsAfterSaved.indexOf(event));
                eventsSaved.add(event);
            }
        }
        return eventsSaved;
    }

    public synchronized static void updateEvent(Context context, Contact contact, DateUnknownYear newBirthday) throws EventException {
        // TODO UNIFORMISE
        for (CalendarEvent event : getEventsSavedOrCreateNewsForEachYear(context, contact)) {
            // Construct each anniversary of new birthday
            int year = new DateTime(event.getDate()).getYear();
            Date newBirthdayDate = DateUnknownYear.getDateWithYear(newBirthday.getDate(), year);
            event.setDateStart(newBirthdayDate);
            event.setAllDay(true);
            ArrayList<ContentProviderOperation> operations = new ArrayList<>();
            ContentProviderOperation contentProviderOperation = EventProvider.update(event);
            operations.add(contentProviderOperation);
            try {
                ContentProviderResult[] contentProviderResults =
                        context.getContentResolver().applyBatch(CalendarContract.AUTHORITY, operations);
                for(ContentProviderResult contentProviderResult : contentProviderResults) {
                    if (contentProviderResult.count != 0)
                        Log.d(TAG, "Update event : " + event.toString());
                }
            } catch (RemoteException|OperationApplicationException e) {
                Log.e(TAG, "Unable to update event : " + e.getMessage());
            }
        }
    }

    private synchronized static List<CalendarEvent> getEventsSavedForEachYear(Context context, Contact contact) throws EventException {
        Log.d(TAG, "Retrieve events saved for each year");
        List<CalendarEvent> eventsSaved = new ArrayList<>();

        CalendarEvent eventToUpdate = getNextEventFromContact(context, contact);

        // Update events for each year
        EventWithoutYear eventWithoutYear = new EventWithoutYear(eventToUpdate);
        List<CalendarEvent> eventsAroundNeeded = eventWithoutYear.getEventsAroundAndForThisYear();
        List<CalendarEvent> eventsAroundSaved = getEventsFromContactWithYears(
                context, contact, eventWithoutYear.getListOfYearsForEachEvent());

        for (CalendarEvent event : eventsAroundNeeded) {
            if (eventsAroundSaved.contains(event)) {
                // For getAutoSmsById id
                event = eventsAroundSaved.get(eventsAroundSaved.indexOf(event));
                eventsSaved.add(event);
            }
        }
        return eventsSaved;
    }

    private synchronized static List<CalendarEvent> getEventsSavedOrCreateNewsForEachYear(Context context, Contact contact) throws EventException {
        Log.d(TAG, "Retrieve events saved for each year");
        List<CalendarEvent> eventsSaved = new ArrayList<>();

        CalendarEvent eventToUpdate = getNextEventFromContact(context, contact);

        // Update events for each year
        EventWithoutYear eventWithoutYear = new EventWithoutYear(eventToUpdate);
        List<CalendarEvent> eventsAroundNeeded = eventWithoutYear.getEventsAroundAndForThisYear();
        List<CalendarEvent> eventsAroundSaved = getEventsFromContactWithYears(
                context, contact, eventWithoutYear.getListOfYearsForEachEvent());

        for (CalendarEvent event : eventsAroundNeeded) {
            if (eventsAroundSaved.contains(event)) {
                // For getAutoSmsById id
                event = eventsAroundSaved.get(eventsAroundSaved.indexOf(event));
                eventsSaved.add(event);
            } else {
                // TODO Replace by saveEventIfNotExistsFromContactWithBirthday
                saveEventsIfNotExistsFromAllContactWithBirthday(context);
                // TODO getAutoSmsById all news
            }
        }
        return eventsSaved;
    }

    public synchronized static void deleteEventsFromContact(Context context, Contact contact) {
        ArrayList<ContentProviderOperation> operations = new ArrayList<>();
        try {
            for (CalendarEvent event : getEventsSavedForEachYear(context, contact)) {
                operations.add(ReminderProvider.deleteAll(context, event.getId()));
                operations.add(EventProvider.delete(event));
            }
            ContentProviderResult[] contentProviderResults =
                    context.getContentResolver().applyBatch(CalendarContract.AUTHORITY, operations);
            for(ContentProviderResult contentProviderResult : contentProviderResults) {
                Log.d(TAG, contentProviderResult.toString());
                if (contentProviderResult.uri != null)
                    Log.d(TAG, contentProviderResult.uri.toString());
            }
        } catch (RemoteException |OperationApplicationException |EventException e) {
            Log.e(TAG, "Unable to deleteById events : " + e.getMessage());
        }
    }

    public synchronized static void saveEventIfNotExistsFromContactWithBirthday(Context context, Contact contact) {
        //TODO
    }

    /**
     * Save all events and default reminders from contacts with birthday
     * @param context Context to call
     */
    public synchronized static void saveEventsIfNotExistsFromAllContactWithBirthday(Context context) {
        ContentResolver contentResolver = context.getContentResolver();

        if (contentResolver == null) {
            Log.e(TAG, "Unable to getAutoSmsById content resolver!");
            return;
        }

        long calendarId = CalendarLoader.getCalendar(context);
        if (calendarId == -1) {
            Log.e(TAG, "Unable to create calendar");
            return;
        }

        // Sync flow:
        // 1. Clear events table for this account completely
        //CalendarLoader.cleanTables(context, calendarId);
        // 2. Get birthdays from contacts
        // 3. Create events and reminders for each birthday

        //List<ContactEventOperation> contactEventOperationList = new ArrayList<>();
        ArrayList<ContentProviderOperation> allOperationList = new ArrayList<>();

        // iterate through all Contact
        List<Contact> contactList = ContactLoader.getAllContacts(context);

        int backRef = 0;
        for (Contact contact : contactList) {

            // TODO Ids
            Log.d(TAG, "BackRef is " + backRef);

            // If next event in calendar is empty, add new event
            CalendarEvent eventToAdd = CalendarEvent.buildDefaultEventFromContactToSave(context, contact);

            // TODO ENCAPSULATE
            EventWithoutYear eventWithoutYear = new EventWithoutYear(eventToAdd);
            List<CalendarEvent> eventsAroundNeeded = eventWithoutYear.getEventsAroundAndForThisYear();
            List<CalendarEvent> eventsAroundSaved = getEventsFromContactWithYears(
                    context, contact, eventWithoutYear.getListOfYearsForEachEvent());

            for (CalendarEvent event : eventsAroundNeeded) {
                if (!eventsAroundSaved.contains(event)) {

                    // Add event operation in list of contact manager
                    allOperationList.add(EventProvider.insert(context, calendarId, event, contact));

                    int noOfReminderOperations = 0;
                    for (Reminder reminder : eventToAdd.getReminders()) {
                        allOperationList.add(ReminderProvider.insert(context, reminder, backRef));
                        noOfReminderOperations += 1;
                    }
                    // back references for the next reminders, 1 is for the event
                    backRef += 1 + noOfReminderOperations;

                }
            }
        }

        /* Create events with reminders and linkEventContract
         * intermediate commit - otherwise the binder transaction fails on large
         * operationList
         * TODO for large list > 200, make multiple apply
         */
        try {
            Log.d(TAG, "Start applying the batch...");

            /*
             * Apply all Reminder Operations
             */
            ContentProviderResult[] contentProviderResults =
                    contentResolver.applyBatch(CalendarContract.AUTHORITY, allOperationList);
            for(ContentProviderResult contentProviderResult : contentProviderResults) {
                Log.d(TAG, "ReminderOperation apply : " + contentProviderResult.toString());
            }

            Log.d(TAG, "Applying the batch was successful!");
        } catch (RemoteException |OperationApplicationException e) {
            Log.e(TAG, "Applying batch error!", e);
        }
    }

    /**
     * Event exception class
     */
    public static class EventException extends Exception {
        public EventException(String message) {
            super(message);
        }
    }
} public class ReminderLoader {

    public static List<Reminder> getRemindersFromEvent(Context context, CalendarEvent calendarEvent) {
        List<Reminder> reminderList = new ArrayList<>();
        String[] projection = new String[] {
                CalendarContract.Reminders._ID,
                CalendarContract.Reminders.EVENT_ID,
                CalendarContract.Reminders.MINUTES,
                CalendarContract.Reminders.METHOD};
        String where = CalendarContract.Reminders.EVENT_ID + "=?";
        String[] whereParam = {Long.toString(calendarEvent.getId())};

        ContentResolver contentResolver = context.getContentResolver();
        Cursor cursor = contentResolver.query(
                CalendarLoader.getBirthdayAdapterUri(context, CalendarContract.Reminders.CONTENT_URI),
                projection,
                where,
                whereParam,
                null);
        if(cursor != null) {
            cursor.moveToFirst();
            while (!cursor.isAfterLast()) {
                Reminder reminder = new Reminder(
                        calendarEvent.getDate(),
                        cursor.getInt(cursor.getColumnIndex(CalendarContract.Reminders.MINUTES)));
                reminder.setId(cursor.getLong(cursor.getColumnIndex(CalendarContract.Reminders._ID)));
                reminderList.add(reminder);
                cursor.moveToNext();
            }
            cursor.close();
        }
        return reminderList;
    }
} public class IntentCall {

    private IntentCall() {}

    /**
     * Open calendar application at specific time
     * @param context Context to call
     * @param date Date to show in calendar
     */
    public static void openCalendarAt(android.content.Context context, java.util.Date date) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon();
        builder.appendPath("time");
        ContentUris.appendId(builder, calendar.getTimeInMillis());
        Intent intent = new Intent(Intent.ACTION_VIEW)
                .setData(builder.build());
        try {
            context.startActivity(intent);
        } catch(ActivityNotFoundException e) {
            Log.e(context.getClass().getSimpleName(), e.toString());
        }
    }

    /**
     * Open phone app for a call
     * @param context Context to call
     * @param phoneNumber Phone Number as a String
     */
    public static void openCallApp(Context context, String phoneNumber) {
        Intent callIntent = new Intent(
                Intent.ACTION_DIAL,
                Uri.parse("tel:" + phoneNumber));
        if (callIntent.resolveActivity(context.getPackageManager()) != null) {
            context.startActivity(callIntent);
        }
    }

    /**
     * Open SMS app
     * @param context Context to call
     * @param phoneNumber Phone Number as a String
     * @param defaultMessage Default message, can be null
     */
    public static void openSMSApp(Context context, String phoneNumber, @Nullable String defaultMessage) {
        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("sms:" + phoneNumber));
        if(defaultMessage != null)
            intent.putExtra("sms_body", defaultMessage);
        context.startActivity(intent);
    }

    /**
     * Open SMS app
     * @param context Context to call
     * @param phoneNumber Phone Number as a String
     */
    public static void openSMSApp(Context context, String phoneNumber) {
        openSMSApp(context, phoneNumber, null);
    }

    /**
     * Open Synchronization settings for contacts and calendar
     * @param context Context to call
     */
    public static void openSyncSettings(Context context) {
        context.startActivity(new Intent(Settings.ACTION_SYNC_SETTINGS));
    }

    /**
     * Open the specific page to make a contribution
     * @param context Context to call
     */
    public static void openContributionPage(Context context) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setData(Uri.parse(Constants.URL_PARTICIPATION));
        context.startActivity(intent);
    }

    /**
     * Open the store for pro version
     * @param context Context to call
     */
    public static void openStoreProApplication(Context context) {
        try {
            context.startActivity(new Intent(Intent.ACTION_VIEW,
                    Uri.parse("market://details?id=" + BuildConfig.PRO_APPLICATION_ID)));
        } catch (android.content.ActivityNotFoundException e) {
            context.startActivity(new Intent(Intent.ACTION_VIEW,
                    Uri.parse("http://play.google.com/store/apps/details?id="
                            + BuildConfig.PRO_APPLICATION_ID)));
        }
    }

    public static final int MODIFY_CONTACT_RESULT_CODE = 1518;
    /**
     * Open activity for contact modifications
     */
     public static void openAppForContactModifications(Activity activity, Contact contact) {
        Intent editIntent = new Intent(Intent.ACTION_EDIT);
        editIntent.setDataAndType(contact.getUri(), ContactsContract.Contacts.CONTENT_ITEM_TYPE);
        // Response in activity
        activity.startActivityForResult(editIntent, MODIFY_CONTACT_RESULT_CODE);
    }

    /**
     * Created by joker on 08/08/17.
     */
    public interface OnContactModify {
        void onEventContactModify();
    }
} public class Utility {

    private static final String FIRST_TIME_KEY = "FIRST_TIME_KEY";

    /**
     * Utility class for setBackground and not depend to SDKVersion
     * @param view View to set background
     * @param drawable Background
     */
    @SuppressWarnings("deprecation")
    public static void setBackground(View view, Drawable drawable) {
        if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) {
            view.setBackgroundDrawable(drawable);
        } else {
            view.setBackground(drawable);
        }
    }

    /***
     * Checks that application runs first time and write flag at SharedPreferences
     * @return true if 1st time
     */
    public static boolean isFirstTime(Activity activity) {
        //TODO Possible bug
        SharedPreferences preferences = activity.getPreferences(MODE_PRIVATE);
        boolean ranBefore = preferences.getBoolean(FIRST_TIME_KEY, false);
        if (!ranBefore) {
            // first time
            SharedPreferences.Editor editor = preferences.edit();
            editor.putBoolean(FIRST_TIME_KEY, true);
            editor.apply();
        }
        return !ranBefore;
    }

    /**
     * Assign custom phrase in TextView to describe the number of days remaining until the birthday
     * @param textView View used to display text
     * @param numberDaysRemaining Number of days left
     */
    public static void assignDaysRemainingInTextView(TextView textView, int numberDaysRemaining) {
        Resources resources = textView.getResources();
        if(numberDaysRemaining == 0) {
            textView.setText(resources.getString(R.string.dialog_select_birthday_zero_day_left));
        } else if(numberDaysRemaining == 1){
            textView.setText(resources.getString(R.string.dialog_select_birthday_one_day_left));
        } else{
            textView.setText(resources.getString(R.string.dialog_select_birthday_number_days_left, numberDaysRemaining));
        }
    }

}
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.kunzisoft.autosms.database.AutoSmsDbHelper;
import com.kunzisoft.autosms.model.AutoSms;
import com.kunzisoft.remembirthday.BuildConfig;
import com.kunzisoft.remembirthday.R;
import com.kunzisoft.remembirthday.adapter.AutoMessageAdapter;
import com.kunzisoft.remembirthday.adapter.MenuAdapter;
import com.kunzisoft.remembirthday.adapter.ReminderCalendarNotificationsAdapter;
import com.kunzisoft.remembirthday.adapter.observer.AutoSmsDatabaseObserver;
import com.kunzisoft.remembirthday.adapter.observer.AutoSmsToastObserver;
import com.kunzisoft.remembirthday.adapter.observer.ReminderCalendarProviderObserver;
import com.kunzisoft.remembirthday.adapter.observer.ReminderToastObserver;
import com.kunzisoft.remembirthday.animation.AnimationViewCircle;
import com.kunzisoft.remembirthday.element.CalendarEvent;
import com.kunzisoft.remembirthday.element.Contact;
import com.kunzisoft.remembirthday.element.DateUnknownYear;
import com.kunzisoft.remembirthday.element.PhoneNumber;
import com.kunzisoft.remembirthday.element.ProxyAutoMessage;
import com.kunzisoft.remembirthday.element.Reminder;
import com.kunzisoft.remembirthday.exception.NoPhoneNumberException;
import com.kunzisoft.remembirthday.exception.PhoneNumberNotInitializedException;
import com.kunzisoft.remembirthday.factory.ActionContactMenu;
import com.kunzisoft.remembirthday.factory.MenuAction;
import com.kunzisoft.remembirthday.factory.MenuActionAutoMessage;
import com.kunzisoft.remembirthday.factory.MenuActionCalendar;
import com.kunzisoft.remembirthday.factory.MenuActionCall;
import com.kunzisoft.remembirthday.factory.MenuActionGift;
import com.kunzisoft.remembirthday.factory.MenuActionMessage;
import com.kunzisoft.remembirthday.factory.MenuActionReminder;
import com.kunzisoft.remembirthday.factory.MenuContact;
import com.kunzisoft.remembirthday.factory.MenuContactCreator;
import com.kunzisoft.remembirthday.preference.PreferencesManager;
import com.kunzisoft.remembirthday.provider.ContactLoader;
import com.kunzisoft.remembirthday.provider.ContactPhoneNumberLoader;
import com.kunzisoft.remembirthday.provider.ContactProvider;
import com.kunzisoft.remembirthday.provider.EventLoader;
import com.kunzisoft.remembirthday.provider.ReminderLoader;
import com.kunzisoft.remembirthday.utility.IntentCall;
import com.kunzisoft.remembirthday.utility.Utility;
import java.util.List;
package com.kunzisoft.remembirthday.activity;




/**
 * Activity who showMessage the details of buddy selected
 */
public class DetailsBuddyFragment extends Fragment implements ActionContactMenu{

    private static final String TAG = "DetailsBuddyFragment";

    private static final String CONTACT_KEY = "CONTACT_KEY";

    private static final String CALENDAR_DIALOG_TAG = "CALENDAR_DIALOG_TAG";
    private static final String SPECIAL_FEATURES_DIALOG_TAG = "SPECIAL_FEATURES_DIALOG_TAG";

    private Contact contact;

    protected RecyclerView autoMessagesListView;
    protected AutoMessageAdapter autoMessagesAdapter;

    protected RecyclerView remindersListView;
    protected ReminderCalendarNotificationsAdapter remindersAdapter;

    private RecyclerView menuListView;
    private GridLayoutManager gridLayoutManager;
    private MenuAdapter menuAdapter;
    private int spanCount = 3;
    private MenuContact menuContact;

    private View menuView;
    private View menuViewButton;
    private AnimationViewCircle menuAnimationViewCircle;

    private DialogFragment calendarDialog;
    private DialogFragment specialFeaturesDialog;

    public void setBuddy(Contact currentContact) {
        Bundle args = new Bundle();
        args.putParcelable(BuddyActivity.EXTRA_BUDDY, currentContact);
        setArguments(args);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.fragment_details_buddy, container, false);

        TextView dayAndMonthTextView = (TextView) root.findViewById(R.id.fragment_details_buddy_dayAndMonth);
        TextView yearTextView = (TextView) root.findViewById(R.id.fragment_details_buddy_year);
        TextView daysLeftTextView = (TextView) root.findViewById(R.id.fragment_details_buddy_days_left);
        View selectBirthdayButton = root.findViewById(R.id.fragment_details_buddy_date);

        // Animation init
        menuView = root.findViewById(R.id.fragment_details_buddy_add_menu);
        menuViewButton = root.findViewById(R.id.fragment_details_buddy_add_button);
        menuAnimationViewCircle = AnimationViewCircle.build(menuView);

        menuListView = (RecyclerView) root.findViewById(R.id.fragment_details_buddy_menu_list);
        gridLayoutManager = new GridLayoutManager(getContext(), spanCount);
        menuListView.setLayoutManager(gridLayoutManager);

        // List of reminders elements
        remindersListView = (RecyclerView) root.findViewById(R.id.fragment_details_buddy_list_reminders);
        LinearLayoutManager linearLayoutManagerReminder = new LinearLayoutManager(getContext());
        linearLayoutManagerReminder.setOrientation(LinearLayoutManager.VERTICAL);
        remindersListView.setLayoutManager(linearLayoutManagerReminder);

        // List of auto messages elements
        autoMessagesListView = (RecyclerView) root.findViewById(R.id.fragment_details_buddy_list_auto_messages);
        LinearLayoutManager linearLayoutManagerAutoMessage = new LinearLayoutManager(getContext());
        linearLayoutManagerAutoMessage.setOrientation(LinearLayoutManager.VERTICAL);
        autoMessagesListView.setLayoutManager(linearLayoutManagerAutoMessage);

        // Contact attributes
        contact = null;
        if(savedInstanceState != null)
            contact = savedInstanceState.getParcelable(CONTACT_KEY);
        else if(getArguments()!=null) {
            contact = getArguments().getParcelable(BuddyActivity.EXTRA_BUDDY);
        }
        if(contact != null) {
            // For insert memory getAutoSmsById RawId only when showMessage details
            setHasOptionsMenu(true);

            ContactLoader.assignRawContactIdToContact(getContext(), contact);

            selectBirthdayButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    ((AnniversaryDialogOpen) getActivity()).openAnniversaryDialogSelection(contact);
                }
            });

            if(contact.hasBirthday()) {
                // Display date
                DateUnknownYear currentBuddyBirthday = contact.getBirthday();

                // Assign text for day and month
                dayAndMonthTextView.setText(currentBuddyBirthday.toStringMonthAndDay(java.text.DateFormat.FULL));

                // Assign text for year
                if (contact.getBirthday().containsYear()) {
                    yearTextView.setVisibility(View.VISIBLE);
                    yearTextView.setText(currentBuddyBirthday.toStringYear());
                } else {
                    yearTextView.setVisibility(View.GONE);
                    yearTextView.setText("");
                }
                // Number days left before birthday
                Utility.assignDaysRemainingInTextView(daysLeftTextView, contact.getBirthdayDaysRemaining());

                // Animation for menu
                menuViewButton.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View viewButton) {
                        menuAnimationViewCircle
                                .startPoint(menuView.getWidth() - 80, 0)
                                .animate();
                    }
                });
            } else {
                //TODO Error
            }
        }
        return root;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        // Initialize dialogs
        calendarDialog = new NeedCalendarDialogFragment();
        specialFeaturesDialog = new NeedSpecialFeaturesDialogFragment();

        // Initialize adapters
        if(contact != null && contact.hasBirthday()) {
            if(PreferencesManager.isCustomCalendarActive(getContext())) {
                // Add default reminders and link view to adapter
                remindersAdapter = new ReminderCalendarNotificationsAdapter(getContext(), contact.getBirthday());
                remindersListView.setAdapter(remindersAdapter);

                // Build default elements
                try {
                    CalendarEvent event = EventLoader.getNextEventOrCreateNewFromContact(getContext(), contact);
                    List<Reminder> reminders = ReminderLoader.getRemindersFromEvent(getContext(), event);