Read the original article:How to programmatically create custom calendar events?
Context
Programmatically creating calendar events on HarmonyOS involves using the system's built-in Calendar Manager service. This service provides a structured API for applications to interact with the device's central calendar database, allowing them to schedule events, set reminders, and manage attendee information without needing to build their own calendar system. This is essential for apps that need to create reminders, book appointments, or sync schedules with the user's primary HarmonyOS calendar. The process requires requesting user permissions and using the specific data models defined by the HarmonyOS API.
Description
This guide explains how to develop a feature that creates a new calendar event on a HarmonyOS device. The core steps include requesting necessary permissions from the user, importing the required modules, creating a Calendar object to define the account under which the event will be created, and populating an Event object with all relevant details like title, time, and location. Finally, the calendarManager.addEvent() method is called to insert the new event into the system's calendar database as well calendarManager.editEvent method to insert event with the popup provided by the HarmonyOS. The Calendar Kit is a pre-built set of APIs (Application Programming Interfaces) provided by HarmonyOS to allow developers to interact directly with the device's native calendar service. Think of it as a bridge between your application and the central calendar database on the HarmonyOS device.
Required Permissions
To maintain user privacy and system security, access to the calendar is strictly controlled. Any application that wants to use the Calendar Kit must declare and request explicit permission from the user.
There are two distinct permissions, both classified as normal permissions, meaning they are automatically granted at installation time. However, it is considered a best practice to request them dynamically at runtime to clearly inform the user why they are needed.
-
ohos.permission.READ_CALENDAR- Purpose: This permission grants your application the ability to read data from the device's calendar.
- When is it needed?
- Querying for existing calendar accounts (
getCalendars()). - Fetching a list of events within a date range (
getEvents()). - Reading the details of a specific event (
getEventById()).
- Querying for existing calendar accounts (
-
ohos.permission.WRITE_CALENDAR- Purpose: This permission grants your application the ability to write, edit, and delete data in the device's calendar.
- When is it needed?
- Creating a new event (
addEvent()). - Updating an existing event (
updateEvent()). - Deleting an event (
deleteEvent()). - Adding attendees to an event.
- Creating a new event (
Core Event Management APIs
| Function | Purpose |
|---|---|
getCalendarManager(context) |
Get manager instance. |
createCalendar(calendarAccount) |
Create a new calendar. |
addEvent(event) |
Add event. |
editEvent(event) |
Edit event or create if given event is new in popup form by the user. |
deleteEvent(id) |
Delete event by ID. |
updateEvent(event) |
Update existing event. |
getEvents([filter, keys]) |
Query events. |
calendarManager.Event:
| Property | Type | Description | Example/Options |
|---|---|---|---|
| title | string |
The title/name of the event. | 'title' |
| type | calendarManager.EventType |
The type of the event. Third-party developers are advised not to use IMPORTANT. |
calendarManager.EventType.NORMAL |
| startTime | number |
The start time of the event (epoch timestamp in milliseconds). | date.getTime() |
| endTime | number |
The end time of the event (epoch timestamp in milliseconds). | date.getTime() + 60 * 60 * 1000 |
| reminderTime | number[] |
An array of reminder times (in minutes) before the event starts. |
[10] (a 10-minute reminder) |
| recurrenceRule | Object |
The rule defining how the event repeats. Mandatory for recurring events. | {...} |
| ↳ recurrenceFrequency | calendarManager.RecurrenceFrequency |
The frequency of recurrence (daily, weekly, monthly, yearly). | calendarManager.RecurrenceFrequency.DAILY |
| ↳ count | number |
The number of times the event will recur. Takes precedence over expire if both are set. |
100 |
| ↳ interval | number |
The interval between recurrences, related to the frequency. |
2 (every 2 days) |
| ↳ expire | number |
The expiration time for the recurring event (epoch timestamp in milliseconds). | date.getTime() + 60 * 60 * 1000 * 3 |
| ↳ excludedDates | number[] |
Specific dates (epoch timestamps) to exclude from the recurrence. | [date.getTime() + 60 * 60 * 1000 * 2] |
| service | Object |
(Optional) The service associated with the event for one-click access. | {...} |
| ↳ type | calendarManager.ServiceType |
The type of service (e.g., TRIP, MEETING, WATCHING). | calendarManager.ServiceType.TRIP |
| ↳ uri | string |
The service URI in DeepLink format for redirection. | 'xxx://xxx.xxx.com/xxx' |
| ↳ description | string |
(Optional) An auxiliary description for the service. | 'One-click service' |
Solution / Approach
1.Request Necessary Permissions: First, we must declare the required permissions in our project's module.json5 file under "requestPermissions":
"requestPermissions": [
{
"name": "ohos.permission.READ_CALENDAR",
"reason": "$string:EntryAbility_desc",
"usedScene": {
"when": "inuse"
}
},
{
"name": "ohos.permission.WRITE_CALENDAR",
"reason": "$string:EntryAbility_desc",
"usedScene": {
"when": "inuse"
}
},
]
2.Check permission when the main page is about to appear:
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import PermissionHandler from '../services/PermissionHandler';
import { calendarManager } from '@kit.CalendarKit';
export let calendarMgr: calendarManager.CalendarManager | null = null;
const DOMAIN = 0x0000;
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
try {
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
} catch (err) {
hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
}
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
}
onDestroy(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
if (PermissionHandler.getInstance().checkPermissionGrant()) {
calendarMgr = calendarManager.getCalendarManager(this.context);
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
3.Dynamically Request Calendar Permissions: Check for the calendar permissions:
import { abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import GlobalContext from '../models/GlobalContext';
const CALENDAR_PERMISSIONS: Permissions[] = [
'ohos.permission.READ_CALENDAR',
'ohos.permission.READ_WHOLE_CALENDAR',
'ohos.permission.WRITE_CALENDAR',
'ohos.permission.WRITE_WHOLE_CALENDAR',
]
export default class PermissionHandler {
private static instance: PermissionHandler;
private context?: Context;
constructor(context?: Context) {
this.context = context;
}
static getInstance(): PermissionHandler {
if (!PermissionHandler.instance) {
let context = GlobalContext.getInstance().getContext();
PermissionHandler.instance = new PermissionHandler(context);
}
return PermissionHandler.instance;
}
checkPermissionGrant(): boolean {
let hasPermission = false;
let tokenId: number = 0;
try {
let bundleInfo: bundleManager.BundleInfo =
bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo;
tokenId = appInfo.accessTokenId;
} catch (error) {
const err: BusinessError = error as BusinessError;
hilog.error(0x0000, 'Index',
`Failed to get bundle info for self. Code is ${err.code}, message is ${err.message}`);
}
try {
let atManager = abilityAccessCtrl.createAtManager();
let writeCalendar = atManager.checkAccessTokenSync(tokenId, 'ohos.permission.WRITE_CALENDAR');
let writeWholeCalendar = atManager.checkAccessTokenSync(tokenId, 'ohos.permission.WRITE_WHOLE_CALENDAR');
let readCalendar = atManager.checkAccessTokenSync(tokenId, 'ohos.permission.READ_CALENDAR');
let readWholeCalendar = atManager.checkAccessTokenSync(tokenId, 'ohos.permission.READ_WHOLE_CALENDAR');
hasPermission = writeCalendar === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED &&
readCalendar === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED &&
writeWholeCalendar === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED &&
readWholeCalendar === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
} catch (error) {
const err: BusinessError = error as BusinessError;
hilog.error(0x0000, 'Index', `Failed to check access token. Code is ${err.code}, message is ${err.message}`);
}
if (!hasPermission) {
return this.requestPermissions();
}
4.Request for the calendar permissions: if the permission has not been granted request the permissions from the user when app starts:
requestPermissions(): boolean {
let atManager = abilityAccessCtrl.createAtManager();
try {
atManager.requestPermissionsFromUser(
this.context,
CALENDAR_PERMISSIONS
).then((data) => {
if (data.authResults[0] === -1 || data.authResults[1] === -1) {
if (data.dialogShownResults && (data.dialogShownResults[0] || data.dialogShownResults[1])) {
console.log('Permissions granted')
return true;
} else {
return this.openPermissionsSetting();
}
}
if (data.authResults[0] !== 0) {
return false;
}
return false;
}).catch((err: Error) => {
hilog.error(0x0000, 'Index', 'requestPermissionsFromUser err:' + JSON.stringify(err));
});
} catch (err) {
hilog.error(0x0000, 'Index', 'requestPermissionsFromUser err:' + JSON.stringify(err));
}
return false;
}
5.Open permission granting dialog:
private openPermissionsSetting(): boolean {
let atManager = abilityAccessCtrl.createAtManager();
atManager.requestPermissionOnSetting(
this.context,
CALENDAR_PERMISSIONS
)
.then((data: Array<abilityAccessCtrl.GrantStatus>) => {
console.log('Permission Granted')
return true;
})
.catch((err: BusinessError) => {
hilog.error(0x0000, 'Index', 'data:' + JSON.stringify(err));
});
return false;
}
6.GlobalContext for storing context for later using when creating calendar manager object:
export default class GlobalContext {
private static instance: GlobalContext;
private uiContext?: Context;
static getInstance() {
if (!GlobalContext.instance) {
GlobalContext.instance = new GlobalContext();
}
return GlobalContext.instance;
}
public saveContext(context: Context) {
this.uiContext = context;
}
public getContext() {
return this.uiContext;
}
}
initialize GlobalContexton onWindowStageCreate:
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
let context: Context | undefined = windowStage.getMainWindowSync().getUIContext().getHostContext();
if (context) {
AppStorage.setOrCreate('context', context);
GlobalContext.getInstance().saveContext(context)
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
7.Usage in Component where we can add event to default calendar:
import { calendarManager } from '@kit.CalendarKit';
import PermissionHandler from '../services/PermissionHandler';
@Entry
@Component
struct Index {
private context: Context | undefined = this.getUIContext().getHostContext();
private calendar: calendarManager.Calendar | undefined;
async getCalendar(): Promise<calendarManager.Calendar | undefined> {
try {
let manager = calendarManager.getCalendarManager(this.context);
return await manager.getCalendar();
} catch (err) {
console.error(`Failed to get calendar. Code: ${err.code}, message: ${err.message}`);
}
return undefined;
}
aboutToAppear(): void {
PermissionHandler.getInstance().checkPermissionGrant();
this.getCalendar().then((calendar) => {
this.calendar = calendar;
})
}
build() {
Column() {
Button('Add Event')
.onClick(() => {
try {
let date = new Date();
let event: calendarManager.Event = {
title: 'Event',
type: calendarManager.EventType.NORMAL,
startTime: date.getTime(),
endTime: date.getTime() + 60 * 60 * 1000,
reminderTime: [10],
recurrenceRule: {
recurrenceFrequency: calendarManager.RecurrenceFrequency.DAILY,
count: 100,
interval: 2,
expire: date.getTime() + 60 * 60 * 1000 * 3,
excludedDates: [date.getTime() + 60 * 60 * 1000 * 2]
},
service: {
type: calendarManager.ServiceType.TRIP,
uri: 'xxx://xxx.xxx.com/xxx',
description: 'One-click service'
}
}
this.calendar?.addEvent(event).then((result) => {
this.getUIContext().getRouter().pushUrl({ url: 'pages/EventsPage' }).catch(() => {
})
})
} catch (error) {
}
return 0;
})
.margin({ top: 25 })
Button('Edit Last Event')
Button('Show Events')
.margin({ bottom: 25 })
.onClick(() => {
this.getUIContext().getRouter().pushUrl({ url: 'pages/EventsPage' }).catch(() => {
})
})
}
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(HorizontalAlign.Center)
.height('100%')
.width('100%')
}
}
8.List the added events:
import { calendarManager } from '@kit.CalendarKit';
import { ArcList, ArcListAttribute, ArcListItem, ArcListItemAttribute, LengthMetrics } from '@kit.ArkUI';
import GlobalContext from '../models/GlobalContext';
class ListDataSource<T> implements IDataSource {
private listeners: DataChangeListener[] = [];
private dataArray: T[] = [];
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener);
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
this.listeners.splice(pos, 1);
}
}
public totalCount(): number {
return this.dataArray.length;
}
public getData(index: number): T {
return this.dataArray[index];
}
public pushData(data: T): void {
this.dataArray.push(data);
let index = this.dataArray.length - 1;
this.listeners.forEach(listener => {
listener.onDataAdd(index);
});
}
}
@Entry
@Component
struct EventsPage {
private events: ListDataSource<calendarManager.Event> = new ListDataSource<calendarManager.Event>();
loadEvents() {
const context = GlobalContext.getInstance().getContext();
let calendarMgr = calendarManager.getCalendarManager(context);
calendarMgr?.getCalendar().then((calendar) => {
calendar.getEvents().then((events) => {
events.forEach(event => {
this.events.pushData(event);
});
})
})
}
aboutToAppear(): void {
this.loadEvents()
}
build() {
Flex({
justifyContent: FlexAlign.Start,
direction: FlexDirection.Column,
alignItems: ItemAlign.Center
}) {
ArcList() {
LazyForEach(
this.events,
(event: calendarManager.Event) => {
ArcListItem() {
Text(`Title: ${event.title}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
}
},
(event: calendarManager.Event) => event.title
)
}
.layoutWeight(1)
.scrollBar(BarState.On)
.space(LengthMetrics.px(10))
.width('100%')
.height('100%')
}
.width('100%')
.width('100%')
}
}
Key Takeaways
-
editEvent()provides a user-friendly popup for event confirmation/modification, whileaddEvent()works silently but there is no interface to customize or change the appearance ofeditEventpopup form. - Always handle permissions before calling event-creation APIs to avoid failures.



Top comments (0)