Home | History | Annotate | Download | only in deskclock
      1 /*
      2  * Copyright (C) 2013 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 package com.android.deskclock;
     17 
     18 import android.content.ContentResolver;
     19 import android.content.Context;
     20 import android.content.res.Resources;
     21 import android.database.ContentObserver;
     22 import android.graphics.Canvas;
     23 import android.graphics.Color;
     24 import android.graphics.Paint;
     25 import android.graphics.Paint.Align;
     26 import android.net.Uri;
     27 import android.os.AsyncTask;
     28 import android.os.Handler;
     29 import android.text.format.DateFormat;
     30 import android.text.format.DateUtils;
     31 import android.util.AttributeSet;
     32 import android.view.View;
     33 
     34 import com.android.deskclock.provider.Alarm;
     35 
     36 import java.text.SimpleDateFormat;
     37 import java.util.Calendar;
     38 import java.util.Date;
     39 import java.util.HashSet;
     40 import java.util.Iterator;
     41 import java.util.List;
     42 import java.util.Locale;
     43 import java.util.TreeMap;
     44 
     45 /**
     46  * Renders a tree-like view of the next alarm times over the period of a week.
     47  * The timeline begins at the time of the next alarm, and ends a week after that time.
     48  * The view is currently only shown in the landscape mode of tablets.
     49  */
     50 public class AlarmTimelineView extends View {
     51 
     52     private static final String TAG = "AlarmTimelineView";
     53 
     54     private static final String FORMAT_12_HOUR = "E h mm a";
     55     private static final String FORMAT_24_HOUR = "E H mm";
     56 
     57     private static final int DAYS_IN_WEEK = 7;
     58 
     59     private int mAlarmTimelineColor;
     60     private int mAlarmTimelineLength;
     61     private int mAlarmTimelineMarginTop;
     62     private int mAlarmTimelineMarginBottom;
     63     private int mAlarmNodeRadius;
     64     private int mAlarmNodeInnerRadius;
     65     private int mAlarmNodeInnerRadiusColor;
     66     private int mAlarmTextPadding;
     67     private int mAlarmTextSize;
     68     private int mAlarmMinDistance;
     69 
     70     private Paint mPaint;
     71     private ContentResolver mResolver;
     72     private SimpleDateFormat mDateFormat;
     73     private TreeMap<Date, AlarmTimeNode> mAlarmTimes = new TreeMap<Date, AlarmTimeNode>();
     74     private Calendar mCalendar;
     75     private AlarmObserver mAlarmObserver = new AlarmObserver(getHandler());
     76     private GetAlarmsTask mAlarmsTask = new GetAlarmsTask();
     77     private String mNoAlarmsScheduled;
     78     private boolean mIsAnimatingOut;
     79 
     80     /**
     81      * Observer for any changes to the alarms in the content provider.
     82      */
     83     private class AlarmObserver extends ContentObserver {
     84 
     85         public AlarmObserver(Handler handler) {
     86             super(handler);
     87         }
     88 
     89         @Override
     90         public void onChange(boolean changed) {
     91             if (mAlarmsTask != null) {
     92                 mAlarmsTask.cancel(true);
     93             }
     94             mAlarmsTask = new GetAlarmsTask();
     95             mAlarmsTask.execute();
     96         }
     97 
     98         @Override
     99         public void onChange(boolean changed, Uri uri) {
    100             onChange(changed);
    101         }
    102     }
    103 
    104     /**
    105      * The data model for one node on the timeline.
    106      */
    107     private class AlarmTimeNode {
    108         public Date date;
    109         public boolean isRepeating;
    110 
    111         public AlarmTimeNode(Date date, boolean isRepeating) {
    112             this.date = date;
    113             this.isRepeating = isRepeating;
    114         }
    115     }
    116 
    117     /**
    118      * Retrieves alarms from the content provider and generates an alarm node tree sorted by date.
    119      */
    120     private class GetAlarmsTask extends AsyncTask<Void, Void, Void> {
    121 
    122         @Override
    123         protected synchronized Void doInBackground(Void... params) {
    124             List<Alarm> enabledAlarmList = Alarm.getAlarms(mResolver, Alarm.ENABLED + "=1");
    125             final Date currentTime = mCalendar.getTime();
    126             mAlarmTimes.clear();
    127             for (Alarm alarm : enabledAlarmList) {
    128                 int hour = alarm.hour;
    129                 int minutes = alarm.minutes;
    130                 HashSet<Integer> repeatingDays = alarm.daysOfWeek.getSetDays();
    131 
    132                 // If the alarm is not repeating,
    133                 if (repeatingDays.isEmpty()) {
    134                     mCalendar.add(Calendar.DATE, getDaysFromNow(hour, minutes));
    135                     mCalendar.set(Calendar.HOUR_OF_DAY, alarm.hour);
    136                     mCalendar.set(Calendar.MINUTE, alarm.minutes);
    137                     Date date = mCalendar.getTime();
    138 
    139                     if (!mAlarmTimes.containsKey(date)) {
    140                         // Add alarm if there is no other alarm with this date.
    141                         mAlarmTimes.put(date, new AlarmTimeNode(date, false));
    142                     }
    143                     mCalendar.setTime(currentTime);
    144                     continue;
    145                 }
    146 
    147                 // If the alarm is repeating, iterate through each alarm date.
    148                 for (int day : alarm.daysOfWeek.getSetDays()) {
    149                     mCalendar.add(Calendar.DATE, getDaysFromNow(day, hour, minutes));
    150                     mCalendar.set(Calendar.HOUR_OF_DAY, alarm.hour);
    151                     mCalendar.set(Calendar.MINUTE, alarm.minutes);
    152                     Date date = mCalendar.getTime();
    153 
    154                     if (!mAlarmTimes.containsKey(date)) {
    155                         // Add alarm if there is no other alarm with this date.
    156                         mAlarmTimes.put(date, new AlarmTimeNode(mCalendar.getTime(), true));
    157                     } else {
    158                         // If there is another alarm with this date, make it
    159                         // repeating.
    160                         mAlarmTimes.get(date).isRepeating = true;
    161                     }
    162                     mCalendar.setTime(currentTime);
    163                 }
    164             }
    165             return null;
    166         }
    167 
    168         @Override
    169         protected void onPostExecute(Void result) {
    170             requestLayout();
    171             AlarmTimelineView.this.invalidate();
    172         }
    173 
    174         // Returns whether this non-repeating alarm is firing today or tomorrow.
    175         private int getDaysFromNow(int hour, int minutes) {
    176             final int currentHour = mCalendar.get(Calendar.HOUR_OF_DAY);
    177             if (hour > currentHour ||
    178                     (hour == currentHour && minutes >= mCalendar.get(Calendar.MINUTE)) ) {
    179                 return 0;
    180             }
    181             return 1;
    182         }
    183 
    184         // Returns the days from now of the next instance of this alarm, given the repeated day.
    185         private int getDaysFromNow(int day, int hour, int minute) {
    186             final int currentDay = mCalendar.get(Calendar.DAY_OF_WEEK);
    187             if (day != currentDay) {
    188                 if (day < currentDay) {
    189                     day += DAYS_IN_WEEK;
    190                 }
    191                 return day - currentDay;
    192             }
    193 
    194             final int currentHour = mCalendar.get(Calendar.HOUR_OF_DAY);
    195             if (hour != currentHour) {
    196                 return (hour < currentHour) ? DAYS_IN_WEEK : 0;
    197             }
    198 
    199             final int currentMinute = mCalendar.get(Calendar.MINUTE);
    200             return (minute < currentMinute) ? DAYS_IN_WEEK : 0;
    201         }
    202     }
    203 
    204     public AlarmTimelineView(Context context) {
    205         super(context);
    206         init(context);
    207     }
    208 
    209     public AlarmTimelineView(Context context, AttributeSet attrs) {
    210         super(context, attrs);
    211         init(context);
    212     }
    213 
    214     private void init(Context context) {
    215         mResolver = context.getContentResolver();
    216 
    217         final Resources res = context.getResources();
    218 
    219         mAlarmTimelineColor = res.getColor(R.color.alarm_timeline_color);
    220         mAlarmTimelineLength = res.getDimensionPixelOffset(R.dimen.alarm_timeline_length);
    221         mAlarmTimelineMarginTop = res.getDimensionPixelOffset(R.dimen.alarm_timeline_margin_top);
    222         mAlarmTimelineMarginBottom = res.getDimensionPixelOffset(R.dimen.footer_button_size) +
    223                 2 * res.getDimensionPixelOffset(R.dimen.footer_button_layout_margin);
    224         mAlarmNodeRadius = res.getDimensionPixelOffset(R.dimen.alarm_timeline_radius);
    225         mAlarmNodeInnerRadius = res.getDimensionPixelOffset(R.dimen.alarm_timeline_inner_radius);
    226         mAlarmNodeInnerRadiusColor = res.getColor(R.color.blackish);
    227         mAlarmTextSize = res.getDimensionPixelOffset(R.dimen.alarm_text_font_size);
    228         mAlarmTextPadding = res.getDimensionPixelOffset(R.dimen.alarm_text_padding);
    229         mAlarmMinDistance = res.getDimensionPixelOffset(R.dimen.alarm_min_distance) +
    230                 2 * mAlarmNodeRadius;
    231         mNoAlarmsScheduled = context.getString(R.string.no_upcoming_alarms);
    232 
    233         mPaint = new Paint();
    234         mPaint.setTextSize(mAlarmTextSize);
    235         mPaint.setStrokeWidth(res.getDimensionPixelOffset(R.dimen.alarm_timeline_width));
    236         mPaint.setAntiAlias(true);
    237 
    238         mCalendar = Calendar.getInstance();
    239         final Locale locale = Locale.getDefault();
    240         String formatString = DateFormat.is24HourFormat(context) ? FORMAT_24_HOUR : FORMAT_12_HOUR;
    241         String format = DateFormat.getBestDateTimePattern(locale, formatString);
    242         mDateFormat = new SimpleDateFormat(format, locale);
    243 
    244         mAlarmsTask.execute();
    245     }
    246 
    247     @Override
    248     public void onAttachedToWindow() {
    249         super.onAttachedToWindow();
    250         mResolver.registerContentObserver(Alarm.CONTENT_URI, true, mAlarmObserver);
    251     }
    252 
    253     @Override
    254     public void onDetachedFromWindow() {
    255         super.onDetachedFromWindow();
    256         mResolver.unregisterContentObserver(mAlarmObserver);
    257     }
    258 
    259     @Override
    260     public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    261         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    262         int timelineHeight = !mAlarmTimes.isEmpty() ?  mAlarmTimelineLength : 0;
    263         setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
    264                 timelineHeight + mAlarmTimelineMarginTop + mAlarmTimelineMarginBottom);
    265     }
    266 
    267     @Override
    268     public synchronized void onDraw(Canvas canvas) {
    269 
    270         // If the view is in the process of animating out, do not change the text or the timeline.
    271         if (mIsAnimatingOut) {
    272             return;
    273         }
    274 
    275         super.onDraw(canvas);
    276 
    277         final int x = getWidth() / 2;
    278         int y = mAlarmTimelineMarginTop;
    279 
    280         mPaint.setColor(mAlarmTimelineColor);
    281 
    282         // If there are no alarms, draw the no alarms text.
    283         if (mAlarmTimes == null || mAlarmTimes.isEmpty()) {
    284             mPaint.setTextAlign(Align.CENTER);
    285             canvas.drawText(mNoAlarmsScheduled, x, y, mPaint);
    286             return;
    287         }
    288 
    289         // Draw the timeline.
    290         canvas.drawLine(x, y, x, y + mAlarmTimelineLength, mPaint);
    291 
    292         final int xLeft = x - mAlarmNodeRadius - mAlarmTextPadding;
    293         final int xRight = x + mAlarmNodeRadius + mAlarmTextPadding;
    294 
    295         // Iterate through each of the alarm times chronologically.
    296         Iterator<AlarmTimeNode> iter = mAlarmTimes.values().iterator();
    297         Date firstDate = null;
    298         int prevY = 0;
    299         int i=0;
    300         final int maxY = mAlarmTimelineLength + mAlarmTimelineMarginTop;
    301         while (iter.hasNext()) {
    302             AlarmTimeNode node = iter.next();
    303             Date date = node.date;
    304 
    305             if (firstDate == null) {
    306                 // If this is the first alarm, set the node to the top of the timeline.
    307                 y = mAlarmTimelineMarginTop;
    308                 firstDate = date;
    309             } else {
    310                 // If this is not the first alarm, set the distance based upon the time from the
    311                 // first alarm.  If a node already exists at that time, use the minimum distance
    312                 // required from the last drawn node.
    313                 y = Math.max(convertToDistance(date, firstDate), prevY + mAlarmMinDistance);
    314             }
    315 
    316             if (y > maxY) {
    317                 // If the y value has somehow exceeded the timeline length, draw node on end of
    318                 // timeline.  We should never reach this state.
    319                 Log.wtf("Y-value exceeded timeline length.  Should never happen.");
    320                 Log.wtf("alarm date=" + node.date.getTime() + ", isRepeating=" + node.isRepeating
    321                         + ", y=" + y + ", maxY=" + maxY);
    322                 y = maxY;
    323             }
    324 
    325             // Draw the node.
    326             mPaint.setColor(Color.WHITE);
    327             canvas.drawCircle(x, y, mAlarmNodeRadius, mPaint);
    328 
    329             // If the node is not repeating, draw an inner circle to make the node "open".
    330             if (!node.isRepeating) {
    331                 mPaint.setColor(mAlarmNodeInnerRadiusColor);
    332                 canvas.drawCircle(x, y, mAlarmNodeInnerRadius, mPaint);
    333             }
    334             prevY = y;
    335 
    336             // Draw the alarm text.  Alternate left and right of the timeline.
    337             final String timeString = mDateFormat.format(date).toUpperCase();
    338             mPaint.setColor(mAlarmTimelineColor);
    339             if (i % 2 == 0) {
    340                 mPaint.setTextAlign(Align.RIGHT);
    341                 canvas.drawText(timeString, xLeft, y + mAlarmTextSize / 3, mPaint);
    342             } else {
    343                 mPaint.setTextAlign(Align.LEFT);
    344                 canvas.drawText(timeString, xRight, y + mAlarmTextSize / 3, mPaint);
    345             }
    346             i++;
    347         }
    348     }
    349 
    350     // This method is necessary to ensure that the view does not re-draw while it is being
    351     // animated out.  The timeline should remain on-screen as is, even though no alarms
    352     // are present, as the view moves off-screen.
    353     public void setIsAnimatingOut(boolean animatingOut) {
    354         mIsAnimatingOut = animatingOut;
    355     }
    356 
    357     // Convert the time difference between the date and the first date to a distance along the
    358     // timeline.
    359     private int convertToDistance(final Date date, final Date firstDate) {
    360         if (date == null || firstDate == null) {
    361             return 0;
    362         }
    363         return (int) ((date.getTime() - firstDate.getTime())
    364                 * mAlarmTimelineLength / DateUtils.WEEK_IN_MILLIS + mAlarmTimelineMarginTop);
    365     }
    366 }
    367