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 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 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 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 CalendarLoader {

    private static final String TAG = "CalendarLoader";
    private static String CALENDAR_COLUMN_NAME = "birthday_adapter";

    /**
     * Builds URI for Birthday Adapter based on account. Ensures that only the calendar of Birthday
     * Adapter is chosen.
     */
    public static Uri getBirthdayAdapterUri(Context context, Uri uri) {
        return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
                .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, CalendarAccount.getAccountName(context))
                .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarAccount.getAccountType(context)).build();
    }

    /**
     * Updates calendar color
     */
    @SuppressWarnings("deprecation")
    public static void updateCalendarColor(Context context) {
        int color = PreferencesManager.getCustomCalendarColor(context);
        ContentResolver contentResolver = context.getContentResolver();

        Uri uri = ContentUris.withAppendedId(
                CalendarLoader.getBirthdayAdapterUri(context, CalendarContract.Calendars.CONTENT_URI),
                getCalendar(context));

        Log.d(TAG, "Updating calendar color to " + color + " with uri " + uri.toString());

        ContentProviderClient client = contentResolver
                .acquireContentProviderClient(CalendarContract.AUTHORITY);
        if(client != null) {
            ContentValues values = new ContentValues();
            values.put(CalendarContract.Calendars.CALENDAR_COLOR, color);
            try {
                client.update(uri, values, null, null);
            } catch (RemoteException e) {
                Log.e(TAG, "Error while updating calendar color!", e);
            }

            if (android.os.Build.VERSION.SDK_INT < 24) {
                client.release();
            } else {
                client.close();
            }
        }
    }

    /**
     * Gets calendar id, when no calendar is present, create one!
     */
    public static long getCalendar(Context context) {
        ContentResolver contentResolver = context.getContentResolver();

        // Find the calendar if we've got one
        Uri calenderUri = getBirthdayAdapterUri(context, CalendarContract.Calendars.CONTENT_URI);

        // be sure to select the birthday calendar only (additionally to appendQueries in
        // getBirthdayAdapterUri for Android < 4)
        Cursor cursor = contentResolver.query(calenderUri, new String[]{BaseColumns._ID},
                CalendarContract.Calendars.ACCOUNT_NAME + " = ? AND " + CalendarContract.Calendars.ACCOUNT_TYPE + " = ?",
                new String[]{CalendarAccount.getAccountName(context), CalendarAccount.getAccountType(context)}, null);

        try {
            if (cursor != null && cursor.moveToNext()) {
                return cursor.getLong(0);
            } else {
                ArrayList<ContentProviderOperation> operationList = new ArrayList<>();

                ContentProviderOperation.Builder builder = ContentProviderOperation
                        .newInsert(calenderUri);
                builder.withValue(CalendarContract.Calendars.ACCOUNT_NAME, CalendarAccount.getAccountName(context));
                builder.withValue(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarAccount.getAccountType(context));
                builder.withValue(CalendarContract.Calendars.NAME, CALENDAR_COLUMN_NAME);
                builder.withValue(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,
                        context.getString(R.string.calendar_display_name));
                builder.withValue(CalendarContract.Calendars.CALENDAR_COLOR, PreferencesManager.getCustomCalendarColor(context));
                //if (BuildConfig.DEBUG) {
                //    builder.withValue(CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, CalendarContract.Calendars.CAL_ACCESS_EDITOR);
                //} else {
                builder.withValue(CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, CalendarContract.Calendars.CAL_ACCESS_READ);
                //}
                builder.withValue(CalendarContract.Calendars.OWNER_ACCOUNT, CalendarAccount.getAccountName(context));
                builder.withValue(CalendarContract.Calendars.SYNC_EVENTS, 1);
                builder.withValue(CalendarContract.Calendars.VISIBLE, 1);
                operationList.add(builder.build());
                try {
                    contentResolver.applyBatch(CalendarContract.AUTHORITY, operationList);
                } catch (Exception e) {
                    Log.e(TAG, "getCalendar() failed", e);
                    return -1;
                }
                return getCalendar(context);
            }
        } finally {
            if (cursor != null && !cursor.isClosed())
                cursor.close();
        }
    }

    public static void cleanTables(Context context, long calendarId) {

        // empty table
        // with additional selection of calendar id, necessary on Android < 4 to remove events only
        // from birthday calendar
        int delEventsRows = context.getContentResolver().delete(getBirthdayAdapterUri(context, CalendarContract.Events.CONTENT_URI),
                CalendarContract.Events.CALENDAR_ID + " = ?", new String[]{String.valueOf(calendarId)});
        Log.i(TAG, "Events of birthday calendar is now empty, deleted " + delEventsRows
                + " rows!");
        Log.i(TAG, "Reminders of birthday calendar is now empty!");
    }
} 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 EventProvider {

    private static final String TAG = "EventProvider";

    /**
     * Get a new ContentProviderOperation to insert an event
     */
    public static ContentProviderOperation insert(Context context, long calendarId,
                                                  CalendarEvent event, @Nullable Contact contact) {
        ContentProviderOperation.Builder builder;

        builder = ContentProviderOperation.newInsert(CalendarLoader.getBirthdayAdapterUri(context, CalendarContract.Events.CONTENT_URI));
        builder.withValue(CalendarContract.Events.CALENDAR_ID, calendarId);
        assignValuesInBuilder(builder, event);

        builder.withValue(CalendarContract.Events.STATUS, CalendarContract.Events.STATUS_CONFIRMED);

        /*
         * Enable reminders for this event
         * Note: Needs to be explicitly set on Android < 4 to enable reminders
         */
        builder.withValue(CalendarContract.Events.HAS_ALARM, 1);

        /*
         * Set availability to free.
         * Note: HTC calendar (4.0.3 Android + HTC Sense 4.0) will show a conflict with other events
         * if availability is not set to free!
         */
        if (Build.VERSION.SDK_INT >= 14) {
            builder.withValue(CalendarContract.Events.AVAILABILITY, CalendarContract.Events.AVAILABILITY_FREE);
        }

        // add button to open contact
        if (Build.VERSION.SDK_INT >= 16 && contact != null && contact.getLookUpKey() != null) {
            builder.withValue(CalendarContract.Events.CUSTOM_APP_PACKAGE, context.getPackageName());
            Uri contactLookupUri = Uri.withAppendedPath(
                    ContactsContract.Contacts.CONTENT_LOOKUP_URI, contact.getLookUpKey());
            builder.withValue(CalendarContract.Events.CUSTOM_APP_URI, contactLookupUri.toString());
        }
        Log.d(TAG, "Build insert event : " + event);
        return builder.build();
    }

    /**
     * Update the specific event, id must be specified
     * @param event Event to update
     * @return ContentProviderOperation to apply or null if no id
     */
    public static ContentProviderOperation update(CalendarEvent event) {
        if(event.hasId()) {
            ContentProviderOperation.Builder builder;
            builder = ContentProviderOperation.newUpdate(
                    ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, event.getId()));
            // Push values
            assignValuesInBuilder(builder, event);
            Log.d(TAG, "Build update event : " + event);
            return builder.build();
        } else {
            Log.e(TAG, "Can't update the event, there is no id");
            return null;
        }
    }

    /**
     * Utility method for add values in Builder
     * @param builder ContentProviderOperation.Builder
     * @param event Event to add
     */
    private static synchronized void assignValuesInBuilder(ContentProviderOperation.Builder builder, CalendarEvent event) {
        if(event.isAllDay()) {
            // ALL_DAY events must be UTC
            DateTime dateTimeStartUTC = new DateTime(event.getDateStart()).withZoneRetainFields(DateTimeZone.UTC);
            DateTime dateTimeStopUTC = new DateTime(event.getDateStop()).withZoneRetainFields(DateTimeZone.UTC);
            builder.withValue(CalendarContract.Events.DTSTART, dateTimeStartUTC.toDate().getTime());
            builder.withValue(CalendarContract.Events.DTEND, dateTimeStopUTC.toDate().getTime());
            builder.withValue(CalendarContract.Events.EVENT_TIMEZONE, "UTC");
            builder.withValue(CalendarContract.Events.ALL_DAY, 1);
        } else {
            builder.withValue(CalendarContract.Events.DTSTART, event.getDateStart().getTime());
            builder.withValue(CalendarContract.Events.DTEND, event.getDateStop().getTime());
            builder.withValue(CalendarContract.Events.EVENT_TIMEZONE, TimeZone.getDefault().getID());
        }
        builder.withValue(CalendarContract.Events.TITLE, event.getTitle());
    }

    /**
     * Delete the specific event, id must be specified
     * @param event Event to deleteById
     * @return ContentProviderOperation to apply or null if no id
     */
    public static ContentProviderOperation delete(CalendarEvent event) {
        if(event.hasId()) {
            ContentProviderOperation.Builder builder;
            builder = ContentProviderOperation.newDelete(
                    ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, event.getId()));
            Log.d(TAG, "Build deleteById event : " + event);
            return builder.build();
        } else {
            Log.e(TAG, "Can't deleteById the event, there is no id");
            return null;
        }
    }


} public class ReminderProvider {

    private static final String TAG = "ReminderProvider";

    public static ContentProviderOperation insert(Context context, long eventId, Reminder reminder) {
        ContentProviderOperation.Builder builder = ContentProviderOperation
                .newInsert(CalendarLoader.getBirthdayAdapterUri(context, CalendarContract.Reminders.CONTENT_URI));
        builder.withValue(CalendarContract.Reminders.EVENT_ID, eventId);
        return insert(builder, reminder);
    }


    public static ContentProviderOperation insert(Context context, Reminder reminder, int backref) {
        ContentProviderOperation.Builder builder = ContentProviderOperation
                .newInsert(CalendarLoader.getBirthdayAdapterUri(context, CalendarContract.Reminders.CONTENT_URI));
        /*
         * add reminder to last added event identified by backRef
         * see http://stackoverflow.com/questions/4655291/semantics-of-
         * withvaluebackreference
         */
        builder.withValueBackReference(CalendarContract.Reminders.EVENT_ID, backref);
        return insert(builder, reminder);
    }

    private static ContentProviderOperation insert(ContentProviderOperation.Builder builder, Reminder reminder) {
        builder.withValue(CalendarContract.Reminders.MINUTES, reminder.getMinutesBeforeEvent());
        builder.withValue(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT);
        return builder.build();
    }

    public static ContentProviderOperation update(Context context, long eventId, Reminder reminder) {
        ContentProviderOperation.Builder builder = ContentProviderOperation
                .newUpdate(CalendarLoader.getBirthdayAdapterUri(context, CalendarContract.Reminders.CONTENT_URI))
            .withSelection(CalendarContract.Reminders._ID + " =?"
                    + " AND " + CalendarContract.Reminders.EVENT_ID + " =?"
                , new String[]{String.valueOf(reminder.getId()),
                    String.valueOf(eventId)})
            .withValue(CalendarContract.Reminders.MINUTES, reminder.getMinutesBeforeEvent());
        return  builder.build();
    }

    public static ContentProviderOperation updateWithUnknownId(Context context, long eventId, Reminder reminder, int newMinutes) {
        // TODO update
        ContentProviderOperation.Builder builder = ContentProviderOperation
                .newUpdate(CalendarLoader.getBirthdayAdapterUri(context, CalendarContract.Reminders.CONTENT_URI))
                .withSelection(CalendarContract.Reminders.EVENT_ID + " =?"
                                + " AND " + CalendarContract.Reminders.MINUTES + " =?"
                        , new String[]{String.valueOf(eventId),
                                String.valueOf(reminder.getMinutesBeforeEvent())})
                .withValue(CalendarContract.Reminders.MINUTES, newMinutes);
        return  builder.build();
    }

    public static ContentProviderOperation delete(Context context, long eventId, Reminder reminder) {
        ContentProviderOperation.Builder builder = ContentProviderOperation
                .newDelete(CalendarLoader.getBirthdayAdapterUri(context, CalendarContract.Reminders.CONTENT_URI))
                .withSelection(CalendarContract.Reminders._ID + " =?"
                                + " AND " + CalendarContract.Reminders.EVENT_ID + " =?"
                        , new String[]{String.valueOf(reminder.getId()),
                                String.valueOf(eventId)});
        return  builder.build();
    }

    public static ContentProviderOperation deleteWithUnknownId(Context context, long eventId, Reminder reminder) {
        // TODO delete
        ContentProviderOperation.Builder builder = ContentProviderOperation
                .newDelete(CalendarLoader.getBirthdayAdapterUri(context, CalendarContract.Reminders.CONTENT_URI))
                .withSelection(CalendarContract.Reminders.EVENT_ID + " =?"
                                + " AND " + CalendarContract.Reminders.MINUTES + " =?"
                        , new String[]{String.valueOf(eventId),
                                String.valueOf(reminder.getMinutesBeforeEvent())});
        return  builder.build();
    }

    public static ContentProviderOperation deleteAll(Context context, long eventId) {
        ContentProviderOperation.Builder builder = ContentProviderOperation
                .newDelete(CalendarLoader.getBirthdayAdapterUri(context, CalendarContract.Reminders.CONTENT_URI))
                .withSelection(CalendarContract.Reminders.EVENT_ID + " =?"
                        , new String[]{String.valueOf(eventId)});
        return  builder.build();
    }
}
import android.app.Activity;
import android.content.ContentProviderOperation;
import android.content.Intent;
import android.content.OperationApplicationException;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import android.provider.CalendarContract;
import android.provider.ContactsContract;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import com.kunzisoft.remembirthday.R;
import com.kunzisoft.remembirthday.element.CalendarEvent;
import com.kunzisoft.remembirthday.element.Contact;
import com.kunzisoft.remembirthday.element.DateUnknownYear;
import com.kunzisoft.remembirthday.element.Reminder;
import com.kunzisoft.remembirthday.preference.PreferencesManager;
import com.kunzisoft.remembirthday.provider.ContactProvider;
import com.kunzisoft.remembirthday.provider.CalendarLoader;
import com.kunzisoft.remembirthday.provider.ContactLoader;
import com.kunzisoft.remembirthday.provider.EventProvider;
import com.kunzisoft.remembirthday.provider.ReminderProvider;
import java.util.ArrayList;
package com.kunzisoft.remembirthday.activity;




