Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ad2399f3 authored by John Spurlock's avatar John Spurlock Committed by Android (Google) Code Review
Browse files

Merge "Zen: Implement calendar event system condition provider." into mnc-dev

parents 5f809e3f 2f096ed7
Loading
Loading
Loading
Loading
+289 −0
Original line number Diff line number Diff line
/*
 * 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.BaseColumns;
import android.provider.CalendarContract.Attendees;
import android.provider.CalendarContract.Instances;
import android.service.notification.ZenModeConfig.EventInfo;
import android.util.Log;

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.OWNER_ACCOUNT,
        Instances.CALENDAR_ID,
    };

    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,
        Attendees.ATTENDEE_TYPE,
    };

    private static final String ATTENDEE_SELECTION = Attendees.EVENT_ID + " = ? AND "
            + Attendees.ATTENDEE_EMAIL + " = ?";

    private final Context mContext;

    private Callback mCallback;
    private boolean mRegistered;

    public CalendarTracker(Context context) {
        mContext = context;
    }

    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);
    }

    public void dumpContent(Uri uri) {
        Log.d(TAG, "dumpContent: " + uri);
        final Cursor cursor = mContext.getContentResolver().query(uri, null, null, null, null);
        try {
            int r = 0;
            while (cursor.moveToNext()) {
                Log.d(TAG, "Row " + (++r) + ": id="
                        + cursor.getInt(cursor.getColumnIndex(BaseColumns._ID)));
                for (int i = 0; i < cursor.getColumnCount(); i++) {
                    final String name = cursor.getColumnName(i);
                    final int type = cursor.getType(i);
                    Object o = null;
                    String typeName = null;
                    switch (type) {
                        case Cursor.FIELD_TYPE_INTEGER:
                            o = cursor.getLong(i);
                            typeName = "INTEGER";
                            break;
                        case Cursor.FIELD_TYPE_STRING:
                            o = cursor.getString(i);
                            typeName = "STRING";
                            break;
                        case Cursor.FIELD_TYPE_NULL:
                            o = null;
                            typeName = "NULL";
                            break;
                        default:
                            throw new UnsupportedOperationException("type: " + type);
                    }
                    if (name.equals(BaseColumns._ID)
                            || name.toLowerCase().contains("sync")
                            || o == null) {
                        continue;
                    }
                    Log.d(TAG, "  " + name + "(" + typeName + ")=" + o);
                }
            }
            Log.d(TAG, "  " + uri + " " + r + " rows");
        } finally {
            cursor.close();
        }
    }



    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 = mContext.getContentResolver().query(uri, INSTANCE_PROJECTION, null,
                null, INSTANCE_ORDER_BY);
        final CheckEventResult result = new CheckEventResult();
        result.recheckAt = time + EVENT_CHECK_LOOKAHEAD;
        try {
            while (cursor.moveToNext()) {
                final long begin = cursor.getLong(0);
                final long end = cursor.getLong(1);
                final String title = cursor.getString(2);
                final boolean visible = cursor.getInt(3) == 1;
                final int eventId = cursor.getInt(4);
                final String owner = cursor.getString(5);
                final long calendarId = cursor.getLong(6);
                if (DEBUG) Log.d(TAG, String.format("%s %s-%s v=%s eid=%s o=%s cid=%s", title,
                        new Date(begin), new Date(end), visible, eventId, owner, calendarId));
                final boolean meetsTime = time >= begin && time < end;
                final boolean meetsCalendar = visible
                        && (filter.calendar == 0 || filter.calendar == calendarId);
                if (meetsCalendar) {
                    if (DEBUG) Log.d(TAG, "  MEETS CALENDAR");
                    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;
                        }
                    }
                }
            }
        } finally {
            cursor.close();
        }
        return result;
    }

    private boolean meetsAttendee(EventInfo filter, int eventId, String email) {
        String selection = ATTENDEE_SELECTION;
        String[] selectionArgs = { Integer.toString(eventId), email };
        if (DEBUG_ATTENDEES) {
            selection = null;
            selectionArgs = null;
        }
        final Cursor cursor = mContext.getContentResolver().query(Attendees.CONTENT_URI,
                ATTENDEE_PROJECTION, selection, selectionArgs, null);
        try {
            if (cursor.getCount() == 0) {
                if (DEBUG) Log.d(TAG, "No attendees found");
                return true;
            }
            boolean rt = false;
            while (cursor.moveToNext()) {
                final long rowEventId = cursor.getLong(0);
                final String rowEmail = cursor.getString(1);
                final int status = cursor.getInt(2);
                final int type = cursor.getInt(3);
                final boolean meetsReply = meetsReply(filter.reply, status);
                final boolean meetsAttendance = meetsAttendance(filter.attendance, type);
                if (DEBUG) Log.d(TAG, (DEBUG_ATTENDEES ? String.format(
                        "rowEventId=%s, rowEmail=%s, ", rowEventId, rowEmail) : "") +
                        String.format("status=%s, type=%s, meetsReply=%s, meetsAttendance=%s",
                        attendeeStatusToString(status), attendeeTypeToString(type), meetsReply,
                        meetsAttendance));
                final boolean eventMeets = rowEventId == eventId && Objects.equals(rowEmail, email)
                        && meetsReply && meetsAttendance;
                rt |= eventMeets;
            }
            return rt;
        } finally {
            cursor.close();
        }
    }

    private void setRegistered(boolean registered) {
        if (mRegistered == registered) return;
        final ContentResolver cr = mContext.getContentResolver();
        if (mRegistered) {
            cr.unregisterContentObserver(mObserver);
        }
        mRegistered = registered;
        if (mRegistered) {
            cr.registerContentObserver(Instances.CONTENT_URI, false, mObserver);
        }
    }

    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 attendeeTypeToString(int type) {
        switch (type) {
            case Attendees.TYPE_NONE: return "TYPE_NONE";
            case Attendees.TYPE_REQUIRED: return "TYPE_REQUIRED";
            case Attendees.TYPE_OPTIONAL: return "TYPE_OPTIONAL";
            case Attendees.TYPE_RESOURCE: return "TYPE_RESOURCE";
            default: return "TYPE_" + type;
        }
    }

    private static boolean meetsAttendance(int attendance, int attendeeType) {
        switch (attendance) {
            case EventInfo.ATTENDANCE_OPTIONAL:
                return attendeeType == Attendees.TYPE_OPTIONAL;
            case EventInfo.ATTENDANCE_REQUIRED:
                return attendeeType == Attendees.TYPE_REQUIRED;
            default: // EventInfo.ATTENDANCE_REQUIRED_OR_OPTIONAL
                return true;
        }
    }

    private static boolean meetsReply(int reply, int attendeeStatus) {
        switch (reply) {
            case EventInfo.REPLY_YES:
                return attendeeStatus == Attendees.ATTENDEE_STATUS_ACCEPTED;
            case EventInfo.REPLY_ANY_EXCEPT_NO:
                return attendeeStatus != Attendees.ATTENDEE_STATUS_DECLINED;
            default: // EventInfo.REPLY_ANY
                return true;
        }
    }

    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);
            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();
    }

}
+3 −0
Original line number Diff line number Diff line
@@ -121,6 +121,9 @@ public class ConditionProviders extends ManagedServices {
    @Override
    public void onBootPhaseAppsCanStart() {
        super.onBootPhaseAppsCanStart();
        for (int i = 0; i < mSystemConditionProviders.size(); i++) {
            mSystemConditionProviders.valueAt(i).onBootComplete();
        }
        if (mCallback != null) {
            mCallback.onBootComplete();
        }
+7 −7
Original line number Diff line number Diff line
@@ -34,12 +34,11 @@ import android.util.Slog;
import com.android.server.notification.NotificationManagerService.DumpFilter;

import java.io.PrintWriter;
import java.util.Date;

/** Built-in zen condition provider for simple time-based conditions */
public class CountdownConditionProvider extends SystemConditionProviderService {
    private static final String TAG = "ConditionProviders";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
    private static final String TAG = "ConditionProviders.CCP";
    private static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG);

    public static final ComponentName COMPONENT =
            new ComponentName("android", CountdownConditionProvider.class.getName());
