사실 Calendar provider를 비롯하여 기본 content provider를 직접 사용하는 경우는 많이 없습니다. 대부분 개인 정보에 접근하게 되기에 관련 제약과 권한이 필요해집니다. 게다가 구글 캘린더같이 활용할 수 있는 API도 있기 때문에 대체 방안을 사용하기도 하죠. Room 같은 내부 DB로 데이터를 구성하고 별도의 UI를 제공하기도 합니다. Intent를 제공할 수도 있지만 매우 한정적인 연산만 할 수 있습니다. 그럼에도 content provider에 대한 이해를 위해 기본 provider에 대해 학습하고 예제를 진행할 것입니다. 다음 포스팅에 다른 기본 content provider인 contacts provider에 대해 알아보고 마무리할 예정입니다.
Calendar provider 간단한 예제를 진행하겠습니다. Provider는 비동기로 실행되어야 합니다. Calendar provider 사용에 집중하기 위해 간단히 thread에서 진행할 겁니다. 로컬 calendar의 여러 데이터 모델를 읽거나 쓸 겁니다.
사용자 권한을 추가해야합니다. Calendar의 데이터를 읽기 위해서는 READ_CALENDAR , 삭제하거나 삽입 및 업데이트일 때는 WRITE_CALENDAR 가 필수로 있어야 합니다. 저희는 읽기와 쓰기 모두 필요하기 때문에 2가지 모두 추가해줍니다.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"...>
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
...
</manifest>
Provider 관련된 코드는 CalendarManager 클래스에서 작성한 후 MainActivity에서 호출할겁니다. Provider 사용에 필요한 context는 생성자를 통해 받습니다.
특정 Calendar에 대해 쿼리하기 위해서는 이를 구별할 수 있는 조건이 필요합니다. 로컬 calendar에 대해서만 읽고 싶은데 어떤 값들을 가지는지 모르니 조건을 설정하기 어렵습니다. 이를 해결하는 가장 쉬운 방법은 전체 calendar 정보를 가져와 확인하는 겁니다. 우선 가져올 정보(필드명)를 담는 배열과 해당 배열의 인덱스를 의미하는 변수를 선언합니다.
private static final String[] CALENDAR_PROJECTION = new String[] {
CalendarContract.Calendars._ID,
CalendarContract.Calendars.ACCOUNT_NAME,
CalendarContract.Calendars.ACCOUNT_TYPE,
CalendarContract.Calendars.CALENDAR_DISPLAY_NAME
};
private static final int CALENDAR_PROJECTION_CALENDAR_ID_IDX=0;
private static final int CALENDAR_PROJECTION_ACCOUNT_NAME_IDX=1;
private static final int CALENDAR_PROJECTION_ACCOUNT_TYPE_IDX=2;
private static final int CALENDAR_PROJECTION_DISPLAY_NAME_IDX=3;
전체 calendar 정보를 가져오면 되기에 calendar의 Uri와 calendar의 특정 필드를 의미하는 Projection만 전달하면 됩니다.
Cursor cursor=context.getContentResolver().query(
CalendarContract.Calendars.CONTENT_URI,
CALENDAR_PROJECTION,
null,
null,
null
);
다음은 결과로 받은 cursor를 통해 데이터를 얻을 겁니다. 위에서 본 Projection 배열 인덱스로 설정된 상수를 사용하여 각 필드의 값을 반환합니다.
if(cursor!=null){
while(cursor.moveToNext()){
Log.d(LOG_TAG, cursor.getString(CALENDAR_PROJECTION_CALENDAR_ID_IDX));
Log.d(LOG_TAG, cursor.getString(CALENDAR_PROJECTION_ACCOUNT_NAME_IDX));
Log.d(LOG_TAG, cursor.getString(CALENDAR_PROJECTION_ACCOUNT_TYPE_IDX));
Log.d(LOG_TAG, cursor.getString(CALENDAR_PROJECTION_DISPLAY_NAME_IDX));
Log.d(LOG_TAG,"------------------------------------------");
}
Log.d(LOG_TAG,"===========================================================");
cursor.close();
}
cursor가 가리키는 행이 끝날 때까지 while문으로 반복하며 출력합니다.

