Как стать автором
Обновить

Простой Тайм Менеджер для Android. Часть 2

Время на прочтение12 мин
Количество просмотров9.7K
В этой части мы будем доделывать приложение «Менеджер Времени» из первой части.

Сразу выражаю огромную благодарность Belkin и всем тем, кто плюсанул первую часть, вы помогли мне с инвайтом)

Хочу обратить ваше внимание на то, что я отказался от использование сервиса, как изначально задумывалось. Сами посудите, запускать процесс только для того, чтоб каждую секунду увеличивать таймер — глупость. Решение простое: перед каждой остановкой программы, сохраняем время, а после запуска вычитаем его из текущего времени, получая число секунд.

План действий:
  • Багфиксы предыдущей версии.
  • Модификация кода, для постоянной работы приложения, а не только для запущенного Activity
  • Уведомления
  • Закинем все это дело на маркет


Сразу хочу извиниться и похвалиться) Извиняюсь за то что заставил вас долго ждать, а похвалиться тем, что на днях Android принес мне первые ну ооооочень хорошие деньги и я на радостях купил себе xbox и проиграл в него все выходные, поэтому и задержался)

Итак, поехали!


Сначала либо читаем первую часть, и
чтоб самим было понятно все пишем под мое повествование в ней, либо,
если все понятно, то качаем исходники первой части, чтоб было над чем
работать.
Исходники (134 Кб)

Открыли, запустили, на всякий пожарный собрали и запустили.

[2010-03-12 09:12:15 - TimeManager]Android Launch!
[2010-03-12 09:12:15 - TimeManager]adb is running normally.
[2010-03-12 09:12:15 - TimeManager]Performing
com.nixan.timemanager.Main activity launch
[2010-03-12 09:12:15 - TimeManager]Automatic Target Mode: using device
'HT91MKV01100'
[2010-03-12 09:12:15 - TimeManager]Uploading TimeManager.apk onto
device 'HT91MKV01100'
[2010-03-12 09:12:15 - TimeManager]Installing TimeManager.apk...
[2010-03-12 09:12:19 - TimeManager]Re-installation failed due to
different application signatures.
[2010-03-12 09:12:19 - TimeManager]You must perform a full uninstall
of the application. WARNING: This will remove the application data!
[2010-03-12 09:12:19 - TimeManager]Please execute 'adb uninstall
com.nixan.timemanager' in a shell.
[2010-03-12 09:12:19 - TimeManager]Launch canceled!

В моем случае Eclipse ругнулся на то, что не может установить апплет,
в связи с тем, что он уже установлен.
Небольшое пояснение: собирая апплет в Eclipse с последующей
установкой, приложение подписывается debug ключами, а при установке из
маркета — нормальными. Так вот эклипс не имеет возможности удалить
приложение установленное через маркет, поэтому приходится удалять его
в ручную. Тут все уж совсем просто — идем в маркет и удаляем), либо
поступаем так, как нам сказал Eclipse — в терминале делаем adb
uninstall <имя пакета>.

Еще одно отступление: вообще adb очень полезная штуковина. К
примеру можно даже базой данных рулить через него, через него же
снимаются скриншоты, и даже скринкаст, который я в первой части на
youtube выложил.


Итак, удалили, еще раз собрали и установили. Все должно работать, если
нет, ищите ошибки, либо качайте мои исходники.

Багфиксы и улучшения


Денек попользовавшись тулзой, я понял, что поворот экрана, хоть и не
выключает счетчик, но сбрасывает его на момент до запуска и что
программе не хватает сброса счетчиков, плюс об этом же баге сообщил Riot. Чтож поехали, правим.

Багфикс
Для справки открываем activity lifecycle. При повороте экрана, апплет
убивает activity и затем рестартует его. В прошлый раз мы в onCreate()
добавляли подгрузку значений запущен ли таймер и сколько секунд на них
натикало. Дело за малым — нужно лишь при убивании activity сохранять
значения таймеров.
@Override
   public void onStop()
   {
   	super.onStop();
		stats_editor.putInt("key_rest_time", rest_time);
		stats_editor.putInt("key_work_time", work_time);
		stats_editor.commit();
   	
   }

