1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.notification;
18
19import android.content.ContentResolver;
20import android.content.ContentUris;
21import android.content.Context;
22import android.database.ContentObserver;
23import android.database.Cursor;
24import android.net.Uri;
25import android.provider.BaseColumns;
26import android.provider.CalendarContract.Attendees;
27import android.provider.CalendarContract.Calendars;
28import android.provider.CalendarContract.Events;
29import android.provider.CalendarContract.Instances;
30import android.service.notification.ZenModeConfig.EventInfo;
31import android.util.ArraySet;
32import android.util.Log;
33import android.util.Slog;
34
35import java.io.PrintWriter;
36import java.util.Date;
37import java.util.Objects;
38
39public class CalendarTracker {
40    private static final String TAG = "ConditionProviders.CT";
41    private static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG);
42    private static final boolean DEBUG_ATTENDEES = false;
43
44    private static final int EVENT_CHECK_LOOKAHEAD = 24 * 60 * 60 * 1000;
45
46    private static final String[] INSTANCE_PROJECTION = {
47            Instances.BEGIN,
48            Instances.END,
49            Instances.TITLE,
50            Instances.VISIBLE,
51            Instances.EVENT_ID,
52            Instances.CALENDAR_DISPLAY_NAME,
53            Instances.OWNER_ACCOUNT,
54            Instances.CALENDAR_ID,
55            Instances.AVAILABILITY,
56    };
57
58    private static final String INSTANCE_ORDER_BY = Instances.BEGIN + " ASC";
59
60    private static final String[] ATTENDEE_PROJECTION = {
61        Attendees.EVENT_ID,
62        Attendees.ATTENDEE_EMAIL,
63        Attendees.ATTENDEE_STATUS,
64    };
65
66    private static final String ATTENDEE_SELECTION = Attendees.EVENT_ID + " = ? AND "
67            + Attendees.ATTENDEE_EMAIL + " = ?";
68
69    private final Context mSystemContext;
70    private final Context mUserContext;
71
72    private Callback mCallback;
73    private boolean mRegistered;
74
75    public CalendarTracker(Context systemContext, Context userContext) {
76        mSystemContext = systemContext;
77        mUserContext = userContext;
78    }
79
80    public void setCallback(Callback callback) {
81        if (mCallback == callback) return;
82        mCallback = callback;
83        setRegistered(mCallback != null);
84    }
85
86    public void dump(String prefix, PrintWriter pw) {
87        pw.print(prefix); pw.print("mCallback="); pw.println(mCallback);
88        pw.print(prefix); pw.print("mRegistered="); pw.println(mRegistered);
89        pw.print(prefix); pw.print("u="); pw.println(mUserContext.getUserId());
90    }
91
92    private ArraySet<Long> getPrimaryCalendars() {
93        final long start = System.currentTimeMillis();
94        final ArraySet<Long> rt = new ArraySet<>();
95        final String primary = "\"primary\"";
96        final String[] projection = { Calendars._ID,
97                "(" + Calendars.ACCOUNT_NAME + "=" + Calendars.OWNER_ACCOUNT + ") AS " + primary };
98        final String selection = primary + " = 1";
99        Cursor cursor = null;
100        try {
101            cursor = mUserContext.getContentResolver().query(Calendars.CONTENT_URI, projection,
102                    selection, null, null);
103            while (cursor != null && cursor.moveToNext()) {
104                rt.add(cursor.getLong(0));
105            }
106        } finally {
107            if (cursor != null) {
108                cursor.close();
109            }
110        }
111        if (DEBUG) Log.d(TAG, "getPrimaryCalendars took " + (System.currentTimeMillis() - start));
112        return rt;
113    }
114
115    public CheckEventResult checkEvent(EventInfo filter, long time) {
116        final Uri.Builder uriBuilder = Instances.CONTENT_URI.buildUpon();
117        ContentUris.appendId(uriBuilder, time);
118        ContentUris.appendId(uriBuilder, time + EVENT_CHECK_LOOKAHEAD);
119        final Uri uri = uriBuilder.build();
120        final Cursor cursor = mUserContext.getContentResolver().query(uri, INSTANCE_PROJECTION,
121                null, null, INSTANCE_ORDER_BY);
122        final CheckEventResult result = new CheckEventResult();
123        result.recheckAt = time + EVENT_CHECK_LOOKAHEAD;
124        try {
125            final ArraySet<Long> primaryCalendars = getPrimaryCalendars();
126            while (cursor != null && cursor.moveToNext()) {
127                final long begin = cursor.getLong(0);
128                final long end = cursor.getLong(1);
129                final String title = cursor.getString(2);
130                final boolean calendarVisible = cursor.getInt(3) == 1;
131                final int eventId = cursor.getInt(4);
132                final String name = cursor.getString(5);
133                final String owner = cursor.getString(6);
134                final long calendarId = cursor.getLong(7);
135                final int availability = cursor.getInt(8);
136                final boolean calendarPrimary = primaryCalendars.contains(calendarId);
137                if (DEBUG) Log.d(TAG, String.format(
138                        "%s %s-%s v=%s a=%s eid=%s n=%s o=%s cid=%s p=%s",
139                        title,
140                        new Date(begin), new Date(end), calendarVisible,
141                        availabilityToString(availability), eventId, name, owner, calendarId,
142                        calendarPrimary));
143                final boolean meetsTime = time >= begin && time < end;
144                final boolean meetsCalendar = calendarVisible && calendarPrimary
145                        && (filter.calendar == null || Objects.equals(filter.calendar, owner)
146                        || Objects.equals(filter.calendar, name));
147                final boolean meetsAvailability = availability != Instances.AVAILABILITY_FREE;
148                if (meetsCalendar && meetsAvailability) {
149                    if (DEBUG) Log.d(TAG, "  MEETS CALENDAR & AVAILABILITY");
150                    final boolean meetsAttendee = meetsAttendee(filter, eventId, owner);
151                    if (meetsAttendee) {
152                        if (DEBUG) Log.d(TAG, "    MEETS ATTENDEE");
153                        if (meetsTime) {
154                            if (DEBUG) Log.d(TAG, "      MEETS TIME");
155                            result.inEvent = true;
156                        }
157                        if (begin > time && begin < result.recheckAt) {
158                            result.recheckAt = begin;
159                        } else if (end > time && end < result.recheckAt) {
160                            result.recheckAt = end;
161                        }
162                    }
163                }
164            }
165        } catch (Exception e) {
166            Slog.w(TAG, "error reading calendar", e);
167        } finally {
168            if (cursor != null) {
169                cursor.close();
170            }
171        }
172        return result;
173    }
174
175    private boolean meetsAttendee(EventInfo filter, int eventId, String email) {
176        final long start = System.currentTimeMillis();
177        String selection = ATTENDEE_SELECTION;
178        String[] selectionArgs = { Integer.toString(eventId), email };
179        if (DEBUG_ATTENDEES) {
180            selection = null;
181            selectionArgs = null;
182        }
183        final Cursor cursor = mUserContext.getContentResolver().query(Attendees.CONTENT_URI,
184                ATTENDEE_PROJECTION, selection, selectionArgs, null);
185        try {
186            if (cursor == null || cursor.getCount() == 0) {
187                if (DEBUG) Log.d(TAG, "No attendees found");
188                return true;
189            }
190            boolean rt = false;
191            while (cursor != null && cursor.moveToNext()) {
192                final long rowEventId = cursor.getLong(0);
193                final String rowEmail = cursor.getString(1);
194                final int status = cursor.getInt(2);
195                final boolean meetsReply = meetsReply(filter.reply, status);
196                if (DEBUG) Log.d(TAG, (DEBUG_ATTENDEES ? String.format(
197                        "rowEventId=%s, rowEmail=%s, ", rowEventId, rowEmail) : "") +
198                        String.format("status=%s, meetsReply=%s",
199                        attendeeStatusToString(status), meetsReply));
200                final boolean eventMeets = rowEventId == eventId && Objects.equals(rowEmail, email)
201                        && meetsReply;
202                rt |= eventMeets;
203            }
204            return rt;
205        } finally {
206            if (cursor != null) {
207                cursor.close();
208            }
209            if (DEBUG) Log.d(TAG, "meetsAttendee took " + (System.currentTimeMillis() - start));
210        }
211    }
212
213    private void setRegistered(boolean registered) {
214        if (mRegistered == registered) return;
215        final ContentResolver cr = mSystemContext.getContentResolver();
216        final int userId = mUserContext.getUserId();
217        if (mRegistered) {
218            if (DEBUG) Log.d(TAG, "unregister content observer u=" + userId);
219            cr.unregisterContentObserver(mObserver);
220        }
221        mRegistered = registered;
222        if (DEBUG) Log.d(TAG, "mRegistered = " + registered + " u=" + userId);
223        if (mRegistered) {
224            if (DEBUG) Log.d(TAG, "register content observer u=" + userId);
225            cr.registerContentObserver(Instances.CONTENT_URI, true, mObserver, userId);
226            cr.registerContentObserver(Events.CONTENT_URI, true, mObserver, userId);
227            cr.registerContentObserver(Calendars.CONTENT_URI, true, mObserver, userId);
228        }
229    }
230
231    private static String attendeeStatusToString(int status) {
232        switch (status) {
233            case Attendees.ATTENDEE_STATUS_NONE: return "ATTENDEE_STATUS_NONE";
234            case Attendees.ATTENDEE_STATUS_ACCEPTED: return "ATTENDEE_STATUS_ACCEPTED";
235            case Attendees.ATTENDEE_STATUS_DECLINED: return "ATTENDEE_STATUS_DECLINED";
236            case Attendees.ATTENDEE_STATUS_INVITED: return "ATTENDEE_STATUS_INVITED";
237            case Attendees.ATTENDEE_STATUS_TENTATIVE: return "ATTENDEE_STATUS_TENTATIVE";
238            default: return "ATTENDEE_STATUS_UNKNOWN_" + status;
239        }
240    }
241
242    private static String availabilityToString(int availability) {
243        switch (availability) {
244            case Instances.AVAILABILITY_BUSY: return "AVAILABILITY_BUSY";
245            case Instances.AVAILABILITY_FREE: return "AVAILABILITY_FREE";
246            case Instances.AVAILABILITY_TENTATIVE: return "AVAILABILITY_TENTATIVE";
247            default: return "AVAILABILITY_UNKNOWN_" + availability;
248        }
249    }
250
251    private static boolean meetsReply(int reply, int attendeeStatus) {
252        switch (reply) {
253            case EventInfo.REPLY_YES:
254                return attendeeStatus == Attendees.ATTENDEE_STATUS_ACCEPTED;
255            case EventInfo.REPLY_YES_OR_MAYBE:
256                return attendeeStatus == Attendees.ATTENDEE_STATUS_ACCEPTED
257                        || attendeeStatus == Attendees.ATTENDEE_STATUS_TENTATIVE;
258            case EventInfo.REPLY_ANY_EXCEPT_NO:
259                return attendeeStatus != Attendees.ATTENDEE_STATUS_DECLINED;
260            default:
261                return false;
262        }
263    }
264
265    private final ContentObserver mObserver = new ContentObserver(null) {
266        @Override
267        public void onChange(boolean selfChange, Uri u) {
268            if (DEBUG) Log.d(TAG, "onChange selfChange=" + selfChange + " uri=" + u
269                    + " u=" + mUserContext.getUserId());
270            mCallback.onChanged();
271        }
272
273        @Override
274        public void onChange(boolean selfChange) {
275            if (DEBUG) Log.d(TAG, "onChange selfChange=" + selfChange);
276        }
277    };
278
279    public static class CheckEventResult {
280        public boolean inEvent;
281        public long recheckAt;
282    }
283
284    public interface Callback {
285        void onChanged();
286    }
287
288}
289