/**
 * Created by joker on 19/01/17.
 */
public class ListContactsActivity extends AppCompatActivity
        implements ContactProvider.CallbackActionBirthday, AnniversaryDialogOpen {

    public static final int INSERT_BIRTHDAY_RESULT_CODE = 1619;
    private static final int INSERT_CONTACT_RESULT_CODE = 1567;

    // Dialog for birthday selection
    private static final String TAG_SELECT_DIALOG = "TAG_SELECT_DIALOG";
    private BirthdayDialogFragment dialogSelection;
    private Contact contactWithRawIdSelected;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_list_contacts);

        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        toolbar.setTitle(getString(R.string.select_contact_title));
        setSupportActionBar(toolbar);
        assert getSupportActionBar() != null;
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setDisplayShowHomeEnabled(true);

        FloatingActionButton addContactButton = (FloatingActionButton) findViewById(R.id.fab_add_buddy);
        addContactButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(Intent.ACTION_INSERT, ContactsContract.Contacts.CONTENT_URI);
                startActivityForResult(intent, INSERT_CONTACT_RESULT_CODE);
            }
        });

        // Initialize dialog for birthday selection
        dialogSelection = (BirthdayDialogFragment) getSupportFragmentManager().findFragmentByTag(TAG_SELECT_DIALOG);
        if(dialogSelection == null)
            dialogSelection = new BirthdayDialogFragment();

        dialogSelection.setOnClickListener(new BirthdayDialogFragment.OnClickBirthdayListener() {
            @Override
            public void onClickPositiveButton(DateUnknownYear dateUnknownYear) {
                contactWithRawIdSelected.setBirthday(dateUnknownYear);

                // All operations to apply
                ArrayList<ContentProviderOperation> allOperationList = new ArrayList<>();

                // Add new birthday in database
                ContactProvider.AddBirthdayContactProvider addBirthdayContactProvider =
                        new ContactProvider.AddBirthdayContactProvider(
                                ListContactsActivity.this,
                                contactWithRawIdSelected.getRawId(),
                                dateUnknownYear);
                addBirthdayContactProvider.setCallbackActionBirthday(ListContactsActivity.this);
                addBirthdayContactProvider.execute();
                setResult(Activity.RESULT_OK);
                finish();

                // Add new event in calendar
                if(PreferencesManager.isCustomCalendarActive(ListContactsActivity.this)) {
                    // TODO Encapsulate
                    long calendarId = CalendarLoader.getCalendar(ListContactsActivity.this);
                    if (calendarId != -1) {
                        CalendarEvent event = CalendarEvent.buildDefaultEventFromContactToSave(
                                ListContactsActivity.this,
                                contactWithRawIdSelected);
                        allOperationList.add(
                                EventProvider.insert(ListContactsActivity.this,