super.onStop(); — строка вызывает метод onStop() родителя. Если
вам это ничего не говорит, то хорошо бы вам почитать что-нибудь про
ООП в java.
stats_editor.putInt(); — в прошлый раз я уже говорил об этом,
но повторюсь, эти методы кладут в файл настроек приложения, которые я
использую, чтоб хранить данные, наши переменные с ключом.
stats_editor.commit(); — метод сохраняет переменные, которые мы
ранее записали с помощью putInt, putBoolean, putString… и т.д.
Важно помнить, что по-хорошему одновременно открытыми для
записи файл настройки может быть только один раз, иначе имея две
переменные с настройками, после commit() мы рискуем получить потом на
выходе то, что записала именно последняя переменная, у которой
вызывался метод commit();

Разжую немного:
Допустим в настройках уже лежат два integer'а integer_one и
integer_two, оба равны 1.
SharedPreferences.Editor stats_editor1 =
PreferenceManager.getDefaultSharedPreferences(this).edit();
SharedPreferences.Editor stats_editor2 =
PreferenceManager.getDefaultSharedPreferences(this).edit();
stats_editor1.putInt("integer_one", 2);
stats_editor2.putInt("integer_two", 2);
stats_editor1.commit();
stats_editor2.commit();

Мы рискуем получить на выходе integer_one == 1 и integer_two == 2.

Собираем, запускаем, у меня все заработало.

Добавляем возможность сброса таймеров.
Надо создать меню, вызываемое по нажатию кнопки <> на девайсе, и
там должна быть кнопка, которая будет сбрасывать таймеры.
Для начала, кнопку надо обозвать. Вспомните, как мы в прошлый раз
редактировали файл res/values/strings.xml, и допишите туда и, если
делали локализацию, в файл с локализацией.
<string name="button_reset">Reset timers</string>

У меня строка будет называться button_reset.
Отступление: вот сейчас вы были свидетелями доказательства того,
что локализацию удобнее всего делать в сааааамом конце. Просто
прикиньте, что у вас не 5 строк, а 200 с лишним.


Затем в главном, и, пока единственном классе, надо добавить два метода:
public boolean onCreateOptionsMenu(Menu menu)

и
public boolean onOptionsItemSelected(MenuItem item)

Первый — генерирует меню, второй обрабатывает нажатия на его элементы.

Затем пишем код для генерации меню:
MenuItem menuItem;
menuItem = menu.add(Menu.NONE, Menu.NONE, Menu.NONE, R.string.button_reset);
menuItem.setIcon(android.R.drawable.ic_menu_delete);
return super.onCreateOptionsMenu(menu);

menuItem — переменная содержит саму кнопочку в меню. Первой
строкой мы ее лишь обозначаем.
menu.add — во-первых она берется из вызова метода — посмотрите,
она там как входной параметр обозначена, во-вторых этот метод
возвращает тип MenuItem, который мы и пихнем в переменную menuItem.
В параметрах передаются следующие значения:
1. Группа кнопок. У меня Menu.NONE, тк не использую.
2. Идентификатор элемента. Вообще по-хорошему надо ставить везде
разные, легче всего этот участок кода поставить в цикл и передавать
туда счетчик цикла. Нужен для того, чтоб понять в обработчике нажатий,
на какую кнопку именно мы нажали.
3. Порядок следования.
4. Имя кнопки. Ссылается на button_reset в res/values/strings.xml.
setIcon — выставляет иконку для элемента меню.
Обратите внимания, что я не заливал ic_menu_delete.png в папку
drawable, и на то что обращаемся не как обычно — R.drawable, а
android.R.drawable. Это значит, что система будет искать эту картинку
в картинках самой системы, а не конкретно нашего приложения.
Как всегда отступление. Зачем использовать картинки из самой
системы? Во-первых нет необходимости рисовать или выдерать то, что уже
давно нарисовано. Во-вторых это самый идеальный вариант сделать
систему по гайдлайнам. В-третьих, к примеру взгляните на мой
РобоЖурнал — клиент для жж (доступен в маркете, опять же поиск по
фразе pub:«Nixan»). В 1.5 и 1.6 анимация в всплывающем окне была одна,
а в 2.0 ее обновили. Как итог на разных системах мы имеем разные
картинки.


Давайте соберем это дело, и посмотрим, что получилось.

Вот мой вариант.

Осталось для полной радости лишь написать обработчик нажатия на меню.