@@ -73,6 +72,11 @@ public class CountdownConditionProvider extends SystemConditionProviderService {
        attachBaseContext(base);
    }

    @Override
    public void onBootComplete() {
        // noop
    }

    @Override
    public IConditionProvider asInterface() {
        return (IConditionProvider) onBind(null);
@@ -170,8 +174,4 @@ public class CountdownConditionProvider extends SystemConditionProviderService {
                ts(time), time - now, span, ts(now));
    }

    private static String ts(long time) {
        return new Date(time) + " (" + time + ")";
    }

}
+106 −7
Original line number Diff line number Diff line
@@ -16,16 +16,23 @@

package com.android.server.notification;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.service.notification.Condition;
import android.service.notification.IConditionProvider;
import android.service.notification.ZenModeConfig;
import android.service.notification.ZenModeConfig.EventInfo;
import android.util.ArraySet;
import android.util.Log;
import android.util.Slog;

import com.android.server.notification.CalendarTracker.CheckEventResult;
import com.android.server.notification.NotificationManagerService.DumpFilter;

import java.io.PrintWriter;
@@ -34,20 +41,27 @@ import java.io.PrintWriter;
 * Built-in zen condition provider for calendar event-based conditions.
 */
public class EventConditionProvider extends SystemConditionProviderService {
    private static final String TAG = "ConditionProviders";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
    private static final String TAG = "ConditionProviders.ECP";
    private static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG);

    public static final ComponentName COMPONENT =
            new ComponentName("android", EventConditionProvider.class.getName());
    private static final String NOT_SHOWN = "...";
    private static final String SIMPLE_NAME = EventConditionProvider.class.getSimpleName();
    private static final String ACTION_EVALUATE = SIMPLE_NAME + ".EVALUATE";
    private static final int REQUEST_CODE_EVALUATE = 1;
    private static final String EXTRA_TIME = "time";

    private final Context mContext = this;
    private final ArraySet<Uri> mSubscriptions = new ArraySet<Uri>();
    private final CalendarTracker mTracker = new CalendarTracker(mContext);

    private boolean mConnected;
    private boolean mRegistered;
    private boolean mBootComplete;  // don't hammer the calendar provider until boot completes.

    public EventConditionProvider() {
        if (DEBUG) Slog.d(TAG, "new EventConditionProvider()");
        if (DEBUG) Slog.d(TAG, "new " + SIMPLE_NAME + "()");
    }

    @Override
