CalendarAppWidgetService.java revision c46c2dc5dbec57616d799b1d0290d7c827b48d0c
1bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang/*
2bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang * Copyright (C) 2009 The Android Open Source Project
3bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang *
4bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang * Licensed under the Apache License, Version 2.0 (the "License");
5bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang * you may not use this file except in compliance with the License.
6bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang * You may obtain a copy of the License at
7bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang *
8bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang *      http://www.apache.org/licenses/LICENSE-2.0
9bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang *
10bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang * Unless required by applicable law or agreed to in writing, software
11bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang * distributed under the License is distributed on an "AS IS" BASIS,
12bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang * See the License for the specific language governing permissions and
14bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang * limitations under the License.
15bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang */
16bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
179a3cb14e28536e4133dddbe952f47189fe344ec1Mason Tangpackage com.android.calendar.widget;
18bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
19a07cedd5af736339a7dd6166970311c487366500Daisuke Miyakawaimport com.google.common.annotations.VisibleForTesting;
20a07cedd5af736339a7dd6166970311c487366500Daisuke Miyakawa
214143cfabb36f0c3adcad410a6eed29bc2b89d6e3Daisuke Miyakawaimport com.android.calendar.R;
224143cfabb36f0c3adcad410a6eed29bc2b89d6e3Daisuke Miyakawaimport com.android.calendar.Utils;
234143cfabb36f0c3adcad410a6eed29bc2b89d6e3Daisuke Miyakawaimport com.android.calendar.widget.CalendarAppWidgetModel.DayInfo;
244143cfabb36f0c3adcad410a6eed29bc2b89d6e3Daisuke Miyakawaimport com.android.calendar.widget.CalendarAppWidgetModel.EventInfo;
254143cfabb36f0c3adcad410a6eed29bc2b89d6e3Daisuke Miyakawaimport com.android.calendar.widget.CalendarAppWidgetModel.RowInfo;
264143cfabb36f0c3adcad410a6eed29bc2b89d6e3Daisuke Miyakawa
27c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErikimport android.appwidget.AppWidgetManager;
28c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErikimport android.content.BroadcastReceiver;
29bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.content.ContentResolver;
30bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.content.Context;
31c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErikimport android.content.CursorLoader;
32bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.content.Intent;
33c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErikimport android.content.IntentFilter;
34c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErikimport android.content.Loader;
35a07cedd5af736339a7dd6166970311c487366500Daisuke Miyakawaimport android.content.res.Resources;
36bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.database.Cursor;
3747d40324272ae39af0872bf5cbf27e1800478021Mason Tangimport android.database.MatrixCursor;
38bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.net.Uri;
39c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErikimport android.os.Handler;
40c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErikimport android.provider.Calendar;
41bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.provider.Calendar.Attendees;
42bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.provider.Calendar.Calendars;
43bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.provider.Calendar.Instances;
44bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.text.format.DateUtils;
45bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.text.format.Time;
46bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.util.Log;
47bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.view.View;
48bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.widget.RemoteViews;
4947d40324272ae39af0872bf5cbf27e1800478021Mason Tangimport android.widget.RemoteViewsService;
5047d40324272ae39af0872bf5cbf27e1800478021Mason Tang
51bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
5247d40324272ae39af0872bf5cbf27e1800478021Mason Tangpublic class CalendarAppWidgetService extends RemoteViewsService {
530c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa    private static final String TAG = "CalendarWidget";
54bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
553ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang    static final int EVENT_MIN_COUNT = 20;
563ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang    static final int EVENT_MAX_COUNT = 503;
57c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik    // Minimum delay between queries on the database for widget updates in ms
58c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik    static final int WIDGET_UPDATE_THROTTLE = 500;
5947d40324272ae39af0872bf5cbf27e1800478021Mason Tang
60bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    private static final String EVENT_SORT_ORDER = Instances.START_DAY + " ASC, "
61bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            + Instances.START_MINUTE + " ASC, " + Instances.END_DAY + " ASC, "
623ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang            + Instances.END_MINUTE + " ASC LIMIT " + EVENT_MAX_COUNT;
63bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
64bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    // TODO can't use parameter here because provider is dropping them
65bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    private static final String EVENT_SELECTION = Calendars.SELECTED + "=1 AND "
66bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            + Instances.SELF_ATTENDEE_STATUS + "!=" + Attendees.ATTENDEE_STATUS_DECLINED;
67bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
68bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static final String[] EVENT_PROJECTION = new String[] {
69bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        Instances.ALL_DAY,
70bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        Instances.BEGIN,
71bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        Instances.END,
72bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        Instances.TITLE,
73bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        Instances.EVENT_LOCATION,
74bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        Instances.EVENT_ID,
753ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang        Instances.START_DAY,
76a71e0a520493532dbb7e9bfa164ab78e59e797a3Daisuke Miyakawa        Instances.END_DAY,
77a71e0a520493532dbb7e9bfa164ab78e59e797a3Daisuke Miyakawa        Instances.COLOR
78bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    };
79bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
80bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static final int INDEX_ALL_DAY = 0;
81bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static final int INDEX_BEGIN = 1;
82bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static final int INDEX_END = 2;
83bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static final int INDEX_TITLE = 3;
84bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static final int INDEX_EVENT_LOCATION = 4;
85bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static final int INDEX_EVENT_ID = 5;
863ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang    static final int INDEX_START_DAY = 6;
873ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang    static final int INDEX_END_DAY = 7;
88a71e0a520493532dbb7e9bfa164ab78e59e797a3Daisuke Miyakawa    static final int INDEX_COLOR = 8;
89bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
903ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang    static final int MAX_DAYS = 7;
913ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang
923ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang    private static final long SEARCH_DURATION = MAX_DAYS * DateUtils.DAY_IN_MILLIS;
93bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
940c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa    /**
950c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa     * Update interval used when no next-update calculated, or bad trigger time in past.
960c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa     * Unit: milliseconds.
970c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa     */
980c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa    private static final long UPDATE_TIME_NO_EVENTS = DateUtils.HOUR_IN_MILLIS * 6;
99bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
10047d40324272ae39af0872bf5cbf27e1800478021Mason Tang    @Override
10147d40324272ae39af0872bf5cbf27e1800478021Mason Tang    public RemoteViewsFactory onGetViewFactory(Intent intent) {
10247d40324272ae39af0872bf5cbf27e1800478021Mason Tang        return new CalendarFactory(getApplicationContext(), intent);
103bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    }
104bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
105c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik    protected static class CalendarFactory extends BroadcastReceiver implements
106c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            RemoteViewsService.RemoteViewsFactory, Loader.OnLoadCompleteListener<Cursor> {
107f9df037f350fad73659307ba05f230d2db69051aMason Tang        private static final boolean LOGD = false;
1080c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa
1090c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa        // Suppress unnecessary logging about update time. Need to be static as this object is
1100c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa        // re-instanciated frequently.
1110c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa        // TODO: It seems loadData() is called via onCreate() four times, which should mean
1120c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa        // unnecessary CalendarFactory object is created and dropped. It is not efficient.
1130c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa        private static long sLastUpdateTime = UPDATE_TIME_NO_EVENTS;
1140c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa
11547d40324272ae39af0872bf5cbf27e1800478021Mason Tang        private Context mContext;
116a07cedd5af736339a7dd6166970311c487366500Daisuke Miyakawa        private Resources mResources;
11747d40324272ae39af0872bf5cbf27e1800478021Mason Tang        private CalendarAppWidgetModel mModel;
11847d40324272ae39af0872bf5cbf27e1800478021Mason Tang        private Cursor mCursor;
119c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik        private CursorLoader mLoader;
120c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik        private Handler mHandler = new Handler();
121c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik        private int mAppWidgetId;
122c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik
123c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik        private Runnable mTimezoneChanged = new Runnable() {
124c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            @Override
125c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            public void run() {
126c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik                if (mLoader != null) {
127c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik                    mLoader.forceLoad();
128c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik                }
129c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            }
130c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik        };
131c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik
132c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik        private Runnable mUpdateLoader = new Runnable() {
133c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            @Override
134c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            public void run() {
135c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik                if (mLoader != null) {
136c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik                    Uri uri = createLoaderUri();
137c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik                    mLoader.setUri(uri);
138c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik                    mLoader.forceLoad();
139c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik                }
140c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            }
141c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik        };
14247d40324272ae39af0872bf5cbf27e1800478021Mason Tang
14347d40324272ae39af0872bf5cbf27e1800478021Mason Tang        protected CalendarFactory(Context context, Intent intent) {
14447d40324272ae39af0872bf5cbf27e1800478021Mason Tang            mContext = context;
145a07cedd5af736339a7dd6166970311c487366500Daisuke Miyakawa            mResources = context.getResources();
146c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            mAppWidgetId = intent.getIntExtra(
147c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik                    AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
148bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
149bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
15047d40324272ae39af0872bf5cbf27e1800478021Mason Tang        @Override
15147d40324272ae39af0872bf5cbf27e1800478021Mason Tang        public void onCreate() {
152c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            initLoader();
153bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
154bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
15547d40324272ae39af0872bf5cbf27e1800478021Mason Tang        @Override
156d4b2dbb63039e8e63ef92b4b984aae9c68e1a3b5Winson Chung        public void onDataSetChanged() {
157d4b2dbb63039e8e63ef92b4b984aae9c68e1a3b5Winson Chung        }
158d4b2dbb63039e8e63ef92b4b984aae9c68e1a3b5Winson Chung
159d4b2dbb63039e8e63ef92b4b984aae9c68e1a3b5Winson Chung        @Override
16047d40324272ae39af0872bf5cbf27e1800478021Mason Tang        public void onDestroy() {
16147d40324272ae39af0872bf5cbf27e1800478021Mason Tang            mCursor.close();
162c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            mLoader.reset();
163c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            mContext.unregisterReceiver(this);
164bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
165bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
16647d40324272ae39af0872bf5cbf27e1800478021Mason Tang        @Override
16747d40324272ae39af0872bf5cbf27e1800478021Mason Tang        public RemoteViews getLoadingView() {
16847d40324272ae39af0872bf5cbf27e1800478021Mason Tang            RemoteViews views = new RemoteViews(mContext.getPackageName(),
16947d40324272ae39af0872bf5cbf27e1800478021Mason Tang                    R.layout.appwidget_loading);
17047d40324272ae39af0872bf5cbf27e1800478021Mason Tang            return views;
171bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
172bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
17347d40324272ae39af0872bf5cbf27e1800478021Mason Tang        @Override
17447d40324272ae39af0872bf5cbf27e1800478021Mason Tang        public RemoteViews getViewAt(int position) {
17547d40324272ae39af0872bf5cbf27e1800478021Mason Tang            // we use getCount here so that it doesn't return null when empty
17647d40324272ae39af0872bf5cbf27e1800478021Mason Tang            if (position < 0 || position >= getCount()) {
17747d40324272ae39af0872bf5cbf27e1800478021Mason Tang                return null;
17847d40324272ae39af0872bf5cbf27e1800478021Mason Tang            }
179bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
180c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            if (mModel == null || mModel.mEventInfos.isEmpty() || mModel.mRowInfos.isEmpty()) {
1813ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang                RemoteViews views = new RemoteViews(mContext.getPackageName(),
1823ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang                        R.layout.appwidget_no_events);
183bdbf15078ad5efdf27c021d7aca8c8aa4693878cMichael Chan                final Intent intent =  CalendarAppWidgetProvider.getLaunchFillInIntent(0, 0, 0);
1844143cfabb36f0c3adcad410a6eed29bc2b89d6e3Daisuke Miyakawa                views.setOnClickFillInIntent(R.id.appwidget_no_events, intent);
1853ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang                return views;
1863ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang            }
1873ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang
1883ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang            RowInfo rowInfo = mModel.mRowInfos.get(position);
1893ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang            if (rowInfo.mType == RowInfo.TYPE_DAY) {
1903ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang                RemoteViews views = new RemoteViews(mContext.getPackageName(),
1913ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang                        R.layout.appwidget_day);
1923ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang                DayInfo dayInfo = mModel.mDayInfos.get(rowInfo.mIndex);
1933ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang                updateTextView(views, R.id.date, View.VISIBLE, dayInfo.mDayLabel);
1943ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang                return views;
1953ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang            } else {
196a07cedd5af736339a7dd6166970311c487366500Daisuke Miyakawa                final RemoteViews views = new RemoteViews(mContext.getPackageName(),
19747d40324272ae39af0872bf5cbf27e1800478021Mason Tang                        R.layout.appwidget_row);
198a71e0a520493532dbb7e9bfa164ab78e59e797a3Daisuke Miyakawa                final EventInfo eventInfo = mModel.mEventInfos.get(rowInfo.mIndex);
199a71e0a520493532dbb7e9bfa164ab78e59e797a3Daisuke Miyakawa
200a07cedd5af736339a7dd6166970311c487366500Daisuke Miyakawa                final long now = System.currentTimeMillis();
201a07cedd5af736339a7dd6166970311c487366500Daisuke Miyakawa                if (!eventInfo.allDay && eventInfo.start <= now && now <= eventInfo.end) {
202a07cedd5af736339a7dd6166970311c487366500Daisuke Miyakawa                    views.setInt(R.id.appwidget_row, "setBackgroundColor",
203a07cedd5af736339a7dd6166970311c487366500Daisuke Miyakawa                            mResources.getColor(R.color.appwidget_row_in_progress));
204ffaeace621183dfe8770471a30b2f1138aac5f86Daisuke Miyakawa                } else {
205309c34fcce4912a9c6f1c0a39c090cebf61296beMichael Chan                    views.setInt(R.id.appwidget_row, "setBackgroundResource",
206309c34fcce4912a9c6f1c0a39c090cebf61296beMichael Chan                            R.drawable.bg_event_cal_widget_holo);
207a07cedd5af736339a7dd6166970311c487366500Daisuke Miyakawa                }
208a07cedd5af736339a7dd6166970311c487366500Daisuke Miyakawa
209a71e0a520493532dbb7e9bfa164ab78e59e797a3Daisuke Miyakawa                updateTextView(views, R.id.when, eventInfo.visibWhen, eventInfo.when);
210a71e0a520493532dbb7e9bfa164ab78e59e797a3Daisuke Miyakawa                updateTextView(views, R.id.where, eventInfo.visibWhere, eventInfo.where);
211a71e0a520493532dbb7e9bfa164ab78e59e797a3Daisuke Miyakawa                updateTextView(views, R.id.title, eventInfo.visibTitle, eventInfo.title);
212bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
213a71e0a520493532dbb7e9bfa164ab78e59e797a3Daisuke Miyakawa                views.setViewVisibility(R.id.color, View.VISIBLE);
214a71e0a520493532dbb7e9bfa164ab78e59e797a3Daisuke Miyakawa                views.setInt(R.id.color, "setBackgroundColor", eventInfo.color);
215bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
2164143cfabb36f0c3adcad410a6eed29bc2b89d6e3Daisuke Miyakawa                // An element in ListView.
217bdbf15078ad5efdf27c021d7aca8c8aa4693878cMichael Chan                final Intent fillInIntent = CalendarAppWidgetProvider.getLaunchFillInIntent(
218bdbf15078ad5efdf27c021d7aca8c8aa4693878cMichael Chan                        eventInfo.id, eventInfo.start, eventInfo.end);
2194143cfabb36f0c3adcad410a6eed29bc2b89d6e3Daisuke Miyakawa                views.setOnClickFillInIntent(R.id.appwidget_row, fillInIntent);
22047d40324272ae39af0872bf5cbf27e1800478021Mason Tang                return views;
22147d40324272ae39af0872bf5cbf27e1800478021Mason Tang            }
222bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
223bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
22447d40324272ae39af0872bf5cbf27e1800478021Mason Tang        @Override
22547d40324272ae39af0872bf5cbf27e1800478021Mason Tang        public int getViewTypeCount() {
2263ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang            return 4;
22747d40324272ae39af0872bf5cbf27e1800478021Mason Tang        }
228bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
22947d40324272ae39af0872bf5cbf27e1800478021Mason Tang        @Override
23047d40324272ae39af0872bf5cbf27e1800478021Mason Tang        public int getCount() {
23147d40324272ae39af0872bf5cbf27e1800478021Mason Tang            // if there are no events, we still return 1 to represent the "no
23247d40324272ae39af0872bf5cbf27e1800478021Mason Tang            // events" view
2333ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang            return Math.max(1, mModel.mRowInfos.size());
23447d40324272ae39af0872bf5cbf27e1800478021Mason Tang        }
235bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
23647d40324272ae39af0872bf5cbf27e1800478021Mason Tang        @Override
23747d40324272ae39af0872bf5cbf27e1800478021Mason Tang        public long getItemId(int position) {
238c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            RowInfo rowInfo = mModel.mRowInfos.get(position);
239c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            if (rowInfo.mType == RowInfo.TYPE_DAY) {
240c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik                return rowInfo.mIndex;
241c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            }
242c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            EventInfo eventInfo = mModel.mEventInfos.get(rowInfo.mIndex);
243c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            long prime = 31;
244c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            long result = 1;
245c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            result = prime * result + (int) (eventInfo.id ^ (eventInfo.id >>> 32));
246c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            result = prime * result + (int) (eventInfo.start ^ (eventInfo.start >>> 32));
247c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            return result;
24847d40324272ae39af0872bf5cbf27e1800478021Mason Tang        }
249bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
25047d40324272ae39af0872bf5cbf27e1800478021Mason Tang        @Override
25147d40324272ae39af0872bf5cbf27e1800478021Mason Tang        public boolean hasStableIds() {
25247d40324272ae39af0872bf5cbf27e1800478021Mason Tang            return true;
253bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
254bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
25547d40324272ae39af0872bf5cbf27e1800478021Mason Tang        /**
256c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik         * Query across all calendars for upcoming event instances from now
257c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik         * until some time in the future. Widen the time range that we query by
258c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik         * one day on each end so that we can catch all-day events. All-day
259c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik         * events are stored starting at midnight in UTC but should be included
260c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik         * in the list of events starting at midnight local time. This may fetch
261c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik         * more events than we actually want, so we filter them out later.
26247d40324272ae39af0872bf5cbf27e1800478021Mason Tang         *
26347d40324272ae39af0872bf5cbf27e1800478021Mason Tang         * @param resolver {@link ContentResolver} to use when querying
26447d40324272ae39af0872bf5cbf27e1800478021Mason Tang         *            {@link Instances#CONTENT_URI}.
26547d40324272ae39af0872bf5cbf27e1800478021Mason Tang         * @param searchDuration Distance into the future to look for event
26647d40324272ae39af0872bf5cbf27e1800478021Mason Tang         *            instances, in milliseconds.
26747d40324272ae39af0872bf5cbf27e1800478021Mason Tang         * @param now Current system time to use for this update, possibly from
26847d40324272ae39af0872bf5cbf27e1800478021Mason Tang         *            {@link System#currentTimeMillis()}.
26947d40324272ae39af0872bf5cbf27e1800478021Mason Tang         */
270c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik        public void initLoader() {
271c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            if (LOGD)
272c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik                Log.d(TAG, "Querying for widget events...");
273c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            IntentFilter filter = new IntentFilter();
274c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            filter.addAction(Intent.ACTION_PROVIDER_CHANGED);
275c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            filter.addDataScheme(ContentResolver.SCHEME_CONTENT);
276c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            filter.addDataAuthority(Calendar.AUTHORITY, null);
277c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            mContext.registerReceiver(this, filter);
278c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik
279c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            filter = new IntentFilter();
280c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
281c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            filter.addAction(Intent.ACTION_TIME_CHANGED);
282c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            filter.addAction(Intent.ACTION_DATE_CHANGED);
283c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            mContext.registerReceiver(this, filter);
284c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik
28547d40324272ae39af0872bf5cbf27e1800478021Mason Tang            // Search for events from now until some time in the future
286c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            Uri uri = createLoaderUri();
287c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik
288c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            mLoader = new CursorLoader(
289c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik                    mContext, uri, EVENT_PROJECTION, EVENT_SELECTION, null, EVENT_SORT_ORDER);
290c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            mLoader.setUpdateThrottle(WIDGET_UPDATE_THROTTLE);
291c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            mLoader.startLoading();
292c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            mLoader.registerListener(mAppWidgetId, this);
293c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik
294c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik        }
295bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
296c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik        /**
297c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik         * @return The uri for the loader
298c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik         */
299c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik        private Uri createLoaderUri() {
300c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            long now = System.currentTimeMillis();
30147d40324272ae39af0872bf5cbf27e1800478021Mason Tang            // Add a day on either side to catch all-day events
30247d40324272ae39af0872bf5cbf27e1800478021Mason Tang            long begin = now - DateUtils.DAY_IN_MILLIS;
303c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            long end = now + SEARCH_DURATION + DateUtils.DAY_IN_MILLIS;
304bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
305e0cb5ba884c52e9d36875fb4a9ebdf40a81cb642Michael Chan            Uri uri = Uri.withAppendedPath(Instances.CONTENT_URI, Long.toString(begin) + "/" + end);
306c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            return uri;
30721a183875fbbfa54f5a2a87779888a5fb7d1af44Erik        }
30821a183875fbbfa54f5a2a87779888a5fb7d1af44Erik
30947d40324272ae39af0872bf5cbf27e1800478021Mason Tang        @VisibleForTesting
31047d40324272ae39af0872bf5cbf27e1800478021Mason Tang        protected static CalendarAppWidgetModel buildAppWidgetModel(
31121a183875fbbfa54f5a2a87779888a5fb7d1af44Erik                Context context, Cursor cursor, String timeZone) {
312ece2fbd8c2695910148ffa20fb46a508443fd034Michael Chan            CalendarAppWidgetModel model = new CalendarAppWidgetModel(context, timeZone);
31321a183875fbbfa54f5a2a87779888a5fb7d1af44Erik            model.buildFromCursor(cursor, timeZone);
31447d40324272ae39af0872bf5cbf27e1800478021Mason Tang            return model;
315bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
316bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
31747d40324272ae39af0872bf5cbf27e1800478021Mason Tang        /**
3180c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa         * Calculates and returns the next time we should push widget updates.
31947d40324272ae39af0872bf5cbf27e1800478021Mason Tang         */
320ece2fbd8c2695910148ffa20fb46a508443fd034Michael Chan        private long calculateUpdateTime(CalendarAppWidgetModel model, long now, String timeZone) {
3210c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa            // Make sure an update happens at midnight or earlier
322ece2fbd8c2695910148ffa20fb46a508443fd034Michael Chan            long minUpdateTime = getNextMidnightTimeMillis(timeZone);
3230c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa            for (EventInfo event : model.mEventInfos) {
3240c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa                final boolean allDay = event.allDay;
3250c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa                final long start;
3260c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa                final long end;
32747d40324272ae39af0872bf5cbf27e1800478021Mason Tang                if (allDay) {
3280c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa                    // Adjust all-day times into local timezone
32947d40324272ae39af0872bf5cbf27e1800478021Mason Tang                    final Time recycle = new Time();
3300c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa                    start = Utils.convertUtcToLocal(recycle, event.start);
3310c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa                    end = Utils.convertUtcToLocal(recycle, event.end);
3320c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa                } else {
3330c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa                    start = event.start;
3340c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa                    end = event.end;
335bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                }
336bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
3370c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa                // We want to update widget when we enter/exit time range of an event.
3380c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa                if (now < start) {
3390c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa                    minUpdateTime = Math.min(minUpdateTime, start);
3400c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa                } else if (now < end) {
3410c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa                    minUpdateTime = Math.min(minUpdateTime, end);
3420c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa                }
343bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            }
3440c715c837a7ecc2cfda3a62d952f8dc7e79a39f3Daisuke Miyakawa            return minUpdateTime;
345bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
346bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
347ece2fbd8c2695910148ffa20fb46a508443fd034Michael Chan        private static long getNextMidnightTimeMillis(String timezone) {
34847d40324272ae39af0872bf5cbf27e1800478021Mason Tang            Time time = new Time();
34947d40324272ae39af0872bf5cbf27e1800478021Mason Tang            time.setToNow();
35047d40324272ae39af0872bf5cbf27e1800478021Mason Tang            time.monthDay++;
35147d40324272ae39af0872bf5cbf27e1800478021Mason Tang            time.hour = 0;
35247d40324272ae39af0872bf5cbf27e1800478021Mason Tang            time.minute = 0;
35347d40324272ae39af0872bf5cbf27e1800478021Mason Tang            time.second = 0;
354ece2fbd8c2695910148ffa20fb46a508443fd034Michael Chan            long midnightDeviceTz = time.normalize(true);
355ece2fbd8c2695910148ffa20fb46a508443fd034Michael Chan
356ece2fbd8c2695910148ffa20fb46a508443fd034Michael Chan            time.timezone = timezone;
357ece2fbd8c2695910148ffa20fb46a508443fd034Michael Chan            time.setToNow();
358ece2fbd8c2695910148ffa20fb46a508443fd034Michael Chan            time.monthDay++;
359ece2fbd8c2695910148ffa20fb46a508443fd034Michael Chan            time.hour = 0;
360ece2fbd8c2695910148ffa20fb46a508443fd034Michael Chan            time.minute = 0;
361ece2fbd8c2695910148ffa20fb46a508443fd034Michael Chan            time.second = 0;
362ece2fbd8c2695910148ffa20fb46a508443fd034Michael Chan            long midnightHomeTz = time.normalize(true);
363ece2fbd8c2695910148ffa20fb46a508443fd034Michael Chan
364ece2fbd8c2695910148ffa20fb46a508443fd034Michael Chan            return Math.min(midnightDeviceTz, midnightHomeTz);
365bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
366bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
36747d40324272ae39af0872bf5cbf27e1800478021Mason Tang        static void updateTextView(RemoteViews views, int id, int visibility, String string) {
36847d40324272ae39af0872bf5cbf27e1800478021Mason Tang            views.setViewVisibility(id, visibility);
36947d40324272ae39af0872bf5cbf27e1800478021Mason Tang            if (visibility == View.VISIBLE) {
37047d40324272ae39af0872bf5cbf27e1800478021Mason Tang                views.setTextViewText(id, string);
37147d40324272ae39af0872bf5cbf27e1800478021Mason Tang            }
37247d40324272ae39af0872bf5cbf27e1800478021Mason Tang        }
373c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik
374c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik        /*
375c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik         * (non-Javadoc)
376c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik         * @see
377c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik         * android.content.Loader.OnLoadCompleteListener#onLoadComplete(android
378c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik         * .content.Loader, java.lang.Object)
379c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik         */
380c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik        @Override
381c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik        public void onLoadComplete(Loader<Cursor> loader, Cursor cursor) {
382c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            // Copy it to a local static cursor.
383c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            MatrixCursor matrixCursor = Utils.matrixCursorFromCursor(cursor);
384c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            cursor.close();
385c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik
386c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            final long now = System.currentTimeMillis();
387c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            if (mCursor != null) {
388c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik                mCursor.close();
389c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            }
390c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            mCursor = matrixCursor;
391c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            String tz = Utils.getTimeZone(mContext, mTimezoneChanged);
392c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            mModel = buildAppWidgetModel(mContext, mCursor, tz);
393c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik
394c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            // Schedule an alarm to wake ourselves up for the next update.
395c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            // We also cancel
396c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            // all existing wake-ups because PendingIntents don't match
397c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            // against extras.
398c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            long triggerTime = calculateUpdateTime(mModel, now, tz);
399c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik
400c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            // If no next-update calculated, or bad trigger time in past,
401c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            // schedule
402c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            // update about six hours from now.
403c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            if (triggerTime < now) {
404c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik                Log.w(TAG, "Encountered bad trigger time " + formatDebugTime(triggerTime, now));
405c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik                triggerTime = now + UPDATE_TIME_NO_EVENTS;
406c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            }
407c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik
408c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            mHandler.postDelayed(mUpdateLoader, triggerTime - now);
409c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik
410c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            AppWidgetManager.getInstance(mContext).notifyAppWidgetViewDataChanged(
411c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik                    mAppWidgetId, R.id.events_list);
412c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik        }
413c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik
414c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik        @Override
415c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik        public void onReceive(Context context, Intent intent) {
416c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            mHandler.removeCallbacks(mUpdateLoader);
417c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik            mHandler.post(mUpdateLoader);
418c46c2dc5dbec57616d799b1d0290d7c827b48d0cRoboErik        }
4193ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang    }
420bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
4213ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang    /**
4223ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang     * Format given time for debugging output.
4233ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang     *
4243ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang     * @param unixTime Target time to report.
4253ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang     * @param now Current system time from {@link System#currentTimeMillis()}
4263ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang     *            for calculating time difference.
4273ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang     */
4283ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang    static String formatDebugTime(long unixTime, long now) {
4293ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang        Time time = new Time();
4303ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang        time.set(unixTime);
4313ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang
4323ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang        long delta = unixTime - now;
4333ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang        if (delta > DateUtils.MINUTE_IN_MILLIS) {
4343ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang            delta /= DateUtils.MINUTE_IN_MILLIS;
4353ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang            return String.format("[%d] %s (%+d mins)", unixTime,
4363ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang                    time.format("%H:%M:%S"), delta);
4373ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang        } else {
4383ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang            delta /= DateUtils.SECOND_IN_MILLIS;
4393ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang            return String.format("[%d] %s (%+d secs)", unixTime,
4403ea333d41c04fd5f3a5d45f540c17894874429e8Mason Tang                    time.format("%H:%M:%S"), delta);
441bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
442bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    }
443bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang}
444