content: /*
   * Copyright (C) 2015 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.server.notification;
  
  import android.content.ContentResolver;
  import android.content.ContentUris;
  import android.content.Context;
  import android.database.ContentObserver;
  import android.database.Cursor;
  import android.net.Uri;
  import android.provider.CalendarContract.Attendees;
  import android.provider.CalendarContract.Calendars;
  import android.provider.CalendarContract.Events;
  import android.provider.CalendarContract.Instances;
  import android.service.notification.ZenModeConfig.EventInfo;
  import android.util.ArraySet;
  import android.util.Log;
  import android.util.Slog;
  
  import java.io.PrintWriter;
  import java.util.Date;
  import java.util.Objects;
  
  public class CalendarTracker {
      private static final String TAG = "ConditionProviders.CT";
      private static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG);
      private static final boolean DEBUG_ATTENDEES = false;
  
      private static final int EVENT_CHECK_LOOKAHEAD = 24 * 60 * 60 * 1000;
  
      private static final String[] INSTANCE_PROJECTION = {
              Instances.BEGIN,
              Instances.END,
              Instances.TITLE,
              Instances.VISIBLE,
              Instances.EVENT_ID,
              Instances.CALENDAR_DISPLAY_NAME,
              Instances.OWNER_ACCOUNT,
              Instances.CALENDAR_ID,
              Instances.AVAILABILITY,
      };
  
      private static final String INSTANCE_ORDER_BY = Instances.BEGIN + " ASC";
  
      private static final String[] ATTENDEE_PROJECTION = {
          Attendees.EVENT_ID,
          Attendees.ATTENDEE_EMAIL,
          Attendees.ATTENDEE_STATUS,
      };
  
      private static final String ATTENDEE_SELECTION = Attendees.EVENT_ID + " = ? AND "
              + Attendees.ATTENDEE_EMAIL + " = ?";
  
      private final Context mSystemContext;
      private final Context mUserContext;
  
      private Callback mCallback;
      private boolean mRegistered;
  
      public CalendarTracker(Context systemContext, Context userContext) {
          mSystemContext = systemContext;
          mUserContext = userContext;
      }
  
      public void setCallback(Callback callback) {
          if (mCallback == callback) return;
          mCallback = callback;
          setRegistered(mCallback != null);
      }
  
      public void dump(String prefix, PrintWriter pw) {
          pw.print(prefix); pw.print("mCallback="); pw.println(mCallback);
          pw.print(prefix); pw.print("mRegistered="); pw.println(mRegistered);
          pw.print(prefix); pw.print("u="); pw.println(mUserContext.getUserId());
      }
  
      private ArraySet<Long> getCalendarsWithAccess() {
          final long start = System.currentTimeMillis();
          final ArraySet<Long> rt = new ArraySet<>();
          final String[] projection = { Calendars._ID };
          final String selection = Calendars.CALENDAR_ACCESS_LEVEL + " >= "
                  + Calendars.CAL_ACCESS_CONTRIBUTOR
                  + " AND " + Calendars.SYNC_EVENTS + " = 1";
          Cursor cursor = null;
          try {
              cursor = mUserContext.getContentResolver().query(Calendars.CONTENT_URI, projection,
                      selection, null, null);
              while (cursor != null && cursor.moveToNext()) {
                  rt.add(cursor.getLong(0));
              }
          } finally {
              if (cursor != null) {
                  cursor.close();
              }
          }
          if (DEBUG) {
              Log.d(TAG, "getCalendarsWithAccess took " + (System.currentTimeMillis() - start));
          }
          return rt;
      }
  
      public CheckEventResult checkEvent(EventInfo filter, long time) {
          final Uri.Builder uriBuilder = Instances.CONTENT_URI.buildUpon();
          ContentUris.appendId(uriBuilder, time);
          ContentUris.appendId(uriBuilder, time + EVENT_CHECK_LOOKAHEAD);
          final Uri uri = uriBuilder.build();
          final Cursor cursor = mUserContext.getContentResolver().query(uri, INSTANCE_PROJECTION,
                  null, null, INSTANCE_ORDER_BY);
          final CheckEventResult result = new CheckEventResult();
          result.recheckAt = time + EVENT_CHECK_LOOKAHEAD;
          try {
              final ArraySet<Long> calendars = getCalendarsWithAccess();
              while (cursor != null && cursor.moveToNext()) {
                  final long begin = cursor.getLong(0);
                  final long end = cursor.getLong(1);
                  final String title = cursor.getString(2);
                  final boolean calendarVisible = cursor.getInt(3) == 1;
                  final int eventId = cursor.getInt(4);
                  final String name = cursor.getString(5);
                  final String owner = cursor.getString(6);
                  final long calendarId = cursor.getLong(7);
                  final int availability = cursor.getInt(8);
                  final boolean canAccessCal = calendars.contains(calendarId);
                  if (DEBUG) {
                      Log.d(TAG, String.format("title=%s time=%s-%s vis=%s availability=%s "
                                      + "eventId=%s name=%s owner=%s calId=%s canAccessCal=%s",
                              title, new Date(begin), new Date(end), calendarVisible,
                              availabilityToString(availability), eventId, name, owner, calendarId,
                              canAccessCal));
                  }
                  final boolean meetsTime = time >= begin && time < end;
                  final boolean meetsCalendar = calendarVisible && canAccessCal
                          && ((filter.calName == null && filter.calendarId == null)
                          || (Objects.equals(filter.calendarId, calendarId))
                          || Objects.equals(filter.calName, name));
                  final boolean meetsAvailability = availability != Instances.AVAILABILITY_FREE;
                  if (meetsCalendar && meetsAvailability) {
                      if (DEBUG) Log.d(TAG, "  MEETS CALENDAR & AVAILABILITY");
                      final boolean meetsAttendee = meetsAttendee(filter, eventId, owner);
                      if (meetsAttendee) {
                          if (DEBUG) Log.d(TAG, "    MEETS ATTENDEE");
                          if (meetsTime) {
                              if (DEBUG) Log.d(TAG, "      MEETS TIME");
                              result.inEvent = true;
                          }
                          if (begin > time && begin < result.recheckAt) {
                              result.recheckAt = begin;
                          } else if (end > time && end < result.recheckAt) {
                              result.recheckAt = end;
                          }
                      }
                  }
              }
          } catch (Exception e) {
              Slog.w(TAG, "error reading calendar", e);
          } finally {
              if (cursor != null) {
                  cursor.close();
              }
          }
          return result;
      }
  
      private boolean meetsAttendee(EventInfo filter, int eventId, String email) {
          final long start = System.currentTimeMillis();
          String selection = ATTENDEE_SELECTION;
          String[] selectionArgs = { Integer.toString(eventId), email };
          if (DEBUG_ATTENDEES) {
              selection = null;
              selectionArgs = null;
          }
          final Cursor cursor = mUserContext.getContentResolver().query(Attendees.CONTENT_URI,
                  ATTENDEE_PROJECTION, selection, selectionArgs, null);
          try {
              if (cursor == null || cursor.getCount() == 0) {
                  if (DEBUG) Log.d(TAG, "No attendees found");
                  return true;
              }
              boolean rt = false;
              while (cursor != null && cursor.moveToNext()) {
                  final long rowEventId = cursor.getLong(0);
                  final String rowEmail = cursor.getString(1);
                  final int status = cursor.getInt(2);
                  final boolean meetsReply = meetsReply(filter.reply, status);
                  if (DEBUG) Log.d(TAG, (DEBUG_ATTENDEES ? String.format(
                          "rowEventId=%s, rowEmail=%s, ", rowEventId, rowEmail) : "") +
                          String.format("status=%s, meetsReply=%s",
                          attendeeStatusToString(status), meetsReply));
                  final boolean eventMeets = rowEventId == eventId && Objects.equals(rowEmail, email)
                          && meetsReply;
                  rt |= eventMeets;
              }
              return rt;
          } finally {
              if (cursor != null) {
                  cursor.close();
              }
              if (DEBUG) Log.d(TAG, "meetsAttendee took " + (System.currentTimeMillis() - start));
          }
      }
  
      private void setRegistered(boolean registered) {
          if (mRegistered == registered) return;
          final ContentResolver cr = mSystemContext.getContentResolver();
          final int userId = mUserContext.getUserId();
          if (mRegistered) {
              if (DEBUG) Log.d(TAG, "unregister content observer u=" + userId);
              cr.unregisterContentObserver(mObserver);
          }
          mRegistered = registered;
          if (DEBUG) Log.d(TAG, "mRegistered = " + registered + " u=" + userId);
          if (mRegistered) {
              if (DEBUG) Log.d(TAG, "register content observer u=" + userId);
              cr.registerContentObserver(Instances.CONTENT_URI, true, mObserver, userId);
              cr.registerContentObserver(Events.CONTENT_URI, true, mObserver, userId);
              cr.registerContentObserver(Calendars.CONTENT_URI, true, mObserver, userId);
          }
      }
  
      private static String attendeeStatusToString(int status) {
          switch (status) {
              case Attendees.ATTENDEE_STATUS_NONE: return "ATTENDEE_STATUS_NONE";
              case Attendees.ATTENDEE_STATUS_ACCEPTED: return "ATTENDEE_STATUS_ACCEPTED";
              case Attendees.ATTENDEE_STATUS_DECLINED: return "ATTENDEE_STATUS_DECLINED";
              case Attendees.ATTENDEE_STATUS_INVITED: return "ATTENDEE_STATUS_INVITED";
              case Attendees.ATTENDEE_STATUS_TENTATIVE: return "ATTENDEE_STATUS_TENTATIVE";
              default: return "ATTENDEE_STATUS_UNKNOWN_" + status;
          }
      }
  
      private static String availabilityToString(int availability) {
          switch (availability) {
              case Instances.AVAILABILITY_BUSY: return "AVAILABILITY_BUSY";
              case Instances.AVAILABILITY_FREE: return "AVAILABILITY_FREE";
              case Instances.AVAILABILITY_TENTATIVE: return "AVAILABILITY_TENTATIVE";
              default: return "AVAILABILITY_UNKNOWN_" + availability;
          }
      }
  
      private static boolean meetsReply(int reply, int attendeeStatus) {
          switch (reply) {
              case EventInfo.REPLY_YES:
                  return attendeeStatus == Attendees.ATTENDEE_STATUS_ACCEPTED;
              case EventInfo.REPLY_YES_OR_MAYBE:
                  return attendeeStatus == Attendees.ATTENDEE_STATUS_ACCEPTED
                          || attendeeStatus == Attendees.ATTENDEE_STATUS_TENTATIVE;
              case EventInfo.REPLY_ANY_EXCEPT_NO:
                  return attendeeStatus != Attendees.ATTENDEE_STATUS_DECLINED;
              default:
                  return false;
          }
      }
  
      private final ContentObserver mObserver = new ContentObserver(null) {
          @Override
          public void onChange(boolean selfChange, Uri u) {
              if (DEBUG) Log.d(TAG, "onChange selfChange=" + selfChange + " uri=" + u
                      + " u=" + mUserContext.getUserId());
              mCallback.onChanged();
          }
  
          @Override
          public void onChange(boolean selfChange) {
              if (DEBUG) Log.d(TAG, "onChange selfChange=" + selfChange);
          }
      };
  
      public static class CheckEventResult {
          public boolean inEvent;
          public long recheckAt;
      }
  
      public interface Callback {
          void onChanged();
      }
  
  }
  