@@ -62,14 +76,25 @@ public class EventConditionProvider extends SystemConditionProviderService {

    @Override
    public void dump(PrintWriter pw, DumpFilter filter) {
        pw.println("    EventConditionProvider:");
        pw.print("    "); pw.print(SIMPLE_NAME); pw.println(":");
        pw.print("      mConnected="); pw.println(mConnected);
        pw.print("      mRegistered="); pw.println(mRegistered);
        pw.print("      mBootComplete="); pw.println(mBootComplete);
        pw.println("      mSubscriptions=");
        for (Uri conditionId : mSubscriptions) {
            pw.print("        ");
            pw.println(conditionId);
        }
        pw.println("      mTracker=");
        mTracker.dump("        ", pw);
    }

    @Override
    public void onBootComplete() {
        if (DEBUG) Slog.d(TAG, "onBootComplete");
        if (mBootComplete) return;
        mBootComplete = true;
        evaluateSubscriptions();
    }

    @Override
@@ -98,9 +123,10 @@ public class EventConditionProvider extends SystemConditionProviderService {
            notifyCondition(conditionId, Condition.STATE_FALSE, "badCondition");
            return;
        }
        mSubscriptions.add(conditionId);
        if (mSubscriptions.add(conditionId)) {
            evaluateSubscriptions();
        }
    }

    @Override
    public void onUnsubscribe(Uri conditionId) {
@@ -121,9 +147,52 @@ public class EventConditionProvider extends SystemConditionProviderService {
    }

    private void evaluateSubscriptions() {
        if (DEBUG) Log.d(TAG, "evaluateSubscriptions");
        if (!mBootComplete) {
            if (DEBUG) Log.d(TAG, "Skipping evaluate before boot complete");
            return;
        }
        final long now = System.currentTimeMillis();
        mTracker.setCallback(mSubscriptions.isEmpty() ? null : mTrackerCallback);
        setRegistered(!mSubscriptions.isEmpty());
        long reevaluateAt = 0;
        for (Uri conditionId : mSubscriptions) {
            notifyCondition(conditionId, Condition.STATE_FALSE, "notImplemented");
            final EventInfo event = ZenModeConfig.tryParseEventConditionId(conditionId);
            if (event == null) {
                notifyCondition(conditionId, Condition.STATE_FALSE, "badConditionId");
                continue;
            }
            final CheckEventResult result = mTracker.checkEvent(event, now);
            if (result.recheckAt != 0 && (reevaluateAt == 0 || result.recheckAt < reevaluateAt)) {
                reevaluateAt = result.recheckAt;
            }
            if (!result.inEvent) {
                notifyCondition(conditionId, Condition.STATE_FALSE, "!inEventNow");
                continue;
            }
            notifyCondition(conditionId, Condition.STATE_TRUE, "inEventNow");
        }
        updateAlarm(now, reevaluateAt);
        if (DEBUG) Log.d(TAG, "evaluateSubscriptions took " + (System.currentTimeMillis() - now));
    }

    private void updateAlarm(long now, long time) {
        final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
        final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext,
                REQUEST_CODE_EVALUATE,
                new Intent(ACTION_EVALUATE)
                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
                        .putExtra(EXTRA_TIME, time),
                PendingIntent.FLAG_UPDATE_CURRENT);
        alarms.cancel(pendingIntent);
        if (time == 0 || time < now) {
            if (DEBUG) Slog.d(TAG, "Not scheduling evaluate: " + (time == 0 ? "no time specified"
                    : "specified time in the past"));
            return;
        }
        if (DEBUG) Slog.d(TAG, String.format("Scheduling evaluate for %s, in %s, now=%s",
                ts(time), formatDuration(time - now), ts(now)));
        alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
    }

    private void notifyCondition(Uri conditionId, int state, String reason) {
@@ -139,4 +208,34 @@ public class EventConditionProvider extends SystemConditionProviderService {
        return new Condition(id, summary, line1, line2, 0, state, Condition.FLAG_RELEVANT_ALWAYS);
    }

    private void setRegistered(boolean registered) {
        if (mRegistered == registered) return;
        if (DEBUG) Slog.d(TAG, "setRegistered " + registered);
        mRegistered = registered;
        if (mRegistered) {
            final IntentFilter filter = new IntentFilter();
            filter.addAction(Intent.ACTION_TIME_CHANGED);
            filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
            filter.addAction(ACTION_EVALUATE);
            registerReceiver(mReceiver, filter);
        } else {
            unregisterReceiver(mReceiver);
        }
    }

    private final CalendarTracker.Callback mTrackerCallback = new CalendarTracker.Callback() {
        @Override
        public void onChanged() {
            if (DEBUG) Log.d(TAG, "mTrackerCallback.onChanged");
            evaluateSubscriptions();
        }
    };

    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (DEBUG) Slog.d(TAG, "onReceive " + intent.getAction());
            evaluateSubscriptions();
        }
    };
}
+11 −17
Original line number Diff line number Diff line
@@ -31,25 +31,24 @@ import android.service.notification.ZenModeConfig.ScheduleInfo;
import android.util.ArraySet;
import android.util.Log;
import android.util.Slog;
import android.util.TimeUtils;