로그 출력 내용을 보면 저희가 찾는 로컬 calendar의 id는 1인 걸 확인할 수 있습니다.
이제 이 id를 통해 로컬 calendar의 정보를 출력할 수 있습니다. 전체적 로직은 위에서 진행된 쿼리와 비슷합니다. Id가 1인 특정 calendar에 대해 쿼리할 것이기에 조건만 추가하면 됩니다. 쿼리의 기준이 되는 selection에는 calendar ID 컬럼 이름과 함께 =? 를 사용합니다. 여기서 ? 는 실제 값으로 대체되는 매개변수 자리 표시자이며, 해당 값은 selectionArgs 배열에 전달됩니다.
String selection= CalendarContract.Calendars._ID+"=?";
String [] selectionArgs=new String[]{String.valueOf(calendarId)};
Cursor cursor=context.getContentResolver().query(
CalendarContract.Calendars.CONTENT_URI,
CALENDAR_PROJECTION,
selection,
selectionArgs,
null
);
if(cursor!=null){
cursor.moveToNext();
Log.d(LOG_TAG, cursor.getString(CALENDAR_PROJECTION_ACCOUNT_NAME_IDX));
Log.d(LOG_TAG, cursor.getString(CALENDAR_PROJECTION_ACCOUNT_TYPE_IDX));
Log.d(LOG_TAG, cursor.getString(CALENDAR_PROJECTION_DISPLAY_NAME_IDX));
Log.d(LOG_TAG,"===========================================================");
cursor.close();
}

Calendar의 정보를 변경할 수도 있습니다. 이번 코드에서는 display name을 변경할 겁니다. ContentValues를 사용해 열 이름과 변경할 값을 지정합니다. withAppendedId() 를 이용해 calendar Uri에 calendar id를 추가하면 로컬 calendar를 가리키는 전체 URI가 생성됩니다. 이 URI와 ContentValues를 update() 에 전달하면 됩니다. 이때 update()는 변경된 열의 수를 반환합니다.
ContentValues values = new ContentValues();
values.put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, "my local calendar");
Uri uriUpdated = ContentUris.withAppendedId(
CalendarContract.Calendars.CONTENT_URI, calendarId);
int rows = context.getContentResolver()
.update(uriUpdated, values, null, null);
Log.d(LOG_TAG, "업데이트된 열: " + rows);
Log.d(LOG_TAG,"===========================================================");

반환된 열의 수와 실제로 수정한 열의 수가 일치하므로 정상적으로 작동되었음을 확인할 수 있습니다.
Calendar를 다시 쿼리하면 이름이 바뀐 것을 확인할 수 있습니다

이제 event를 추가할 겁니다. 저희는 2025년 4월 2일 부터 2025년 7월 2일까지 A-room302에서 10:30~11:30까지 회의를 진행한다고 가정하겠습니다.
event 추가에는 CALENDAR_ID , DTSTART , EVENT_TIMEZONE 가 필수로 포함되어야 합니다. 반복 event이기 때문에 추가적으로 DURATION + RRUE 또는 DURATION +RDATE 가 필수로 포함되어야 하는데 DURATION + RRUE 으로 진행하겠습니다. DTSTART 의 경우 Calendar의 set() 으로 쉽게 시작 날짜 및 시간을 설정할 수 있습니다. 하지만 DURATION 과 RRUE 는 해당 문법에 맞게 작성해야합니다. 우선 지속 시간을 의미하는 DURATION 의 경우 RFC5545 에 따라 다음과 같은 형식을 가집니다.
P[n]Y[n]M[n]W[n]DT[n]H[n]M[n]S
P는 맨 앞에 두는 접두사이며 Y,M,W,D는 각각 연, 월, 주, 일을 의미합니다. T는 시간을 추가할 경우 구분을 위해 사용됩니다. T이후의 H,M,S는 시, 분, 초를 의미합니다. 저희는 1시간 동안 지속되기 때문에 PT1H 가 됩니다.
반복 규칙을 의미하는 RULE의 경우 다음의 형식을 가집니다.
| 필드 | 설명 | 예시 |
|---|---|---|
| FREQ | 반복 주기(DAILY, WEEKLY, MONTHLY, YEARLY) | FREQ=DAILY |
| INTERVAL | 반복 간격(기본값 1) | INTERVAL=2 → 2일마다 |
| UNTIL | 반복 종료 시점(UTC 형식) | UNTIL=20250701T000000Z |
| COUNT | 반복 횟수 | COUNT=10 → 총 10회 반복 |
| BYDAY | 요일 조건(MO, TU, WE, TH, FR, SA, SU) | BYDAY=MO,WE,FR |
| BYMONTHDAY | 특정 월의 일자 | BYMONTHDAY=15 |
| BYMONTH | 반복되는 월 | BYMONTH=1,7 → 1월과 7월에 반복 |
저희는 매주 진행되며 7월 2일 11:30까지 지속되기 때문에 FREQ=WEEKLY;UNTIL=20250702T113000Z 가 됩니다.
long startMillis;
Calendar beginTime=Calendar.getInstance();
beginTime.set(2025,Calendar.APRIL,2,10,30);
startMillis=beginTime.getTimeInMillis();
ContentValues values=new ContentValues();
values.put(CalendarContract.Events.CALENDAR_ID, calendarId);
values.put(CalendarContract.Events.ORGANIZER,"organizer@mail.com");
values.put(CalendarContract.Events.TITLE,"meetingA");
values.put(CalendarContract.Events.EVENT_LOCATION,"A-room302");
values.put(CalendarContract.Events.EVENT_TIMEZONE,"Asia/Seoul");
values.put(CalendarContract.Events.DTSTART,startMillis);
values.put(CalendarContract.Events.DURATION, "PT1H");
values.put(CalendarContract.Events.RRULE, "FREQ=WEEKLY;UNTIL=20250702T113000Z");
Uri uri=context.getContentResolver()
.insert(CalendarContract.Events.CONTENT_URI,values);
long eventId=0;
if(uri!=null){
eventId=Long.parseLong(Objects.requireNonNull(uri.getLastPathSegment()));
}
return eventId;
로컬 calendar 전체 uri를 얻기 위해 calendar uri에 id를 추가한거처럼 event id를 얻으려면 uri의 마지막 요소를 가져오면 됩니다. 해당 id를 통해 event에 attendee 또는 reminder을 추가하는 등의 calendar 작업을 수행할 수 있습니다.