if (resting)
{
	rest_timer.startAnimation(shrink_rest);
	resting = false;
}
if (working)
{
	work_timer.startAnimation(shrink_work);
	working = false;
}
rest_timer.setText("0:00:00");
work_timer.setText("0:00:00");
rest_time = 0;
work_time = 0;
stats_editor.putInt("key_rest_time", rest_time);
stats_editor.putInt("key_work_time", work_time);
stats_editor.commit();
return super.onOptionsItemSelected(item);

1. Мы останавливаем счетчики.
2. Обнуляем текст на таймерах.
3. Обнуляем сами счетчики.
4. Обнуляем настройки, где они содержатся.

Небольшая оптимизация
Каюсь, но у меня из головы прямо вылетело то, что строку с счетчиком можно задать через String.format();, что я и сделал.
Для этого мы идем в TimerTask, удаляем из списка переменных все String'и, удаляем участки кода где перед секундами и минутами добавляются нули, а
rest_timer.setText(hours_ind_r+":"+minutes_ind_r+":"+seconds_ind_r);

меняем на
rest_timer.setText(String.format("%d:%02d:%02d", hours_r, minutes_r, seconds_r));


Собираем, пробуем. Все работает!

Убираем необходимость держать приложение запущенным.


Как я уже ранее говорил, городить сервис в данном случае — не целесообразно. Не спорю, очень хотелось написать что-нибудь про это, но то что я его не стал делать тоже в какой-то степени howto). К тому же, на хабре есть чудесная статья «Спокойной ночи» 3fonov'а, в которой отлично рассказано про сервисы, а именно про то, что их существует два типа:
1. Создал, оно отработало, остановилось.
2. Подключился, выполнил код под командованием подключающегося activity, остановился.
3fonov подробно описал вторую часть, там есть очень тонкие моменты с интерфейсом и файлом aidl, на котором я в свое время потратил день рабочего времени. В первом же типе все достаточно просто. Возьмите за основу, к примеру, наш класс, а именно его методы onCreate() и onDestroy. Вся работа в таком сервисе протекает именно в них.
Немножко отсебятины. А где мне какой тип сервиса использовать?
Второй тип сервиса очень удобен для приложений, которые подразумевают под собой достаточно активное общение пользователя с сервером, к примеру тот же Google Talk, сервис постоянно висит в памяти. По вызову команды onBind, Activity подключается к этому сервису и спрашивает, допустим, кто онлайн, на что сервис отвечает данными загруженными в момент последнего обновления. В этой же команде наверняка есть методы, позволяющие отсылать сообщения, выставлять статусы. Приемущество такого, что на фоновую работу и на пользователя мы имеем один сокет, одно подключение, реализованное как раз в этом сервисе.
Первый тип, на мой взгляд, гораздо удобней для приложений, где пользователь гораздо меньше воздействует на сервер, я имею ввиду, допустим, функции поиска контента. В данный момент я пишу приложение, у которого сервис имеет как раз TimerTask для обновлений контента, при обновлении, он сохраняет всю информацию в локальной базе, и посылает широковещательное сообщение (в народе Broadcast) об обновлении интерфейса. При этом, если Activity запущено, то оно это сообщение подцепит, и заново прочитает базу данных, выводя все новые элементы, иначе же Broadcast просто ничего не сделает. Плюс этого типа сервиса, в том что по моим наблюдениям, время на его разработку и реализацию, ну… думаю раза в 3 меньше, чем сервисов в возможностью подключения к ним Activity.


Итак, после отступления, продолжим наш реалкодинг)
Для того, чтоб приложение продолжало считать время, даже когда оно не активно, я придумал такой алгоритм.
Когда приложение закрывается, оно в настройки сохраняет текущее время, два таймера работы и отдыха и два булевых значения запущены ли эти таймеры. Затем при запуске, мы считываем эти параметры, а значение текущего времени из настроек мы вычитаем из действительного текущего времени (Правильней было назвать этот параметр дата последнего запуска, или что-то в этом духе).

Делается все это в несколько строчек кода.
Добавляем эту строку в метод onStop() (напомню, метод вызываемый, когда activity убивается и еще раз ссылаюсь на Activity lifecycle) перед методом, который сохраняет настройки.
stats_editor.putLong("key_last_used_date", System.currentTimeMillis());

