Java tutorial
/* * A Time Tracker - Open Source Time Tracker for Android * * Copyright (C) 2013 Markus Kils <markus@markuspage.com> * Copyright (C) 2008, 2009, 2010 Sean Russell <ser@germane-software.com> * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /** * TimeTracker 2008, 2009 Sean Russell * * @author Sean Russell <ser@germane-software.com> */ package com.markuspage.android.atimetracker; import static com.markuspage.android.atimetracker.DBHelper.END; import static com.markuspage.android.atimetracker.DBHelper.NAME; import static com.markuspage.android.atimetracker.DBHelper.RANGES_TABLE; import static com.markuspage.android.atimetracker.DBHelper.RANGE_COLUMNS; import static com.markuspage.android.atimetracker.DBHelper.START; import static com.markuspage.android.atimetracker.DBHelper.TASK_COLUMNS; import static com.markuspage.android.atimetracker.DBHelper.TASK_ID; import static com.markuspage.android.atimetracker.DBHelper.TASK_TABLE; import static com.markuspage.android.atimetracker.Report.weekEnd; import static com.markuspage.android.atimetracker.Report.weekStart; import static com.markuspage.android.atimetracker.TimeRange.NULL; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.TimerTask; import java.util.logging.Level; import java.util.logging.Logger; //import org.apache.commons.logging.LogFactory; import android.app.AlertDialog; import android.app.DatePickerDialog; import android.app.Dialog; import android.app.ListActivity; import android.app.ProgressDialog; //import android.app.Notification; import android.app.NotificationManager; import android.support.v4.app.NotificationCompat; import android.app.PendingIntent; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.media.MediaPlayer; import android.os.Bundle; import android.os.Handler; import android.os.Vibrator; import android.text.method.SingleLineTransformationMethod; import android.text.util.Linkify; import android.util.Log; import android.view.ContextMenu; import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.ContextMenu.ContextMenuInfo; import android.widget.BaseAdapter; import android.widget.DatePicker; import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; import android.widget.AdapterView.AdapterContextMenuInfo; /** * Manages and displays a list of tasks, providing the ability to edit and * display individual task items. * * @author ser */ public class Tasks extends ListActivity { // For logging private static final String TAG = "ATimeTracker.Tasks"; public static final String TIMETRACKERPREF = "timetracker.pref"; protected static final String FONTSIZE = "font-size"; protected static final String NOTIFICATION_MODE = "notification-mode"; protected static final String MILITARY = "military-time"; protected static final String CONCURRENT = "concurrent-tasks"; protected static final String SOUND = "sound-enabled"; protected static final String VIBRATE = "vibrate-enabled"; protected static final String START_DAY = "start_day"; protected static final String START_DATE = "start_date"; protected static final String END_DATE = "end_date"; protected static final String VIEW_MODE = "view_mode"; protected static final String REPORT_DATE = "report_date"; protected static final String TIMEDISPLAY = "time_display"; protected static final String ROUND_REPORT_TIMES = "round_report_times"; /** * Defines how each task's time is displayed */ private static final String FORMAT = "%02d:%02d"; private static final String DECIMAL_FORMAT = "%02d.%02d"; /** * How often to refresh the display, in milliseconds */ private static final int REFRESH_MS = 60000; /** * The model for this view */ private TaskAdapter adapter; /** * A timer for refreshing the display. */ private Handler timer; /** * The call-back that actually updates the display. */ private TimerTask updater; /** * Notification **/ private TaskNotificationThread notificationThread = null; /** * The currently active task (the one that is currently being timed). There * can be only one. */ private boolean running = false; /** * The currently selected task when the context menu is invoked. */ private Task selectedTask; private SharedPreferences preferences; private static int fontSize = 16; private boolean concurrency; private static MediaPlayer clickPlayer; private boolean playClick = false; private boolean vibrateClick = true; private Vibrator vibrateAgent; private ProgressDialog progressDialog = null; private boolean decimalFormat = false; /** * A list of menu options, including both context and options menu items */ protected static final int ADD_TASK = 0, EDIT_TASK = 1, DELETE_TASK = 2, REPORT = 3, SHOW_TIMES = 4, CHANGE_VIEW = 5, SELECT_START_DATE = 6, SELECT_END_DATE = 7, HELP = 8, EXPORT_VIEW = 9, SUCCESS_DIALOG = 10, ERROR_DIALOG = 11, SET_WEEK_START_DAY = 12, MORE = 13, BACKUP = 14, PREFERENCES = 15, PROGRESS_DIALOG = 16; // TODO: This could be done better... private static final String dbPath = "/data/data/com.markuspage.android.atimetracker/databases/timetracker.db"; private static final String dbBackup = "/sdcard/timetracker.db"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //android.os.Debug.waitForDebugger(); preferences = getSharedPreferences(TIMETRACKERPREF, MODE_PRIVATE); fontSize = preferences.getInt(FONTSIZE, 16); concurrency = preferences.getBoolean(CONCURRENT, false); if (preferences.getBoolean(MILITARY, true)) { TimeRange.FORMAT = new SimpleDateFormat("HH:mm"); } else { TimeRange.FORMAT = new SimpleDateFormat("hh:mm a"); } int which = preferences.getInt(VIEW_MODE, 0); if (adapter == null) { adapter = new TaskAdapter(this); setListAdapter(adapter); switchView(which); } if (timer == null) { timer = new Handler(); } if (updater == null) { updater = new TimerTask() { @Override public void run() { if (running) { adapter.notifyDataSetChanged(); setTitle(); Tasks.this.getListView().invalidate(); } timer.postDelayed(this, REFRESH_MS); } }; } playClick = preferences.getBoolean(SOUND, false); if (playClick && clickPlayer == null) { clickPlayer = MediaPlayer.create(this, R.raw.click); try { clickPlayer.prepareAsync(); } catch (IllegalStateException illegalStateException) { // ignore this. There's nothing the user can do about it. Logger.getLogger("TimeTracker").log(Level.SEVERE, "Failed to set up audio player: " + illegalStateException.getMessage()); } } decimalFormat = preferences.getBoolean(TIMEDISPLAY, false); registerForContextMenu(getListView()); if (adapter.tasks.isEmpty()) { showDialog(HELP); } vibrateAgent = (Vibrator) getSystemService(VIBRATOR_SERVICE); vibrateClick = preferences.getBoolean(VIBRATE, true); // Start the notification thread this.notificationThread = TaskNotificationThread.getInstance(); this.notificationThread.init(this, this.adapter); this.notificationThread.start(); } @Override protected void onPause() { super.onPause(); if (timer != null) { timer.removeCallbacks(updater); } } @Override protected void onStop() { if (adapter != null) { adapter.close(); } if (clickPlayer != null) { clickPlayer.release(); } super.onStop(); } @Override protected void onResume() { super.onResume(); // This is only to cause the view to reload, so that we catch // updates to the time list. int which = preferences.getInt(VIEW_MODE, 0); switchView(which); if (timer != null && running) { timer.post(updater); } } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); menu.add(0, ADD_TASK, 0, R.string.add_task_title).setIcon(android.R.drawable.ic_menu_add); menu.add(0, REPORT, 1, R.string.generate_report_title).setIcon(android.R.drawable.ic_menu_week); menu.add(0, MORE, 2, R.string.more).setIcon(android.R.drawable.ic_menu_more); return true; } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { menu.setHeaderTitle("Task menu"); menu.add(0, EDIT_TASK, 0, getText(R.string.edit_task)); menu.add(0, DELETE_TASK, 0, getText(R.string.delete_task)); menu.add(0, SHOW_TIMES, 0, getText(R.string.show_times)); } @Override public boolean onContextItemSelected(MenuItem item) { AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); selectedTask = (Task) adapter.getItem((int) info.id); switch (item.getItemId()) { case SHOW_TIMES: Intent intent = new Intent(this, TaskTimes.class); intent.putExtra(TASK_ID, selectedTask.getId()); if (adapter.currentRangeStart != -1) { intent.putExtra(START, adapter.currentRangeStart); intent.putExtra(END, adapter.currentRangeEnd); } startActivity(intent); break; default: showDialog(item.getItemId()); break; } return super.onContextItemSelected(item); } private AlertDialog operationSucceed; private AlertDialog operationFailed; private String exportMessage; private String baseTitle; private Object notificationBuilder; @Override public boolean onMenuItemSelected(int featureId, MenuItem item) { switch (item.getItemId()) { case ADD_TASK: case MORE: showDialog(item.getItemId()); break; case REPORT: Intent intent = new Intent(this, Report.class); intent.putExtra(REPORT_DATE, System.currentTimeMillis()); intent.putExtra(START_DAY, preferences.getInt(START_DAY, 0) + 1); intent.putExtra(TIMEDISPLAY, decimalFormat); intent.putExtra(ROUND_REPORT_TIMES, preferences.getInt(ROUND_REPORT_TIMES, 0)); startActivity(intent); break; default: // Ignore the other menu items; they're context menu break; } return super.onMenuItemSelected(featureId, item); } @Override protected Dialog onCreateDialog(int id) { switch (id) { case ADD_TASK: return openNewTaskDialog(); case EDIT_TASK: return openEditTaskDialog(); case DELETE_TASK: return openDeleteTaskDialog(); case CHANGE_VIEW: return openChangeViewDialog(); case HELP: return openAboutDialog(); case SUCCESS_DIALOG: operationSucceed = new AlertDialog.Builder(Tasks.this).setTitle(R.string.success) .setIcon(android.R.drawable.stat_notify_sdcard).setMessage(exportMessage) .setPositiveButton(android.R.string.ok, null).create(); return operationSucceed; case ERROR_DIALOG: operationFailed = new AlertDialog.Builder(Tasks.this).setTitle(R.string.failure) .setIcon(android.R.drawable.stat_notify_sdcard).setMessage(exportMessage) .setPositiveButton(android.R.string.ok, null).create(); return operationFailed; case PROGRESS_DIALOG: progressDialog = new ProgressDialog(this); progressDialog.setMessage("Copying records..."); progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); progressDialog.setCancelable(false); return progressDialog; case MORE: return new AlertDialog.Builder(Tasks.this) .setItems(R.array.moreMenu, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { DBBackup backup; System.err.println("IN CLICK"); switch (which) { case 0: // CHANGE_VIEW: showDialog(CHANGE_VIEW); break; case 1: // EXPORT_VIEW: String fname = export(); perform(fname, R.string.export_csv_success, R.string.export_csv_fail); break; case 2: // COPY DB TO SD showDialog(Tasks.PROGRESS_DIALOG); if (new File(dbBackup).exists()) { // Find the database SQLiteDatabase backupDb = SQLiteDatabase.openDatabase(dbBackup, null, SQLiteDatabase.OPEN_READWRITE); SQLiteDatabase appDb = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READONLY); backup = new DBBackup(Tasks.this, progressDialog); backup.execute(appDb, backupDb); } else { InputStream in = null; OutputStream out = null; try { in = new BufferedInputStream(new FileInputStream(dbPath)); out = new BufferedOutputStream(new FileOutputStream(dbBackup)); for (int c = in.read(); c != -1; c = in.read()) { out.write(c); } } catch (Exception ex) { Logger.getLogger(Tasks.class.getName()).log(Level.SEVERE, null, ex); exportMessage = ex.getLocalizedMessage(); showDialog(ERROR_DIALOG); } finally { try { if (in != null) { in.close(); } } catch (IOException ignored) { } try { if (out != null) { out.close(); } } catch (IOException ignored) { } } } break; case 3: // RESTORE FROM BACKUP showDialog(Tasks.PROGRESS_DIALOG); SQLiteDatabase backupDb = SQLiteDatabase.openDatabase(dbBackup, null, SQLiteDatabase.OPEN_READONLY); SQLiteDatabase appDb = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READWRITE); backup = new DBBackup(Tasks.this, progressDialog); backup.execute(backupDb, appDb); break; case 4: // PREFERENCES Intent intent = new Intent(Tasks.this, Settings.class); startActivityForResult(intent, PREFERENCES); break; case 5: // HELP: showDialog(HELP); break; default: break; } } }).create(); } return null; } protected void perform(String message, int success_string, int fail_string) { if (message != null) { exportMessage = getString(success_string, message); if (operationSucceed != null) { operationSucceed.setMessage(exportMessage); } showDialog(SUCCESS_DIALOG); } else { exportMessage = getString(fail_string, message); if (operationFailed != null) { operationFailed.setMessage(exportMessage); } showDialog(ERROR_DIALOG); } } /** * Creates a progressDialog to change the dates for which task times are * shown. Offers a short selection of pre-defined defaults, and the option * to choose a range from a progressDialog. * * @see arrays.xml * @return the progressDialog to be displayed */ private Dialog openChangeViewDialog() { return new AlertDialog.Builder(Tasks.this).setItems(R.array.views, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { SharedPreferences.Editor ed = preferences.edit(); ed.putInt(VIEW_MODE, which); ed.commit(); if (which == 5) { Calendar calInstance = Calendar.getInstance(); new DatePickerDialog(Tasks.this, new DatePickerDialog.OnDateSetListener() { public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) { Calendar start = Calendar.getInstance(); start.set(Calendar.YEAR, year); start.set(Calendar.MONTH, monthOfYear); start.set(Calendar.DAY_OF_MONTH, dayOfMonth); start.set(Calendar.HOUR, start.getMinimum(Calendar.HOUR)); start.set(Calendar.MINUTE, start.getMinimum(Calendar.MINUTE)); start.set(Calendar.SECOND, start.getMinimum(Calendar.SECOND)); start.set(Calendar.MILLISECOND, start.getMinimum(Calendar.MILLISECOND)); SharedPreferences.Editor ed = preferences.edit(); ed.putLong(START_DATE, start.getTime().getTime()); ed.commit(); new DatePickerDialog(Tasks.this, new DatePickerDialog.OnDateSetListener() { public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) { Calendar end = Calendar.getInstance(); end.set(Calendar.YEAR, year); end.set(Calendar.MONTH, monthOfYear); end.set(Calendar.DAY_OF_MONTH, dayOfMonth); end.set(Calendar.HOUR, end.getMaximum(Calendar.HOUR)); end.set(Calendar.MINUTE, end.getMaximum(Calendar.MINUTE)); end.set(Calendar.SECOND, end.getMaximum(Calendar.SECOND)); end.set(Calendar.MILLISECOND, end.getMaximum(Calendar.MILLISECOND)); SharedPreferences.Editor ed = preferences.edit(); ed.putLong(END_DATE, end.getTime().getTime()); ed.commit(); Tasks.this.switchView(5); // Update the list view } }, year, monthOfYear, dayOfMonth).show(); } }, calInstance.get(Calendar.YEAR), calInstance.get(Calendar.MONTH), calInstance.get(Calendar.DAY_OF_MONTH)).show(); } else { switchView(which); } } }).create(); } private void switchView(int which) { Calendar tw = Calendar.getInstance(); int startDay = preferences.getInt(START_DAY, 0) + 1; tw.setFirstDayOfWeek(startDay); String ttl = getString(R.string.title, getResources().getStringArray(R.array.views)[which]); switch (which) { case 0: // today adapter.loadTasks(tw); break; case 1: // this week adapter.loadTasks(weekStart(tw, startDay), weekEnd(tw, startDay)); break; case 2: // yesterday tw.add(Calendar.DAY_OF_MONTH, -1); adapter.loadTasks(tw); break; case 3: // last week tw.add(Calendar.WEEK_OF_YEAR, -1); adapter.loadTasks(weekStart(tw, startDay), weekEnd(tw, startDay)); break; case 4: // all adapter.loadTasks(); break; case 5: // select range Calendar start = Calendar.getInstance(); start.setTimeInMillis(preferences.getLong(START_DATE, 0)); System.err.println("START = " + start.getTime()); Calendar end = Calendar.getInstance(); end.setTimeInMillis(preferences.getLong(END_DATE, 0)); System.err.println("END = " + end.getTime()); adapter.loadTasks(start, end); DateFormat f = DateFormat.getDateInstance(DateFormat.SHORT); ttl = getString(R.string.title, f.format(start.getTime()) + " - " + f.format(end.getTime())); break; default: // Unknown break; } baseTitle = ttl; setTitle(); getListView().invalidate(); } private void setTitle() { long total = 0; for (Task t : adapter.tasks) { total += t.getTotal(); } setTitle(baseTitle + " " + formatTotal(decimalFormat, total, 0)); } /** * Constructs a progressDialog for defining a new task. If accepted, creates * a new task. If cancelled, closes the progressDialog with no affect. * * @return the progressDialog to display */ private Dialog openNewTaskDialog() { LayoutInflater factory = LayoutInflater.from(this); final View textEntryView = factory.inflate(R.layout.edit_task, null); return new AlertDialog.Builder(Tasks.this) //.setIcon(R.drawable.alert_dialog_icon) .setTitle(R.string.add_task_title).setView(textEntryView) .setPositiveButton(R.string.add_task_ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { EditText textView = (EditText) textEntryView.findViewById(R.id.task_edit_name_edit); String name = textView.getText().toString(); adapter.addTask(name); Tasks.this.getListView().invalidate(); } }).setNegativeButton(android.R.string.cancel, null).create(); } /** * Constructs a progressDialog for editing task attributes. If accepted, * alters the task being edited. If cancelled, dismissed the progressDialog * with no effect. * * @return the progressDialog to display */ private Dialog openEditTaskDialog() { if (selectedTask == null) { return null; } LayoutInflater factory = LayoutInflater.from(this); final View textEntryView = factory.inflate(R.layout.edit_task, null); return new AlertDialog.Builder(Tasks.this).setView(textEntryView) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { EditText textView = (EditText) textEntryView.findViewById(R.id.task_edit_name_edit); String name = textView.getText().toString(); selectedTask.setTaskName(name); adapter.updateTask(selectedTask); Tasks.this.getListView().invalidate(); } }).setNegativeButton(android.R.string.cancel, null).create(); } /** * Constructs a progressDialog asking for confirmation for a delete request. * If accepted, deletes the task. If cancelled, closes the progressDialog. * * @return the progressDialog to display */ private Dialog openDeleteTaskDialog() { if (selectedTask == null) { return null; } String formattedMessage = getString(R.string.delete_task_message, selectedTask.getTaskName()); return new AlertDialog.Builder(Tasks.this).setTitle(R.string.delete_task_title) .setIcon(android.R.drawable.stat_sys_warning).setCancelable(true).setMessage(formattedMessage) .setPositiveButton(R.string.delete_ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { adapter.deleteTask(selectedTask); Tasks.this.getListView().invalidate(); } }).setNegativeButton(android.R.string.cancel, null).create(); } final static String SDCARD = "/sdcard/"; private String export() { // Export, then show a progressDialog String rangeName = getRangeName(); String fname = rangeName + ".csv"; File fout = new File(SDCARD + fname); // Change the file name until there's no conflict int counter = 0; while (fout.exists()) { fname = rangeName + "_" + counter + ".csv"; fout = new File(SDCARD + fname); counter++; } try { OutputStream out = new FileOutputStream(fout); Cursor currentRange = adapter.getCurrentRange(); CSVExporter.exportRows(out, currentRange); currentRange.close(); return fname; } catch (FileNotFoundException fnfe) { fnfe.printStackTrace(System.err); return null; } } private String getRangeName() { if (adapter.currentRangeStart == -1) { return "all"; } SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd"); Date d = new Date(); d.setTime(adapter.currentRangeStart); return f.format(d); } private Dialog openAboutDialog() { String versionName = ""; try { PackageInfo pkginfo = this.getPackageManager().getPackageInfo("com.markuspage.android.atimetracker", 0); versionName = pkginfo.versionName; } catch (NameNotFoundException nnfe) { // Denada } String formattedVersion = getString(R.string.version, versionName); LayoutInflater factory = LayoutInflater.from(this); View about = factory.inflate(R.layout.about, null); TextView version = (TextView) about.findViewById(R.id.version); version.setText(formattedVersion); TextView links = (TextView) about.findViewById(R.id.usage); Linkify.addLinks(links, Linkify.ALL); links = (TextView) about.findViewById(R.id.credits); Linkify.addLinks(links, Linkify.ALL); return new AlertDialog.Builder(Tasks.this).setView(about).setPositiveButton(android.R.string.ok, null) .create(); } @Override protected void onPrepareDialog(int id, Dialog d) { EditText textView; switch (id) { case ADD_TASK: textView = (EditText) d.findViewById(R.id.task_edit_name_edit); textView.setText(""); break; case EDIT_TASK: textView = (EditText) d.findViewById(R.id.task_edit_name_edit); textView.setText(selectedTask.getTaskName()); break; default: break; } } /** * The view for an individial task in the list. */ private class TaskView extends LinearLayout { /** * The view of the task name displayed in the list */ private TextView taskName; /** * The view of the total time of the task. */ private TextView total; private ImageView checkMark; public TaskView(Context context, Task t) { super(context); setOrientation(LinearLayout.HORIZONTAL); setPadding(5, 10, 5, 10); taskName = new TextView(context); taskName.setTextSize(fontSize); taskName.setText(t.getTaskName()); addView(taskName, new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.FILL_PARENT, 1f)); checkMark = new ImageView(context); checkMark.setImageResource(R.drawable.ic_check_mark_dark); checkMark.setVisibility(View.INVISIBLE); addView(checkMark, new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.FILL_PARENT, 0f)); total = new TextView(context); total.setTextSize(fontSize); total.setGravity(Gravity.RIGHT); total.setTransformationMethod(SingleLineTransformationMethod.getInstance()); total.setText(formatTotal(decimalFormat, t.getTotal(), 0)); addView(total, new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.FILL_PARENT, 0f)); setGravity(Gravity.TOP); markupSelectedTask(t); } public void setTask(Task t) { taskName.setTextSize(fontSize); total.setTextSize(fontSize); taskName.setText(t.getTaskName()); total.setText(formatTotal(decimalFormat, t.getTotal(), 0)); markupSelectedTask(t); } private void markupSelectedTask(Task t) { if (t.isRunning()) { checkMark.setVisibility(View.VISIBLE); } else { checkMark.setVisibility(View.INVISIBLE); } } } private static final long MS_H = 3600000; private static final long MS_M = 60000; private static final long MS_S = 1000; private static final double D_M = 10.0 / 6.0; private static final double D_S = 1.0 / 36.0; /* * This is pretty stupid, but because Java doesn't support closures, we have * to add extra overhead (more method indirection; method calls are relatively * expensive) if we want to re-use code. Notice that a call to this method * actually filters down through four methods before it returns. */ static String formatTotal(boolean decimalFormat, long ttl, long roundMinutes) { return formatTotal(decimalFormat, FORMAT, ttl, roundMinutes); } static String formatTotal(boolean decimalFormat, String format, long ttl, long roundMinutes) { if (roundMinutes > 0) { long totalMinutes = ttl / MS_M; ttl = roundMinutes * Math.round((float) totalMinutes / roundMinutes) * MS_M; } long hours = ttl / MS_H; long hours_in_ms = hours * MS_H; long minutes = (ttl - hours_in_ms) / MS_M; long minutes_in_ms = minutes * MS_M; long seconds = (ttl - hours_in_ms - minutes_in_ms) / MS_S; return formatTotal(decimalFormat, format, hours, minutes, seconds, roundMinutes); } static String formatTotal(boolean decimalFormat, long hours, long minutes, long seconds, long roundMinutes) { return formatTotal(decimalFormat, FORMAT, hours, minutes, seconds, roundMinutes); } static String formatTotal(boolean decimalFormat, String format, long hours, long minutes, long seconds, long roundMinutes) { if (decimalFormat) { format = DECIMAL_FORMAT; minutes = Math.round((D_M * minutes) + (D_S * seconds)); seconds = 0; } return String.format(format, hours, minutes, seconds); } class TaskAdapter extends BaseAdapter { private DBHelper dbHelper; protected ArrayList<Task> tasks; private Context savedContext; private long currentRangeStart; private long currentRangeEnd; public TaskAdapter(Context c) { savedContext = c; dbHelper = new DBHelper(c); dbHelper.getWritableDatabase(); tasks = new ArrayList<Task>(); } public void close() { dbHelper.close(); } /** * Loads all tasks. */ private void loadTasks() { currentRangeStart = currentRangeEnd = -1; loadTasks("", true); } protected void loadTasks(Calendar day) { loadTasks(day, (Calendar) day.clone()); } protected void loadTasks(Calendar start, Calendar end) { String[] res = makeWhereClause(start, end); loadTasks(res[0], res[1] == null ? false : true); } /** * Java doesn't understand tuples, so the return value of this is a * hack. * * @param start * @param end * @return a String pair hack, where the second item is null for false, * and non-null for true */ private String[] makeWhereClause(Calendar start, Calendar end) { String query = "AND " + START + " < %d AND " + START + " >= %d"; Calendar today = Calendar.getInstance(); today.setFirstDayOfWeek(preferences.getInt(START_DAY, 0) + 1); today.set(Calendar.HOUR_OF_DAY, 12); for (int field : new int[] { Calendar.HOUR_OF_DAY, Calendar.MINUTE, Calendar.SECOND, Calendar.MILLISECOND }) { for (Calendar d : new Calendar[] { today, start, end }) { d.set(field, d.getMinimum(field)); } } end.add(Calendar.DAY_OF_MONTH, 1); currentRangeStart = start.getTimeInMillis(); currentRangeEnd = end.getTimeInMillis(); boolean loadCurrentTask = today.compareTo(start) != -1 && today.compareTo(end) != 1; query = String.format(query, end.getTimeInMillis(), start.getTimeInMillis()); return new String[] { query, loadCurrentTask ? query : null }; } /** * Load tasks, given a filter. This overwrites any currently loaded * tasks in the "tasks" data structure. * * @param whereClause A SQL where clause limiting the range of dates to * load. This must be a clause against the ranges table. * @param loadCurrent Whether or not to include data for currently * active tasks. */ private void loadTasks(String whereClause, boolean loadCurrent) { tasks.clear(); SQLiteDatabase db = dbHelper.getReadableDatabase(); Cursor c = db.query(TASK_TABLE, TASK_COLUMNS, null, null, null, null, null); Task t; if (c.moveToFirst()) { do { int tid = c.getInt(0); String[] tids = { String.valueOf(tid) }; t = new Task(c.getString(1), tid); Cursor r = db.rawQuery("SELECT SUM(end) - SUM(start) AS total FROM " + RANGES_TABLE + " WHERE " + TASK_ID + " = ? AND end NOTNULL " + whereClause, tids); if (r.moveToFirst()) { t.setCollapsed(r.getLong(0)); } r.close(); if (loadCurrent) { r = db.query(RANGES_TABLE, RANGE_COLUMNS, TASK_ID + " = ? AND end ISNULL", tids, null, null, null); if (r.moveToFirst()) { t.setStartTime(r.getLong(0)); } r.close(); } tasks.add(t); } while (c.moveToNext()); } c.close(); Collections.sort(tasks); running = findCurrentlyActive().hasNext(); notifyDataSetChanged(); } /** * Don't forget to close the cursor!! * * @return */ protected Cursor getCurrentRange() { String[] res = { "" }; if (currentRangeStart != -1 && currentRangeEnd != -1) { Calendar start = Calendar.getInstance(); start.setFirstDayOfWeek(preferences.getInt(START_DAY, 0) + 1); start.setTimeInMillis(currentRangeStart); Calendar end = Calendar.getInstance(); end.setFirstDayOfWeek(preferences.getInt(START_DAY, 0) + 1); end.setTimeInMillis(currentRangeEnd); res = makeWhereClause(start, end); } SQLiteDatabase db = dbHelper.getReadableDatabase(); Cursor r = db.rawQuery( "SELECT t.name, r.start, r.end " + " FROM " + TASK_TABLE + " t, " + RANGES_TABLE + " r " + " WHERE r." + TASK_ID + " = t.ROWID " + res[0] + " ORDER BY t.name, r.start ASC", null); return r; } public Iterator<Task> findCurrentlyActive() { return new Iterator<Task>() { Iterator<Task> iter = tasks.iterator(); Task next = null; public boolean hasNext() { if (next != null) { return true; } while (iter.hasNext()) { Task t = iter.next(); if (t.isRunning()) { next = t; return true; } } return false; } public Task next() { if (hasNext()) { Task t = next; next = null; return t; } throw new NoSuchElementException(); } public void remove() { throw new UnsupportedOperationException(); } }; } protected void addTask(String taskName) { SQLiteDatabase db = dbHelper.getWritableDatabase(); ContentValues values = new ContentValues(); values.put(NAME, taskName); long id = db.insert(TASK_TABLE, NAME, values); Task t = new Task(taskName, (int) id); tasks.add(t); Collections.sort(tasks); notifyDataSetChanged(); } protected void updateTask(Task t) { SQLiteDatabase db = dbHelper.getWritableDatabase(); ContentValues values = new ContentValues(); values.put(NAME, t.getTaskName()); String id = String.valueOf(t.getId()); String[] vals = { id }; db.update(TASK_TABLE, values, "ROWID = ?", vals); if (t.getStartTime() != NULL) { values.clear(); long startTime = t.getStartTime(); values.put(START, startTime); vals = new String[] { id, String.valueOf(startTime) }; if (t.getEndTime() != NULL) { values.put(END, t.getEndTime()); } // If an update fails, then this is an insert if (db.update(RANGES_TABLE, values, TASK_ID + " = ? AND " + START + " = ?", vals) == 0) { values.put(TASK_ID, t.getId()); db.insert(RANGES_TABLE, END, values); } } Collections.sort(tasks); notifyDataSetChanged(); } public void deleteTask(Task t) { tasks.remove(t); SQLiteDatabase db = dbHelper.getWritableDatabase(); String[] id = { String.valueOf(t.getId()) }; db.delete(TASK_TABLE, "ROWID = ?", id); db.delete(RANGES_TABLE, TASK_ID + " = ?", id); notifyDataSetChanged(); } public int getCount() { return tasks.size(); } public Object getItem(int position) { return tasks.get(position); } public long getItemId(int position) { return position; } public View getView(int position, View convertView, ViewGroup parent) { TaskView view = null; if (convertView == null) { Object item = getItem(position); if (item != null) { view = new TaskView(savedContext, (Task) item); } } else { view = (TaskView) convertView; Object item = getItem(position); if (item != null) { view.setTask((Task) item); } } return view; } } @Override protected void onListItemClick(ListView l, View v, int position, long id) { if (vibrateClick) { vibrateAgent.vibrate(100); } if (playClick) { try { //clickPlayer.prepare(); clickPlayer.start(); } catch (Exception exception) { // Ignore this; it is probably because the media isn't yet ready. // There's nothing the user can do about it. // ignore this. There's nothing the user can do about it. Logger.getLogger("TimeTracker").log(Level.INFO, "Failed to play audio: " + exception.getMessage()); } } // Display the notification Object item = getListView().getItemAtPosition(position); // Stop the update. If a task is already running and we're stopping // the timer, it'll stay stopped. If a task is already running and // we're switching to a new task, or if nothing is running and we're // starting a new timer, then it'll be restarted. if (item != null) { Task selected = (Task) item; if (!concurrency) { boolean startSelected = !selected.isRunning(); if (running) { running = false; timer.removeCallbacks(updater); // Disable currently running tasks for (Iterator<Task> iter = adapter.findCurrentlyActive(); iter.hasNext();) { Task t = iter.next(); t.stop(); adapter.updateTask(t); } } if (startSelected) { selected.start(); running = true; timer.post(updater); } } else { if (selected.isRunning()) { selected.stop(); running = adapter.findCurrentlyActive().hasNext(); if (!running) { timer.removeCallbacks(updater); } } else { selected.start(); if (!running) { running = true; timer.post(updater); } } } adapter.updateTask(selected); } getListView().invalidate(); super.onListItemClick(l, v, position, id); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == PREFERENCES && data != null) { Bundle extras = data.getExtras(); if (extras.getBoolean(START_DAY)) { switchView(preferences.getInt(VIEW_MODE, 0)); } if (extras.getBoolean(MILITARY)) { if (preferences.getBoolean(MILITARY, true)) { TimeRange.FORMAT = new SimpleDateFormat("HH:mm"); } else { TimeRange.FORMAT = new SimpleDateFormat("hh:mm a"); } } if (extras.getBoolean(CONCURRENT)) { concurrency = preferences.getBoolean(CONCURRENT, false); } if (extras.getBoolean(SOUND)) { playClick = preferences.getBoolean(SOUND, false); if (playClick && clickPlayer == null) { clickPlayer = MediaPlayer.create(this, R.raw.click); try { clickPlayer.prepareAsync(); clickPlayer.setVolume(1, 1); } catch (IllegalStateException illegalStateException) { // ignore this. There's nothing the user can do about it. Logger.getLogger("TimeTracker").log(Level.SEVERE, "Failed to set up audio player: " + illegalStateException.getMessage()); } } } if (extras.getBoolean(VIBRATE)) { vibrateClick = preferences.getBoolean(VIBRATE, true); } if (extras.getBoolean(FONTSIZE)) { fontSize = preferences.getInt(FONTSIZE, 16); } if (extras.getBoolean(TIMEDISPLAY)) { decimalFormat = preferences.getBoolean(TIMEDISPLAY, false); } } if (getListView() != null) { getListView().invalidate(); } } protected void finishedCopy(DBBackup.Result result, String message) { if (result == DBBackup.Result.SUCCESS) { switchView(preferences.getInt(VIEW_MODE, 0)); message = dbBackup; } perform(message, R.string.restore_success, R.string.restore_failed); } }