Event의 장소를 변경할겁니다. 회의실이 A-room302에서 A-room304으로 변경되었습니다. 전체 로직은 calendar에서의 update와 유사합니다. 위에서 얻은 event id를 event Uri에 추가하여 저희가 추가한 event의 전체 Uri를 구합니다.
ContentValues values=new ContentValues();
values.put(CalendarContract.Events.EVENT_LOCATION,"A-room304");
Uri uri=ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI,eventId);
int row=context.getContentResolver()
.update(uri,values,null,null);
Log.d(LOG_TAG, "업데이트된 이벤트 열: " + row);


Calendar Provider는 VIEW intent를 통해 event를 볼 수 있는 두 가지 방식이 있습니다.특정 event 보기와 특정 날짜의 calendar 열기입니다. 특정 event를 보려면 event의 Uri에 event id를 더하여 특정 event의 전체 Uri를 구합니다. Intent에 View action과 해당 uri를 전달해주면 끝입니다.
Uri uri=ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI,eventId);
Intent intent=new Intent(Intent.ACTION_VIEW).setData(uri);
context.startActivity(intent);

특정 날짜를 보려면 확인하고 싶은 시간을 설정하고 Uri에 전달하면 됩니다. Calendar의 Uri에 buildUpon() 호출하여 URI를 조작할 수 있는 Uri.Builder를 생성합니다. 그럼 해당 Uri에 time 경로를 추가하고 확인할 시간을 더해줍니다. 최종 URI는 content://com.android.calendar/time/<startMillis> 형태가 됩니다.
long startMillis;
Calendar beginTime=Calendar.getInstance();
beginTime.set(2025,Calendar.APRIL,2,10,30);
startMillis=beginTime.getTimeInMillis();
Uri.Builder builder=CalendarContract.CONTENT_URI.buildUpon();
builder.appendPath("time");
ContentUris.appendId(builder, startMillis);
Intent intent=new Intent(Intent.ACTION_VIEW).setData(builder.build());
context.startActivity(intent);

Attendee는 ATEUNTEE_NAME 를 제외한 모든 필드를 필수로 포함해야 합니다. 즉 EVENT_ID , ATTENDEE_EMAIL , ATTENDEE_RELATIONSHIP , ATTENDEE_TYPE , ATTENDEE_STATUS 모두 포함해야합니다. 또한 event의 id를 Uri에 추가하는 것이 아닌 값으로 전달해야합니다.
ContentValues values=new ContentValues();
values.put(CalendarContract.Attendees.EVENT_ID,eventId);
values.put(CalendarContract.Attendees.ATTENDEE_NAME,name);
values.put(CalendarContract.Attendees.ATTENDEE_EMAIL,email);
values.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP,relationship);
values.put(CalendarContract.Attendees.ATTENDEE_TYPE,type);
values.put(CalendarContract.Attendees.ATTENDEE_STATUS,status);
Uri uri= context.getContentResolver()
.insert(CalendarContract.Attendees.CONTENT_URI, values);
long attendeeId=0;
if(uri!=null){
attendeeId=Long.parseLong(Objects.requireNonNull(uri.getLastPathSegment()));
}
return attendeeId;
ATTENDEE_RELATIONSHIP , ATTENDEE_TYPE , ATTENDEE_STATUS 가 가질 수 있는 값들은 다음을 참고하세요. insert() 결과로 얻은 attendee id는 attendee 수정이나 삭제에 사용됩니다.