Освежу в памяти. stats_editor — это переменная класса SharedPreferences.Editor, который сохраняет настройки приложения, но мы используем его как хранение данных. Существует еще класс SharedPreferences, но он только для чтения этих настроек. Методу putLong сообщаются две переменные String — ключ настроек и long — значение. Есть еще методы putString, putInt и так далее, у них, соответственно второй параметр будет String, int… Читаются они методом getString, getInt и т.д., тоже с двумя параметрами: ключ и значение по-умолчанию, к примеру если настройки такой система не найдет, то она вернет дефолтное значение.
System.currentTimeMillis() — возвращает long значение текущего времени в миллисекундах.

Ко всем переменным, что мы создавали в первой части добавляем переменную int last_time; для получения разницы между текущим временем и временем последнего запуска (а точнее остановки) приложения.

Затем идем в метод onCreate() и добавляем туда:
last_time = (int) ((System.currentTimeMillis() - saved_stats.getLong("key_last_used_date", System.currentTimeMillis())) / 1000);

Внимание! Имхо стоит упомянуть, хотя я надеюсь все понимают, что строку эту добавлять надо после того, как мы инициализировали переменную saved_stats, иначе мы поймаем NullPointerException.
Разжевываю: Текущее время — время последнего запуска (точнее остановки) и все это разделить на 1000, чтоб получить из миллисекунд секунды, у всей этой конструкции возвращаемый тип int.
Деление? Это же целочисленная переменная! — скажете вы, в яве, при делении целочисленного на целочисленное вернется целая часть от деления.

Осталось добавить эти секунды к активному счетчику. Ищем в том же onCreate() конструкцию, которая в зависимости от булевых переменных resting и working увеличивает размер текста счетчиков и дописываем туда это увеличение.
if (resting)
{
       	rest_timer.startAnimation(magnify_rest);
       	rest_time += last_time;
}
if (working)
{
       	work_timer.startAnimation(magnify_work);
       	work_time += last_time;
}


Запускаем, пробуем, у меня все работает, значит должно и у вас.

Уведомления


Алгоритм такой: если счетчик включен, то выводим уведомление в момент остановки апплета, и посылаем команду скрыть уведомления в любом случае при старте программы.

Объявляем и инициализируем переменную класса NotificationManager, которая как раз и управляет уведомлениями.
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);


У этой переменной нам нужны будут два метода cancel() и notify(). Первый, как вы уже, наверное, догадались — убирает уведомление. Ему передается один параметр — id, на случай, если у приложения несколько уведомлений, чтоб убрать какое-то конеретное, мы используем этот идентификатор. У notify() два параметра — id и переменная класса Notification.
Так как мое приложение имеет только одно уведомление, то смело вместо id ставим 0 или любое другое число. Важно, чтоб у созданного и отмененного они были одинаковые.

После инициализации переменной notificationManager можно сразу воспользоваться методом cancel(), чтоб при запуске приложения уведомление убивалось.
notificationManager.cancel(0);


Теперь идем в onStop() и добавляем туда:
Notification notification = null;
PendingIntent intent = PendingIntent.getActivity(this, 0, new Intent(this, Main.class), PendingIntent.FLAG_UPDATE_CURRENT);
if (working)
{
	notification = new Notification(R.drawable.notification, getResources().getString(R.string.notification_work), System.currentTimeMillis());
	notification.setLatestEventInfo(this, getResources().getString(R.string.notification_work), getResources().getString(R.string.show_app), intent);
}
if (resting)
{
	notification = new Notification(R.drawable.notification, getResources().getString(R.string.notification_rest), System.currentTimeMillis());
	notification.setLatestEventInfo(this, getResources().getString(R.string.notification_rest), getResources().getString(R.string.show_app), intent);
}
if (notification != null)
{
	notification.flags = Notification.FLAG_ONGOING_EVENT | Notification.FLAG_AUTO_CANCEL;
	notificationManager.notify(0, notification);
}