import com.android.server.notification.NotificationManagerService.DumpFilter;

import java.io.PrintWriter;
import java.util.Date;
import java.util.TimeZone;

/**
 * Built-in zen condition provider for daily scheduled time-based conditions.
 */
public class ScheduleConditionProvider extends SystemConditionProviderService {
    private static final String TAG = "ConditionProviders";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
    private static final String TAG = "ConditionProviders.SCP";
    private static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG);

    public static final ComponentName COMPONENT =
            new ComponentName("android", ScheduleConditionProvider.class.getName());
    private static final String NOT_SHOWN = "...";
    private static final String ACTION_EVALUATE = TAG + ".EVALUATE";
    private static final String SIMPLE_NAME = ScheduleConditionProvider.class.getSimpleName();
    private static final String ACTION_EVALUATE =  SIMPLE_NAME + ".EVALUATE";
    private static final int REQUEST_CODE_EVALUATE = 1;
    private static final String EXTRA_TIME = "time";

@@ -60,7 +59,7 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
    private boolean mRegistered;

    public ScheduleConditionProvider() {
        if (DEBUG) Slog.d(TAG, "new ScheduleConditionProvider()");
        if (DEBUG) Slog.d(TAG, "new " + SIMPLE_NAME + "()");
    }

    @Override
@@ -75,7 +74,7 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {

    @Override
    public void dump(PrintWriter pw, DumpFilter filter) {
        pw.println("    ScheduleConditionProvider:");
        pw.print("    "); pw.print(SIMPLE_NAME); pw.println(":");
        pw.print("      mConnected="); pw.println(mConnected);
        pw.print("      mRegistered="); pw.println(mRegistered);
        pw.println("      mSubscriptions=");
@@ -93,6 +92,11 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
        mConnected = true;
    }

    @Override
    public void onBootComplete() {
        // noop
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
@@ -175,16 +179,6 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
        }
    }

    private static String ts(long time) {
        return new Date(time) + " (" + time + ")";
    }

    private static String formatDuration(long millis) {
        final StringBuilder sb = new StringBuilder();
        TimeUtils.formatDuration(millis, sb);
        return sb.toString();
    }

    private static boolean meetsSchedule(Uri conditionId, long time) {
        final ScheduleCalendar cal = toScheduleCalendar(conditionId);
        return cal != null && cal.isInSchedule(time);
Loading