해당 attendee를 클릭하면 해당 상세 정보를 확인할 수 있습니다


Attendee type를 변경하겠습니다. 수정 로직은 큰 변경 없이 비슷하게 진행됩니다.
ContentValues values=new ContentValues();
values.put(
CalendarContract.Attendees.ATTENDEE_TYPE,
CalendarContract.Attendees.TYPE_REQUIRED
);
Uri uri=ContentUris.withAppendedId(
CalendarContract.Attendees.CONTENT_URI,
attendeeId
);
int row=context.getContentResolver().update(uri,values,null,null);
Log.d(LOG_TAG, "업데이트된 참석자 열: " + row);


삭제의 경우 해당 id를 전달해주기만 하면 됩니다.
Uri uri=ContentUris.withAppendedId(
CalendarContract.Attendees.CONTENT_URI,
attendeeId
);
int row=context.getContentResolver().delete(uri,null,null);
Log.d(LOG_TAG, "삭제된 참석자 열: " + row);


Reminder의 경우 모든 필드를 포함해야합니다. EVENT_ID , MINUTES , METHOD 입니다. METHOD 가 가질 수 있는 값들은 다음을 참고하세요. 메서드에 reminder의 Uri과 추가할 값들을 추가해주면 됩니다.
ContentValues values=new ContentValues();
values.put(CalendarContract.Reminders.EVENT_ID,eventId);
values.put(CalendarContract.Reminders.MINUTES,15);
values.put(
CalendarContract.Reminders.METHOD,
CalendarContract.Reminders.METHOD_ALERT
);
Uri uri=context.getContentResolver()
.insert(CalendarContract.Reminders.CONTENT_URI,values);
long reminderId=0;
if(uri!=null){
reminderId=Long.parseLong(Objects.requireNonNull(uri.getLastPathSegment()));
}
return reminderId;

위에서 얻은 reminder의 id를 통해 특정 reminder의 전체 Uri를 구하고 수정할 값을 전달하면 됩니다.
ContentValues values=new ContentValues();
values.put(CalendarContract.Reminders.MINUTES,20);
Uri uri=ContentUris.withAppendedId(
CalendarContract.Reminders.CONTENT_URI,
reminderId
);
int row=context.getContentResolver().update(uri,values,null,null);
Log.d(LOG_TAG, "업데이트된 알림 열: " + row);




Instance는 특정 데이터의 유형이 아닌 event에 대한 단일의 시작과 종료 시간를 의미합니다. 정보 조회용으로 사용되는 데이터입니다. 검색할 event에 대한 시간은 event view때 처럼 Uri에 추가하여 설정해줍니다. 나머지는 calendar에서의 query처럼 검색할 필드 명을 담은 배열과 특정 event의 값을 가져올 수 있도록 조건을 전달해주면 됩니다.
private static final String [] INSTANCE_PROJECTION = new String[]{
CalendarContract.Instances.BEGIN,
CalendarContract.Instances.START_DAY,
CalendarContract.Instances.START_MINUTE,
CalendarContract.Instances.END,
CalendarContract.Instances.END_DAY,
CalendarContract.Instances.END_MINUTE
};
private static final int INSTANCE_PROJECTION_BEGIN_IDX=0;
private static final int INSTANCE_PROJECTION_START_DAY_IDX=1;
private static final int INSTANCE_PROJECTION_START_MINUTE_IDX=2;
private static final int INSTANCE_PROJECTION_END_IDX=3;
private static final int INSTANCE_PROJECTION_END_DAY_IDX=4;
private static final int INSTANCE_PROJECTION_END_MINUTE_IDX=5;
Calendar beginTime = Calendar.getInstance();
beginTime.set(2025,Calendar.APRIL,2,10,30);
long startMillis = beginTime.getTimeInMillis();
Calendar endTime = Calendar.getInstance();
endTime.set(2025,Calendar.MAY,2,10,30);
long endMillis = endTime.getTimeInMillis();
Uri.Builder builder = CalendarContract.Instances.CONTENT_URI.buildUpon();
ContentUris.appendId(builder, startMillis);
ContentUris.appendId(builder, endMillis);
String selection= CalendarContract.Instances.EVENT_ID+"=?";
String [] selectionArgs=new String[]{String.valueOf(eventId)};
Cursor cursor=context.getContentResolver().query(
builder.build(),
INSTANCE_PROJECTION,
selection,
selectionArgs,
null
);
if(cursor!=null){
while(cursor.moveToNext()){
Log.d(LOG_TAG, "begin: "+
cursor.getString(INSTANCE_PROJECTION_BEGIN_IDX));
Log.d(LOG_TAG, "start day: "+
cursor.getString(INSTANCE_PROJECTION_START_DAY_IDX));
Log.d(LOG_TAG, "start minute: "+
cursor.getString(INSTANCE_PROJECTION_START_MINUTE_IDX));
Log.d(LOG_TAG, "end: "+
cursor.getString(INSTANCE_PROJECTION_END_IDX));
Log.d(LOG_TAG, "end day: "+
cursor.getString(INSTANCE_PROJECTION_END_DAY_IDX));
Log.d(LOG_TAG, "end minute: "+
cursor.getString(INSTANCE_PROJECTION_END_MINUTE_IDX));
Log.d(LOG_TAG,"------------------------------------------");
}
cursor.close();
}
저희는 반복 event였기에 while문으로 반복하며 출력해줍니다.