1. Создаем переменную класса Notification.
2. Описываем ее — какие строки и изображение использовать, а так же что надо запустить при нажатии на это уведомление.
3. Делаем notify();
intent — переменна класса PendingIntent — как раз тут и указывается, какой класс надо запустить при нажатии. Так же обратите внимание на последний параметр, там передаются флаги для запущенного intent'а, у меня выставлен флаг, чтоб новый intent обновлял старый.
Конструктор notification'а картинка, текст, который будет показан при создании уведомления, когда оно во всю ширину notification bar'а, время уведомления.
setLatestEventInfo — две строки: первая — то что выводится крупным шрифтом, вторая — подпись мелким шрифтом и Intent для запуска.
notification.flags — как известно, у android'а два типа уведомлений. Яркий пример этого — календарь и монтирование флешки. Одно — предстоящее, другое текущее. Notification.FLAG_ONGOING_EVENT — выставляет уведомлению статус текущего.
Кстати говоря, можно использовать еще один флаг — Notification.FLAG_AUTO_CANCEL, и убрать из метода onCreate() строку, убирающую уведомление. А вообще читайте пояснения, там очень много чего интересного)
notify — собственно применяем это уведомление.

Вот как это все выглядит.


Собираем апплет, пробуем запустить. Все чудесно должно получиться, но на всякий пожарный делюсь исходниками.
Архив zip (158 Кб)

Картинка, которую я использовал в уведомлениях лежит в обновленном архиве со всеми картинками.
Архив zip (295 Кб)

Заливаем на маркет


Предположим, что вы прошли процедуру регистрации разработчика, заплатили пошлину в 25$ и вот пришло время выложить проект. Чтож, поехали!

Первым делом приложению надо выставить корректную версию.
Открываем AndroidManifest.xml и находим
android:versionCode="1"
android:versionName="1.0"

versionCode — нужен для маркета. Он сравнивает текущий установленный код с кодом в маркете и если он меньше предлагает обновление.
versionName — «читабельный» номер версии.

Так как после написания первой части приложение было доступно в маркете, пришло время его обновить. Я увеличиваю versionCode на 1, а в versionName ставлю 1.1, хотя это все на ваше усмотрение, хоть всю жизнь собирайте 0.чтонибудьоченьмаленькоекакбынеещеодинноль.чтонибудьеще.

Сохраняем проект и жмем правой кнопой по нему Android tools — Export Signed Application Package. Вводим имя проекта для экспорта, обычно все уже проставлено и достаточно нажать далее. Затем надо выбрать уже существующий или создать новый ключ.
Вводим путь к ключу и два раза пароль.

Заполняем примерно так. По поводу периода валидности, гугл рекомендует выставлять чуть ли не до 2030 года, поэтому выкручиваем на максимум)

Жмем next и подписываем, получая apk файл с приложением.
Приложение apk (46 Кб)

На всякий случай рекомендую выполнить комманду adb install TimeManager_2.apk, которая установит уже подписанную версию на телефон и еще раз проверить работоспособность.

Идем в админку маркета и заливаем наше приложение. В принципе там тоже все ясно, стоит лишь пояснить как же делать скриншоты.

Нужен установленный SDK.
В терминале делаем ddms, выбираем слева свою трубку или эмулятор, Device — Screen Capture.

И не забывайте написать описание программы и ее название на русском языке)

Специально для MiXeR про сворачивание и так далее, я наверное опять отправлю вас на android.com, а именно в статью из блога комманды разработчиков, там очень много чего интересного.

А по поводу runOnUiThread().
Создавая activity без потоков, мы подразумеваем, что весь код будет выполняться в одном потоке т.н. UI Thread, потому что в нем отрисовывается весь интерфейс. Почему выполняя долгие вычисления интерфейс тормозит? Правильно, потому что все работает в одном потоке, и пока вычисления не закончатся, интерфейс дальше отрисовываться не будет. Для этого используют потоки, хотя это далеко не главное их предназначение. Но не в UI Thread отрисовать интерфейс нельзя. Для этих задач есть Handlers, которые вызываются из поток. В них и происходит отрисовка. Вообще они дают очень много полезностей, как например можно сделать так, чтоб один поток по окончанию вызывал другой, ну это как пример. Но они очень громоздкие, и гораздо легче сделать метод runOnUiThread() у которой в параметрах идет класс Runnable, где в методе run() описывается все что нужно делать именно в потоке интерфейса. Заранее извиняюсь, да я хреновый оратор, но как то так вобщем)
Теги:
Хабы:
+29
Комментарии23

Публикации

Истории

Работа

Ближайшие события