/*
* Copyright (C) 2013 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.embeddedlog.LightUpDroid;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.view.View;
import com.embeddedlog.LightUpDroid.provider.Alarm;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.TreeMap;
/**
* Renders a tree-like view of the next alarm times over the period of a week.
* The timeline begins at the time of the next alarm, and ends a week after that time.
* The view is currently only shown in the landscape mode of tablets.
*/
public class AlarmTimelineView extends View {
private static final String TAG = "AlarmTimelineView";
private static final String FORMAT_12_HOUR = "E h mm a";
private static final String FORMAT_24_HOUR = "E H mm";
private static final int DAYS_IN_WEEK = 7;
private int mAlarmTimelineColor;
private int mAlarmTimelineLength;
private int mAlarmTimelineMarginTop;
private int mAlarmTimelineMarginBottom;
private int mAlarmNodeRadius;
private int mAlarmNodeInnerRadius;
private int mAlarmNodeInnerRadiusColor;
private int mAlarmTextPadding;
private int mAlarmTextSize;
private int mAlarmMinDistance;
private Paint mPaint;
private ContentResolver mResolver;
private SimpleDateFormat mDateFormat;
private TreeMap<Date, AlarmTimeNode> mAlarmTimes = new TreeMap<Date, AlarmTimeNode>();
private Calendar mCalendar;
private AlarmObserver mAlarmObserver = new AlarmObserver(getHandler());
private GetAlarmsTask mAlarmsTask = new GetAlarmsTask();
private String mNoAlarmsScheduled;
private boolean mIsAnimatingOut;
/**
* Observer for any changes to the alarms in the content provider.
*/
private class AlarmObserver extends ContentObserver {
public AlarmObserver(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean changed) {
if (mAlarmsTask != null) {
mAlarmsTask.cancel(true);
}
mAlarmsTask = new GetAlarmsTask();
mAlarmsTask.execute();
}
@Override
public void onChange(boolean changed, Uri uri) {
onChange(changed);
}
}
/**
* The data model for one node on the timeline.
*/
private class AlarmTimeNode {
public Date date;
public boolean isRepeating;
public AlarmTimeNode(Date date, boolean isRepeating) {
this.date = date;
this.isRepeating = isRepeating;
}
}
/**
* Retrieves alarms from the content provider and generates an alarm node tree sorted by date.
*/
private class GetAlarmsTask extends AsyncTask<Void, Void, Void> {
@Override
protected synchronized Void doInBackground(Void... params) {
List<Alarm> enabledAlarmList = Alarm.getAlarms(mResolver, Alarm.ENABLED + "=1");
final Date currentTime = mCalendar.getTime();
mAlarmTimes.clear();
for (Alarm alarm : enabledAlarmList) {
int hour = alarm.hour;
int minutes = alarm.minutes;
HashSet<Integer> repeatingDays = alarm.daysOfWeek.getSetDays();
// If the alarm is not repeating,
if (repeatingDays.isEmpty()) {
mCalendar.add(Calendar.DATE, getDaysFromNow(hour, minutes));
mCalendar.set(Calendar.HOUR_OF_DAY, alarm.hour);
mCalendar.set(Calendar.MINUTE, alarm.minutes);
Date date = mCalendar.getTime();
if (!mAlarmTimes.containsKey(date)) {
// Add alarm if there is no other alarm with this date.
mAlarmTimes.put(date, new AlarmTimeNode(date, false));
}
mCalendar.setTime(currentTime);
continue;
}
// If the alarm is repeating, iterate through each alarm date.
for (int day : alarm.daysOfWeek.getSetDays()) {
mCalendar.add(Calendar.DATE, getDaysFromNow(day, hour, minutes));
mCalendar.set(Calendar.HOUR_OF_DAY, alarm.hour);
mCalendar.set(Calendar.MINUTE, alarm.minutes);
Date date = mCalendar.getTime();
if (!mAlarmTimes.containsKey(date)) {
// Add alarm if there is no other alarm with this date.
mAlarmTimes.put(date, new AlarmTimeNode(mCalendar.getTime(), true));
} else {
// If there is another alarm with this date, make it
// repeating.
mAlarmTimes.get(date).isRepeating = true;
}
mCalendar.setTime(currentTime);
}
}
return null;
}
@Override
protected void onPostExecute(Void result) {
requestLayout();
AlarmTimelineView.this.invalidate();
}
// Returns whether this non-repeating alarm is firing today or tomorrow.
private int getDaysFromNow(int hour, int minutes) {
final int currentHour = mCalendar.get(Calendar.HOUR_OF_DAY);
if (hour > currentHour ||
(hour == currentHour && minutes >= mCalendar.get(Calendar.MINUTE)) ) {
return 0;
}
return 1;
}
// Returns the days from now of the next instance of this alarm, given the repeated day.
private int getDaysFromNow(int day, int hour, int minute) {
final int currentDay = mCalendar.get(Calendar.DAY_OF_WEEK);
if (day != currentDay) {
if (day < currentDay) {
day += DAYS_IN_WEEK;
}
return day - currentDay;
}
final int currentHour = mCalendar.get(Calendar.HOUR_OF_DAY);
if (hour != currentHour) {
return (hour < currentHour) ? DAYS_IN_WEEK : 0;
}
final int currentMinute = mCalendar.get(Calendar.MINUTE);
return (minute < currentMinute) ? DAYS_IN_WEEK : 0;
}
}
public AlarmTimelineView(Context context) {
super(context);
init(context);
}
public AlarmTimelineView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
mResolver = context.getContentResolver();
final Resources res = context.getResources();
mAlarmTimelineColor = res.getColor(R.color.alarm_timeline_color);
mAlarmTimelineLength = res.getDimensionPixelOffset(R.dimen.alarm_timeline_length);
mAlarmTimelineMarginTop = res.getDimensionPixelOffset(R.dimen.alarm_timeline_margin_top);
mAlarmTimelineMarginBottom = res.getDimensionPixelOffset(R.dimen.footer_button_size) +
2 * res.getDimensionPixelOffset(R.dimen.footer_button_layout_margin);
mAlarmNodeRadius = res.getDimensionPixelOffset(R.dimen.alarm_timeline_radius);
mAlarmNodeInnerRadius = res.getDimensionPixelOffset(R.dimen.alarm_timeline_inner_radius);
mAlarmNodeInnerRadiusColor = res.getColor(R.color.primary);
mAlarmTextSize = res.getDimensionPixelOffset(R.dimen.alarm_text_font_size);
mAlarmTextPadding = res.getDimensionPixelOffset(R.dimen.alarm_text_padding);
mAlarmMinDistance = res.getDimensionPixelOffset(R.dimen.alarm_min_distance) +
2 * mAlarmNodeRadius;
mNoAlarmsScheduled = context.getString(R.string.no_upcoming_alarms);
mPaint = new Paint();
mPaint.setTextSize(mAlarmTextSize);
mPaint.setStrokeWidth(res.getDimensionPixelOffset(R.dimen.alarm_timeline_width));
mPaint.setAntiAlias(true);
mCalendar = Calendar.getInstance();
final Locale locale = Locale.getDefault();
String formatString = DateFormat.is24HourFormat(context) ? FORMAT_24_HOUR : FORMAT_12_HOUR;
mDateFormat = new SimpleDateFormat(formatString, locale);
mAlarmsTask.execute();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
mResolver.registerContentObserver(Alarm.CONTENT_URI, true, mAlarmObserver);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
mResolver.unregisterContentObserver(mAlarmObserver);
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int timelineHeight = !mAlarmTimes.isEmpty() ? mAlarmTimelineLength : 0;
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
timelineHeight + mAlarmTimelineMarginTop + mAlarmTimelineMarginBottom);
}
@Override
public synchronized void onDraw(Canvas canvas) {
// If the view is in the process of animating out, do not change the text or the timeline.
if (mIsAnimatingOut) {
return;
}
super.onDraw(canvas);
final int x = getWidth() / 2;
int y = mAlarmTimelineMarginTop;
mPaint.setColor(mAlarmTimelineColor);
// If there are no alarms, draw the no alarms text.
if (mAlarmTimes == null || mAlarmTimes.isEmpty()) {
mPaint.setTextAlign(Align.CENTER);
canvas.drawText(mNoAlarmsScheduled, x, y, mPaint);
return;
}
// Draw the timeline.
canvas.drawLine(x, y, x, y + mAlarmTimelineLength, mPaint);
final int xLeft = x - mAlarmNodeRadius - mAlarmTextPadding;
final int xRight = x + mAlarmNodeRadius + mAlarmTextPadding;
// Iterate through each of the alarm times chronologically.
Iterator<AlarmTimeNode> iter = mAlarmTimes.values().iterator();
Date firstDate = null;
int prevY = 0;
int i=0;
final int maxY = mAlarmTimelineLength + mAlarmTimelineMarginTop;
while (iter.hasNext()) {
AlarmTimeNode node = iter.next();
Date date = node.date;
if (firstDate == null) {
// If this is the first alarm, set the node to the top of the timeline.
y = mAlarmTimelineMarginTop;
firstDate = date;
} else {
// If this is not the first alarm, set the distance based upon the time from the
// first alarm. If a node already exists at that time, use the minimum distance
// required from the last drawn node.
y = Math.max(convertToDistance(date, firstDate), prevY + mAlarmMinDistance);
}
if (y > maxY) {
// If the y value has somehow exceeded the timeline length, draw node on end of
// timeline. We should never reach this state.
Log.wtf("Y-value exceeded timeline length. Should never happen.");
Log.wtf("alarm date=" + node.date.getTime() + ", isRepeating=" + node.isRepeating
+ ", y=" + y + ", maxY=" + maxY);
y = maxY;
}
// Draw the node.
mPaint.setColor(Color.WHITE);
canvas.drawCircle(x, y, mAlarmNodeRadius, mPaint);
// If the node is not repeating, draw an inner circle to make the node "open".
if (!node.isRepeating) {
mPaint.setColor(mAlarmNodeInnerRadiusColor);
canvas.drawCircle(x, y, mAlarmNodeInnerRadius, mPaint);
}
prevY = y;
// Draw the alarm text. Alternate left and right of the timeline.
final String timeString = mDateFormat.format(date).toUpperCase();
mPaint.setColor(mAlarmTimelineColor);
if (i % 2 == 0) {
mPaint.setTextAlign(Align.RIGHT);
canvas.drawText(timeString, xLeft, y + mAlarmTextSize / 3, mPaint);
} else {
mPaint.setTextAlign(Align.LEFT);
canvas.drawText(timeString, xRight, y + mAlarmTextSize / 3, mPaint);
}
i++;
}
}
// This method is necessary to ensure that the view does not re-draw while it is being
// animated out. The timeline should remain on-screen as is, even though no alarms
// are present, as the view moves off-screen.
public void setIsAnimatingOut(boolean animatingOut) {
mIsAnimatingOut = animatingOut;
}
// Convert the time difference between the date and the first date to a distance along the
// timeline.
private int convertToDistance(final Date date, final Date firstDate) {
if (date == null || firstDate == null) {
return 0;
}
return (int) ((date.getTime() - firstDate.getTime())
* mAlarmTimelineLength / DateUtils.WEEK_IN_MILLIS + mAlarmTimelineMarginTop);
}
}