Intent를 사용하면 권한 permission이 필요하지 않다는 장점이 있지만 설정할 수 있는 값들이 매우 제한적입니다. 그렇기에 저희는 삽입 예시만 간단히 진행해보겠습니다. 해당 제한은 다음을 참고하세요
기존에서는 ContentValues를 통해 값을 전달하였지만 intent에서는 setData() 으로 Uri, putExtra() 를 통해 실제 값을 전달합니다. 실행하게 되면 전달한 값들이 포함된 calendar 편집 화면으로 이동하게 됩니다. 사용자가 이를 편집, 저장할 수 있는데 취소 또한 할 수 있기 때문에 해당 event의 id를 얻을 수 없습니다.
2025년 6월 1일에 7:30~8:30 요가 그룹 클래스를 체육관에서 하는 단일 event를 추가하겠습니다. 이때 일정에 여유가 있지 않기에 event에 다른 일정을 추가하지 못합니다.
Calendar beginTime = Calendar.getInstance();
beginTime.set(2025, 6, 1, 7, 30);
Calendar endTime = Calendar.getInstance();
endTime.set(2025, 6, 1, 8, 30);
Intent intent = new Intent(Intent.ACTION_INSERT)
.setData(CalendarContract.Events.CONTENT_URI)
.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, beginTime.getTimeInMillis())
.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endTime.getTimeInMillis())
.putExtra(CalendarContract.Events.TITLE, "Yoga")
.putExtra(CalendarContract.Events.DESCRIPTION, "Group class")
.putExtra(CalendarContract.Events.EVENT_LOCATION, "The gym")
.putExtra(
CalendarContract.Events.AVAILABILITY,
CalendarContract.Events.AVAILABILITY_BUSY
);
context.startActivity(intent);
사용자에 의해 추가 입력을 받을 수 있기 때문에 contentResolver으로 추가하던 경우와 달리 EVENT_TIMEZONE 가 포함되지 않아도 코드가 실행이 됩니다.



