Безысходность и отчаяние я испытывал много дней подряд, пытаясь "подключить" Google календарь к своему приложению. Так долго и так тяжело, как тогда, я не буксовал ни над одной фичей... Я сделал это! Прошло более двух месяцев, пока я и мои почти 200 активных пользователей не протестировали этот функционал в полной мере. Теперь я готов поделиться своим опытом, ибо в сети на русском языке (да и на английском тоже) я не нашел удовлетворяющее меня описание того, как работать с Google календарем через Content Provider. Я работаю над приложением "Учет клиентов для самозанятых". Мне необходимо было реализовать в нем возможность планирования встреч с клиентами. Я сразу остановил свой выбор на Google календаре, т.к. в своей профессиональной деятельности активно им пользуюсь. Более того, мне была важна возможность общего доступа к календарю и его синхронизация с облачным сервисом Google. Супруга у меня тоже самозанятая и нам очень удобно планировать нашу семейную жизнь, поделившись друг с другом доступами для чтения к календарю каждого. Самое простое решение, когда необходим Google календарь - воспользоваться Интентом, с помощью которого можно запускать стандартное приложение Календарь, чтобы добавлять новые события. Мне это решение не подходило по нескольким причинам. Во-первых, помимо добавления событий, мне так же нужно их читать и редактировать. Во-вторых, мое приложение работало со своей локальной Базой Данных, содержащей конфиденциальную информацию (самозанятые коллеги психологи меня поймут), которую не хотелось бы выносить за пределы приложения. И тогда необходимо решать, как локальные данные синхронизировать с данными Google календаря. Вызовом стандартного приложения Календаря не обойтись. Приложение должно само добавлять, редактировать и читать данные из Google календаря. Синхронизировать его с облаком, если это необходимо. Для этого Google предлагает воспользоваться контент провайдером (Content Provider). Излагать подробно теорию не буду, ибо я не профи, а любитель. Прошу заранее прощение за возможные неточности в изложении и косяки в коде. По мере необходимости буду давать ссылки на статьи, из которых сам черпал информацию. Я так понимаю, что доступ к Google календарю в смартфоне похож на доступ к Базе Данных. Точнее, сам Календарь и все, что в нем есть по сути хранится в таблицах, как данные в БД. При помощи Контент провайдера (Content Provider) можно осуществлять запросы в БД Календаря: сохранять, изменять или удалять данные - события, календари, напоминания... Подробнее можете ознакомиться с работой Контент провайдера по ссылке: https://developer.android.com/guide/topics/providers/content-provider-basics Как во многом, что находится вне приложения, сначала необходимо получить к этому доступ. Получение доступа состоит из двух частей: Я на всякий случай запрашиваю доступ и на чтение, и на запись, хотя первое, возможно, делать и не обязательно. Все функции, отвечающие за интеграцию приложения с Google календарем, я упаковал в отдельный класс CalManager, однако доступ у пользователя запрашиваю из MainActivity, т.к. возвращаемый пользователем ответ (ActivityResultCallback), я так понимаю, принимать и обрабатывать возможно только в ней.  Для получения доступа я использую RequestMultiplePermissions контракт, который пришел на замену устаревшего onRequestPermissionsResult. Подробнее о нем можно прочесть здесь: https://developer.android.com/training/permissions/requesting Если кратко, то сначала инициализирую переменную calendarPermission, с помощью которой регистрирую запрос разрешений к Календарю и определяю Callback, который выполнится, когда пользователь отреагирует на просьбу дать доступ. Если разрешения получены, то запускаю функцию initCalendar(), иначе спрашиваю у пользователя "В чем дело, неужели сложно разрешить мне элементарную вещь? :)" - showAskWhyDialog() Мое приложение без доступа к Календарю не работает, поэтому в функции initCalendar() проверяю доступ и опять его запрашиваю. Функция calManager.checkPermission(), описанная в классе CalManager, выглядит следующим образом: Не спрашивайте меня, почему я запрашиваю разрешения на чтение и запись в Календаре, а проверяю только запись... :) Почему-то мой аппарат при попытке приложения запросить доступ повторно, если ранее пользователь отклонил запрос, игнорирует его, как-будто он поставил галочку "Больше не показывать", хотя ее нет в диалоговом окне. Поэтому, если пользователь сразу не дал доступ, то в диалоговом окне я доходчиво ему объясняю, в чем он не прав и направляю его в настройки смартфона, чтобы он дал доступ вручную. А так выглядит переменная getPermissionManually, отвечающая за callBack: Бесконечный цикл - пока не дашь доступ, дальше не пройдешь! No pasaran! :) Далее я буду описывать функции, содержащиеся в классе CalManager, отвечающие в моем приложении за работу Календаря. В первую очередь необходимо выбрать или создать календарь.  В каждой функции, на всякий случай, я проверяю, есть ли доступ к календарю. Не уверен, что это необходимо, но возможен такой вариант, когда пользователь даст доступ, а потом его  попросит обратно, тогда приложение "вылетит" с ошибкой. Случай маловероятный, но лучше перестраховаться. Как видите, получение списка календарей один в один похоже на запрос к Базе Данных. В переменной projection указываем нужные нам имена полей таблицы со списком календарей. В selection - описываем условие, указав только те календари, в которых пользователь считается полноправным владельцем. Потом осуществляем запрос (contentResolver.query) и берем результат из переменной cursor, сохраняя для каждого календаря его имя, аккаунт, тип аккаунта и id. На выходе имеем список календарей calList. Если календарь в списке один, то его и выбираем. Если нет ни одного календаря, то создаем новый. Если календарей больше одного, то проверяем, создан ли локальный календарь "customer_accounting", создаем его, если нет и даем пользователю выбрать календарь. Назначение функции setCalendarVisibilityAndSync() я поясню далее. Тут необходимо сказать, что в приложении можно создавать только локальный календарь. Его нельзя синхронизировать с облаком и просматривать события из него на разных устройствах. Подробнее о том, как работать с Google Календарем через свое приложение можно прочесть здесь: https://developer.android.com/guide/topics/providers/calendar-provider Не сразу я понял, что в поле CALENDAR_TIME_ZONE необходимо указывать не название Зоны в строке, а ее id числом. В остальном новый календарь добавляется, как новая строка в таблицу календарей. Единственное, на что стоит обратить внимание, - это переменная builder. Она, я так понимаю, отвечает за построение Uri - пути, по которому находится необходимое место для записи в таблицу календарей. При ее настройке  я указываю дополнительный параметр CALLER_IS_SYNCADAPTER - true. Дело в том, что доступ к Календарю можно получить двумя способами: "как приложение" или "как адаптер синхронизации". У второго способа -  возможностей больше и... (спойлер) без него у меня не получилось редактировать повторяющиеся события. Но об этом - далее. Подробнее про "адаптер синхронизации" - здесь: https://developer.android.com/guide/topics/providers/calendar-provider#sync-adapter Для предоставления выбора я использую стандартное диалоговое окно AlertDialog. Перед его показом пользователю, создаю массив имен календарей calendarNames и указываю в переменной calIndex - индекс выбранного ранее календаря. Обратите внимание, что при выборе календаря помимо его id я еще в обязательном порядке сохраняю имя календаря, accountName и accountType. Эти поля необходимы для работы "адаптера синхронизации" (наберитесь терпения - об этом чуть дальше). Для новичков, типа меня поясню, что CRUD - это Create, Read, Update, Delete. Но в данном случае вместо Create идет Insert. С нее и начнем.  Все операции с Календарем лучше осуществлять в "фоновом" (асинхронном) режиме, чтобы интерфейс приложения не подтормаживал. Google рекомендует для этого использовать AsyncQueryHandler (подробнее см. https://developer.android.com/reference/android/ content/AsyncQueryHandler). Я не стал, уж очень мне показался он чересчур замысловатым. Запускаю функции работы с Календарем при помощи Корутин (https://developer.android.com/kotlin/coroutines) в параллельном потоке. Я привык так работать с Базой Данных. Повторять то, что уже написано о добавлении событий в статье "Calendar provider overview" (https://developer.android.com/guide/topics/providers/calendar-provider) не буду. Расскажу лишь о тех "граблях", на которые наступал я сам, когда писал и отлаживал код и своих "фишках". timeZone - как и при добавлении календаря, так и при добавлении события - id типа Int, а не String, будьте внимательны! rrule, который отвечает за правила повторяемости события, в моем случае, удобнее было разбить на две части: непосредственно правило и указание, до каких пор работает повторяемость. Это удобно впоследствии, когда повторяющиеся события будут редактироваться. start и end. Мне показалось удобным для тех записей в календаре, которым пользователь не указывает длительность (duration), создавать события целого дня. Тогда необходимо изменять им начало и конец, указывая полночь заданного дня и полночь + 24 часа. Ну и поле Events.ALL_DAY устанавливать в 1. Events.DESCRIPTION. Это единственное поле в записи события в календаре, где можно, как я понял, сохранить некий текст, указывающий принадлежность события моему приложению. У меня это - "Учет клиентов" (название приложения). rrule.isNotEmpty(). Черным по белому написано: если указываете повторяемость события в rrule, то вместо end - duration! Но кто подробно читает мануал? Точно не я. Будьте внимательны! Объяснение концовки кода про функции asSyncAdapter() и syncCalendar() пока опускаю. Продолжаю держать интригу. Ибо для меня эта "жара" стоила 2 недель пота и слез! Две недели, Карл!!! Не спрашивайте меня, почему в случае update в отличие от insert я не использую "адаптер синхронизации". Может быть, с ним хуже работало, а может, я его убрал по каким-то для меня самого не ведомым причинам. Не знаю. Мне до сих пор не очень понятно, как он работает, но без него никак... В моем случае - никак! Подробнее расскажу ниже. А пока, вроде бы, пояснять в коде больше нечего. Изменяем событие по единственному условию  - "${Events._ID} = $eventId" посему должна измениться лишь одна строка таблицы, т.е. успешный исход изменений - row == 1 (true). Здесь также, как и в случае update, я не использую "адаптер синхронизации". Знатоки, подскажите в комментариях, как поступать правильно? Где его использовать, а где - нет, я, видимо, определял опытным путем. Про функцию syncCalendar() я напишу ниже. Здесь уже плавно перехожу к описанию самого сложного для моего понимания - к работе с повторяющимися событиями (reccurent events). С ними я намаялся больше всего! Особенно в свете того, что в моем приложении мне необходимо было синхронизировать работу внутренней Базы Данных и Календаря.  Все события, как одиночные, так и повторяющиеся, записываются в таблицу Events. Для указания повторяемости события, как вы уже поняли, используются поля rrule и duration. А для того, чтобы прочитать события в некотором интервале времени, с учетом их повторяемости, делается запрос в таблицу Instances. В переменной eventsUriBuilder как раз указывается интервал времени (от start до end), из которого выбираются все входящие в него события.  Обратите внимание, что в таблице Instances у событий несколько иные поля. BEGIN и END - начало и конец отдельного события из серии повторяющихся событий или начало и конец одиночного события. EVENT_ID - id исходного события, которое задает серию повторяющихся событий или id одиночного события. DTSTART - начало первого события серии или начало одиночного события. В случае одиночного события DTSTART = BEGIN. В остальном, как видите, чтение событий из Календаря ничем не отличается от запроса из Базы Данных. В projection даем массив необходимых нам полей. В selection - формулируем условия. Подробнее можете прочесть здесь: https://developer.android.com/guide/topics/ providers/calendar-provider#instances Начинается "моя боль". Добавлять повторяющиеся событие не сложнее, чем одиночные. Как читать данные из таблицы Instances, где отображаются события из серии повторяющихся, входящих в заданный интервал, разобрался довольно быстро. Но редактирование и удаление - боль, боль, боль...  Сначала я пытался указывать исключения в поле EXRULE или EXDATE - ничего хорошего из этого не получалось. Я так и не понял, для чего оно нужно, если не работает! Потом для того, чтобы удалить одно событие из серии повторяющихся или изменить его, я добавлял новое событие в таблицу исключений - эпик фэйл! Работало это крайне плохо. События редактировались или удалялись, но потом появлялись вновь, как будто в смартфоне "кто-то" проводит ревизию и все возвращает на свои места. При этом, если добавить новое повторяющееся событие, подождать приличное время и только потом его редактировать или удалять, то все работает нормально. После двух недель мытарств и хождения по мукам (гугля все, что удавалось найти в интернете), я понял, что дело в синхронизации. Нельзя редактировать или удалять одно событие из серии повторяющихся, пока они не будут синхронизированы с облаком. Посему я задался этим вопросом и понял, что необходимо подключать приложение к Календарю, "как адаптер синхронизации" и синхронизировать календарь вручную сразу после добавления нового события. Функцию setCalendarVisibilityAndSync() в обязательном порядке запускаю для того Календаря, в который собираюсь сохранять сообщения. Она "включает" Календарю поля SYNC_EVENTS и VISIBLE.  Функция asSyncAdapter() добавляет к Uri параметры ACCOUNT_NAME, ACCOUNT_TYPE и указывает CALLER_IS_SYNCADAPTER - true. Функция syncCalendar() запускает синхронизацию Календаря. Ее я запускаю каждый раз, сразу после внесения в календарь изменений. Тогда и только тогда не возникает ошибок при редактировании и удалении повторяющихся событий. Для того, чтобы изменить одно событие из серии повторяющихся, необходимо в таблицу исключений (CONTENT_EXCEPTION_URI) добавить новое событие. В нем обязательно необходимо указать поле ORIGINAL_INSTANCE_TIME - начало того события серии, которое хотим изменить. Если я хочу не изменить, а удалить его, то вместо параметра newDate указываю - ноль. Тогда добавляемому событию в таблице исключений, указываю статус - STATUS_CANCELED. Опытным путем обнаружено, что добавлять новое событие в таблицу исключений лучше не используя "адаптер синхронизации", но в обязательном порядке сразу после добавления необходимо синхронизировать календарь - syncCalendar(). В моем приложении нет возможности изменять все последующий события серии, только удалять. По сути, удаление некоторого события серии заключается в изменении исходного события (updateEvent). Я добавляю в правило повторения ограничение его действия через until. Функция dateTimeFormatter форматирует дату события в форме RFC5545 - YYYYMMDDTHHMMSSZ. Подробнее здесь: https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5.2 Очень может быть, код у меня получился не идеальный. Однозначно, я не до конца понимаю, как оно работает. Но практика показывает, что все с ним в порядке. Буду рад прочесть ваши комментарии. И надеюсь, этот гайд будет кому-нибудь полезен. Я ничего подобного в сети не нашел.