From 2f096ed7662f9d20876ae2944ffc4ecf4fd51faa Mon Sep 17 00:00:00 2001 From: John Spurlock Date: Mon, 4 May 2015 11:58:26 -0400 Subject: [PATCH] Zen: Implement calendar event system condition provider. - Wire up basic implementation for the system provider that handles event-based DND subscriptions. - Backed by the standard system calendar content provider. - Move shared time utils to base class, clean up logging. Bug: 20064962 Change-Id: I070b6baa580c592c2ab4101c6b44a254787f9dd7 --- .../server/notification/CalendarTracker.java | 289 ++++++++++++++++++ .../notification/ConditionProviders.java | 3 + .../CountdownConditionProvider.java | 14 +- .../notification/EventConditionProvider.java | 115 ++++++- .../ScheduleConditionProvider.java | 28 +- .../SystemConditionProviderService.java | 13 + 6 files changed, 430 insertions(+), 32 deletions(-) create mode 100644 services/core/java/com/android/server/notification/CalendarTracker.java diff --git a/services/core/java/com/android/server/notification/CalendarTracker.java b/services/core/java/com/android/server/notification/CalendarTracker.java new file mode 100644 index 0000000000000..8734d976db2e0 --- /dev/null +++ b/services/core/java/com/android/server/notification/CalendarTracker.java @@ -0,0 +1,289 @@ +/* + * 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(); + } + +} diff --git a/services/core/java/com/android/server/notification/ConditionProviders.java b/services/core/java/com/android/server/notification/ConditionProviders.java index b36fcd2cc09c6..8c6afafd8ec87 100644 --- a/services/core/java/com/android/server/notification/ConditionProviders.java +++ b/services/core/java/com/android/server/notification/ConditionProviders.java @@ -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(); } diff --git a/services/core/java/com/android/server/notification/CountdownConditionProvider.java b/services/core/java/com/android/server/notification/CountdownConditionProvider.java index b5b97d6bcfb65..07903e97a2d0a 100644 --- a/services/core/java/com/android/server/notification/CountdownConditionProvider.java +++ b/services/core/java/com/android/server/notification/CountdownConditionProvider.java @@ -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 + ")"; - } - } diff --git a/services/core/java/com/android/server/notification/EventConditionProvider.java b/services/core/java/com/android/server/notification/EventConditionProvider.java index 425e22200d174..dea63255594d1 100644 --- a/services/core/java/com/android/server/notification/EventConditionProvider.java +++ b/services/core/java/com/android/server/notification/EventConditionProvider.java @@ -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 mSubscriptions = new ArraySet(); + 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,8 +123,9 @@ public class EventConditionProvider extends SystemConditionProviderService { notifyCondition(conditionId, Condition.STATE_FALSE, "badCondition"); return; } - mSubscriptions.add(conditionId); - evaluateSubscriptions(); + if (mSubscriptions.add(conditionId)) { + evaluateSubscriptions(); + } } @Override @@ -121,9 +147,52 @@ public class EventConditionProvider extends SystemConditionProviderService { } private void evaluateSubscriptions() { - for (Uri conditionId : mSubscriptions) { - notifyCondition(conditionId, Condition.STATE_FALSE, "notImplemented"); + 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) { + 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(); + } + }; } diff --git a/services/core/java/com/android/server/notification/ScheduleConditionProvider.java b/services/core/java/com/android/server/notification/ScheduleConditionProvider.java index 0912e9747d1a4..bca13c202867b 100644 --- a/services/core/java/com/android/server/notification/ScheduleConditionProvider.java +++ b/services/core/java/com/android/server/notification/ScheduleConditionProvider.java @@ -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); diff --git a/services/core/java/com/android/server/notification/SystemConditionProviderService.java b/services/core/java/com/android/server/notification/SystemConditionProviderService.java index 3282a69af15c2..8a8e0635e64d9 100644 --- a/services/core/java/com/android/server/notification/SystemConditionProviderService.java +++ b/services/core/java/com/android/server/notification/SystemConditionProviderService.java @@ -21,10 +21,12 @@ import android.content.Context; import android.net.Uri; import android.service.notification.ConditionProviderService; import android.service.notification.IConditionProvider; +import android.util.TimeUtils; import com.android.server.notification.NotificationManagerService.DumpFilter; import java.io.PrintWriter; +import java.util.Date; public abstract class SystemConditionProviderService extends ConditionProviderService { @@ -33,4 +35,15 @@ public abstract class SystemConditionProviderService extends ConditionProviderSe abstract public IConditionProvider asInterface(); abstract public ComponentName getComponent(); abstract public boolean isValidConditionId(Uri id); + abstract public void onBootComplete(); + + protected static String ts(long time) { + return new Date(time) + " (" + time + ")"; + } + + protected static String formatDuration(long millis) { + final StringBuilder sb = new StringBuilder(); + TimeUtils.formatDuration(millis, sb); + return sb.toString(); + } }