public class CalendarManager {
private final Context context;
private final long calendarId;
private static final String LOG_TAG="CALENDAR";
private static final String[] CALENDAR_PROJECTION = new String[] {
CalendarContract.Calendars._ID,
CalendarContract.Calendars.ACCOUNT_NAME,
CalendarContract.Calendars.ACCOUNT_TYPE,
CalendarContract.Calendars.CALENDAR_DISPLAY_NAME
};
private static final int CALENDAR_PROJECTION_CALENDAR_ID_IDX=0;
private static final int CALENDAR_PROJECTION_ACCOUNT_NAME_IDX=1;
private static final int CALENDAR_PROJECTION_ACCOUNT_TYPE_IDX=2;
private static final int CALENDAR_PROJECTION_DISPLAY_NAME_IDX=3;
private static final String [] INSTANCE_PROJECTION = new String[]{
CalendarContract.Instances.BEGIN,
CalendarContract.Instances.START_DAY,
CalendarContract.Instances.START_MINUTE,
CalendarContract.Instances.END,
CalendarContract.Instances.END_DAY,
CalendarContract.Instances.END_MINUTE
};
private static final int INSTANCE_PROJECTION_BEGIN_IDX=0;
private static final int INSTANCE_PROJECTION_START_DAY_IDX=1;
private static final int INSTANCE_PROJECTION_START_MINUTE_IDX=2;
private static final int INSTANCE_PROJECTION_END_IDX=3;
private static final int INSTANCE_PROJECTION_END_DAY_IDX=4;
private static final int INSTANCE_PROJECTION_END_MINUTE_IDX=5;
public CalendarManager(Context context, long calendarId){
this.context=context;
this.calendarId=calendarId;
}
public void queryAllCalendar(){
Cursor cursor=context.getContentResolver().query(
CalendarContract.Calendars.CONTENT_URI,
CALENDAR_PROJECTION,
null,
null,
null
);
if(cursor!=null){
while(cursor.moveToNext()){
Log.d(LOG_TAG, cursor.getString(CALENDAR_PROJECTION_CALENDAR_ID_IDX));
Log.d(LOG_TAG, cursor.getString(CALENDAR_PROJECTION_ACCOUNT_NAME_IDX));
Log.d(LOG_TAG, cursor.getString(CALENDAR_PROJECTION_ACCOUNT_TYPE_IDX));
Log.d(LOG_TAG, cursor.getString(CALENDAR_PROJECTION_DISPLAY_NAME_IDX));
Log.d(LOG_TAG,"------------------------------------------");
}
Log.d(LOG_TAG,"===========================================================");
cursor.close();
}
}
public void queryCalendar(){
String selection= CalendarContract.Calendars._ID+"=?";
String [] selectionArgs=new String[]{String.valueOf(calendarId)};
Cursor cursor=context.getContentResolver().query(
CalendarContract.Calendars.CONTENT_URI,
CALENDAR_PROJECTION,
selection,
selectionArgs,
null
);
if(cursor!=null){
cursor.moveToNext();
Log.d(LOG_TAG, cursor.getString(CALENDAR_PROJECTION_ACCOUNT_NAME_IDX));
Log.d(LOG_TAG, cursor.getString(CALENDAR_PROJECTION_ACCOUNT_TYPE_IDX));
Log.d(LOG_TAG, cursor.getString(CALENDAR_PROJECTION_DISPLAY_NAME_IDX));
Log.d(LOG_TAG,"===========================================================");
cursor.close();
}
}
public void updateCalendar(){
ContentValues values = new ContentValues();
values.put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, "my local calendar");
Uri uriUpdated = ContentUris.withAppendedId(
CalendarContract.Calendars.CONTENT_URI, calendarId);
int rows = context.getContentResolver()
.update(uriUpdated, values, null, null);
Log.d(LOG_TAG, "업데이트된 열: " + rows);
Log.d(LOG_TAG,"===========================================================");
}
public long insertEvent(){
long startMillis;
Calendar beginTime=Calendar.getInstance();
beginTime.set(2025,Calendar.APRIL,2,10,30);
startMillis=beginTime.getTimeInMillis();
ContentValues values=new ContentValues();
values.put(CalendarContract.Events.CALENDAR_ID, calendarId);
values.put(CalendarContract.Events.ORGANIZER,"organizer@mail.com");
values.put(CalendarContract.Events.TITLE,"meetingA");
values.put(CalendarContract.Events.EVENT_LOCATION,"A-room302");
values.put(CalendarContract.Events.EVENT_TIMEZONE,"Asia/Seoul");
values.put(CalendarContract.Events.DTSTART,startMillis);
values.put(CalendarContract.Events.DURATION, "PT1H");
values.put(CalendarContract.Events.RRULE, "FREQ=WEEKLY;UNTIL=20250702T113000Z");
Uri uri=context.getContentResolver()
.insert(CalendarContract.Events.CONTENT_URI,values);
long eventId=0;
if(uri!=null){
eventId=Long.parseLong(Objects.requireNonNull(uri.getLastPathSegment()));
}
return eventId;
}
public void insertEventUsingIntent(){
Calendar beginTime = Calendar.getInstance();
beginTime.set(2025, 6, 1, 7, 30);
Calendar endTime = Calendar.getInstance();
endTime.set(2025, 6, 1, 8, 30);
Intent intent = new Intent(Intent.ACTION_INSERT)
.setData(CalendarContract.Events.CONTENT_URI)
.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, beginTime.getTimeInMillis())
.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endTime.getTimeInMillis())
.putExtra(CalendarContract.Events.TITLE, "Yoga")
.putExtra(CalendarContract.Events.DESCRIPTION, "Group class")
.putExtra(CalendarContract.Events.EVENT_LOCATION, "The gym")
.putExtra(CalendarContract.Events.AVAILABILITY,
CalendarContract.Events.AVAILABILITY_BUSY);
context.startActivity(intent);
}
public void updateEvent(long eventId){
ContentValues values=new ContentValues();
values.put(CalendarContract.Events.EVENT_LOCATION,"A-room304");
Uri uri=ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI,eventId);
int row=context.getContentResolver()
.update(uri,values,null,null);
Log.d(LOG_TAG, "업데이트된 이벤트 열: " + row);
Log.d(LOG_TAG,"===========================================================");
}
public void viewEvent(long eventId){
Uri uri=ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI,eventId);
Intent intent=new Intent(Intent.ACTION_VIEW).setData(uri);
context.startActivity(intent);
}
public void viewSpecifiedEvent(){
long startMillis;
Calendar beginTime=Calendar.getInstance();
beginTime.set(2025,Calendar.APRIL,2,10,30);
startMillis=beginTime.getTimeInMillis();
Uri.Builder builder=CalendarContract.CONTENT_URI.buildUpon();
builder.appendPath("time");
ContentUris.appendId(builder, startMillis);
Intent intent=new Intent(Intent.ACTION_VIEW)
.setData(builder.build());
context.startActivity(intent);
}
public long insertAttendee(
long eventId, String name, String email,
int relationship, int type, int status
){
ContentValues values=new ContentValues();
values.put(CalendarContract.Attendees.EVENT_ID,eventId);
values.put(CalendarContract.Attendees.ATTENDEE_NAME,name);
values.put(CalendarContract.Attendees.ATTENDEE_EMAIL,email);
values.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP,relationship);
values.put(CalendarContract.Attendees.ATTENDEE_TYPE,type);
values.put(CalendarContract.Attendees.ATTENDEE_STATUS,status);
Uri uri= context.getContentResolver()
.insert(CalendarContract.Attendees.CONTENT_URI, values);
long attendeeId=0;
if(uri!=null){
attendeeId=Long.parseLong(Objects.requireNonNull(uri.getLastPathSegment()));
}
return attendeeId;
}
public void updateAttendee(long attendeeId){
ContentValues values=new ContentValues();
values.put(CalendarContract.Attendees.ATTENDEE_TYPE, CalendarContract.Attendees.TYPE_REQUIRED);
Uri uri=ContentUris.withAppendedId(CalendarContract.Attendees.CONTENT_URI,attendeeId);
int row=context.getContentResolver().update(uri,values,null,null);
Log.d(LOG_TAG, "업데이트된 참석자 열: " + row);
Log.d(LOG_TAG,"===========================================================");
}
public void deleteAttendee(long attendeeId){
Uri uri=ContentUris.withAppendedId(CalendarContract.Attendees.CONTENT_URI,attendeeId);
int row=context.getContentResolver().delete(uri,null,null);
Log.d(LOG_TAG, "삭제된 참석자 열: " + row);
Log.d(LOG_TAG,"===========================================================");
}
public long insertReminder(long eventId){
ContentValues values=new ContentValues();
values.put(CalendarContract.Reminders.EVENT_ID,eventId);
values.put(CalendarContract.Reminders.MINUTES,15);
values.put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT);
Uri uri=context.getContentResolver()
.insert(CalendarContract.Reminders.CONTENT_URI,values);
long reminderId=0;
if(uri!=null){
reminderId=Long.parseLong(Objects.requireNonNull(uri.getLastPathSegment()));
}
return reminderId;
}
public void updateReminder(long reminderId){
ContentValues values=new ContentValues();
values.put(CalendarContract.Reminders.MINUTES,20);
Uri uri=ContentUris.withAppendedId(CalendarContract.Reminders.CONTENT_URI,reminderId);
int row=context.getContentResolver().update(uri,values,null,null);
Log.d(LOG_TAG, "업데이트된 알림 열: " + row);
Log.d(LOG_TAG,"===========================================================");
}
public void queryInstance(long eventId){
Calendar beginTime = Calendar.getInstance();
beginTime.set(2025,Calendar.APRIL,2,10,30);
long startMillis = beginTime.getTimeInMillis();
Calendar endTime = Calendar.getInstance();
endTime.set(2025,Calendar.MAY,2,10,30);
long endMillis = endTime.getTimeInMillis();
Uri.Builder builder = CalendarContract.Instances.CONTENT_URI.buildUpon();
ContentUris.appendId(builder, startMillis);
ContentUris.appendId(builder, endMillis);
String selection= CalendarContract.Instances.EVENT_ID+"=?";
String [] selectionArgs=new String[]{String.valueOf(eventId)};
Cursor cursor=context.getContentResolver().query(
builder.build(),
INSTANCE_PROJECTION,
selection,
selectionArgs,
null
);
if(cursor!=null){
while(cursor.moveToNext()){
Log.d(LOG_TAG, "begin: "+
cursor.getString(INSTANCE_PROJECTION_BEGIN_IDX));
Log.d(LOG_TAG, "start day: "+
cursor.getString(INSTANCE_PROJECTION_START_DAY_IDX));
Log.d(LOG_TAG, "start minute: "+
cursor.getString(INSTANCE_PROJECTION_START_MINUTE_IDX));
Log.d(LOG_TAG, "end: "+
cursor.getString(INSTANCE_PROJECTION_END_IDX));
Log.d(LOG_TAG, "end day: "+
cursor.getString(INSTANCE_PROJECTION_END_DAY_IDX));
Log.d(LOG_TAG, "end minute: "+
cursor.getString(INSTANCE_PROJECTION_END_MINUTE_IDX));
Log.d(LOG_TAG,"------------------------------------------");
}
cursor.close();
}
}
}
CalendarManager를 생성하고 해당 메서드를 호출하면 됩니다. 다만 UI 스레드가 차단되지 않게 비동기로 실행되어야 합니다.
또 다른 주의점으로는 런타임 권한 요청이 필요합니다. 안드로이드 6.0(API 수준 23) 이상부터 특정 권한에는 manifest에 등록해도 별도의 요청이 필요합니다. 저희가 필요한 READ_CALENDAR, WRITE_CALENDAR 모두 이에 해당됩니다. 흔히 볼 수 있는 "기기 위치에 엑세스하도록 허용하시겠습니까?"가 런타임 요청입니다. 코드 로직자체는 어렵지 않으니 MainActivity의 전체 코드를 끝으로 마무리 하겠습니다.
public class MainActivity extends AppCompatActivity {
private static final int REQUEST_CALENDAR_PERMISSIONS=100;
private static final String [] CALENDAR_PERMISSIONS={
Manifest.permission.READ_CALENDAR,
Manifest.permission.WRITE_CALENDAR
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
EdgeToEdge.enable(this);
checkAndRequestCalendarPermissions();
}
private void checkAndRequestCalendarPermissions() {
boolean isReadNotGranted=
ContextCompat.checkSelfPermission(this,Manifest.permission.READ_CALENDAR)!=
PackageManager.PERMISSION_GRANTED;
boolean isWriteNotGranted=
ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_CALENDAR)!=
PackageManager.PERMISSION_GRANTED;
if( isReadNotGranted || isWriteNotGranted){
ActivityCompat.requestPermissions(
this,
CALENDAR_PERMISSIONS,
REQUEST_CALENDAR_PERMISSIONS
);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if(requestCode== REQUEST_CALENDAR_PERMISSIONS){
if( grantResults.length>1&&
grantResults[0]== PackageManager.PERMISSION_GRANTED&&
grantResults[1]== PackageManager.PERMISSION_GRANTED
){
useCalendar();
}else{
Toast.makeText(this, "권한 허용 필요",Toast.LENGTH_LONG).show();
}
}
}
private void useCalendar(){
CalendarManager manager=new CalendarManager(this, 1);
Thread thread=new Thread(() -> {
//calendar
manager.queryAllCalendar();
manager.queryCalendar();
manager.updateCalendar();
manager.queryCalendar();
//event
long eventId=manager.insertEvent();
manager.updateEvent(eventId);
manager.viewEvent(eventId);
manager.viewSpecifiedEvent();
//attendee
long attendeeAId=manager.insertAttendee(
eventId,"A","a@mail.com",
CalendarContract.Attendees.RELATIONSHIP_ATTENDEE,
CalendarContract.Attendees.TYPE_OPTIONAL,
CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED
);
manager.insertAttendee(
eventId,"b","a@mail.com",
CalendarContract.Attendees.RELATIONSHIP_ATTENDEE,
CalendarContract.Attendees.TYPE_OPTIONAL,
CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED
);
manager.updateAttendee(attendeeAId);
manager.deleteAttendee(attendeeAId);
//reminder
long reminderId=manager.insertReminder(eventId);
manager.updateReminder(reminderId);
//instance
manager.queryInstance(eventId);
//intent
manager.insertEventUsingIntent();
});
thread.start();
}
}
이전 CONTENT PROVIDER 포스트
<contentProvider_1(개념)>
<contentProvider_2(생성 과정)>
<contentProvider_3(예제)>
<contentProvider_4(calendar 개념)>
다음 CONTENT PROVIDER 포스트
<contentProvider_6(비동기 방식)>
<contentProvider_7(contact)>