Simple Time Manager for Android. Part 2
I express my deep gratitude to Belkin and to all those who plus the first part, you helped me with an invite)
I want to draw your attention to the fact that I refused to use the service, as originally intended. Judge for yourself, starting the process only in order to increase the timer every second is stupid. The solution is simple: before each program stop, we save the time, and after starting we subtract it from the current time, getting the number of seconds.
Action plan:
- Bugfixes of the previous version.
- Modification of the code, for the constant operation of the application, and not just for the launched Activity
- Notifications
- Throw this whole thing to the market
I want to apologize and boast right away) I apologize for making you wait a long time, and to boast that the other day Android brought me the first well, sooooo good money and I joyfully bought myself an xbox and lost it all the weekend, so I delayed)
So, go!
First, we either read the first part , and
so that we ourselves understand everything, write under my narration in it, or,
if everything is clear, then download the sources of the first part so that there is something
to work on.
Sources (134 Kb)
Opened, launched, collected for every fireman and launched.
[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!
In my case, Eclipse swore that it could not install the applet,
due to the fact that it was already installed.
A small explanation: when collecting an applet in Eclipse with subsequent
installation, the application is signed with debug keys, and when installed from the
market, it is signed with normal keys . So eclipse does not have the ability to remove the
application installed through the market, so you have to delete it
manually. Everything is quite simple here - we go to the market and delete it), or we
act as Eclipse told us - in the terminal we do adb
uninstall <package name>.
Another digression: adb in general is a very useful thing. For
example, you can even steer the database through it, through it
screenshots are taken, and even the screencast, which I
posted on youtube in the first part .
So, uninstall, assembled and installed again. Everything should work, if
not, look for errors, or download my sources.
Bugfixes and improvements
Having used the tool one day, I realized that the screen rotation, although it doesn’t
turn off the counter, but resets it at the moment before launch, and that the
program doesn’t have enough counter reset, plus Riot reported the same bug . Well let's go, right.
Bugfix
For reference, open the activity lifecycle. When you rotate the screen, the applet
kills the activity and then restarts it. Last time, onCreate (), we
added a load of values: did the timer run and how many seconds
did it occur. The point is small - it is only necessary to save the
values of timers when killing 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 (); - the line calls the onStop () method of the parent. If
this does not tell you anything, then it would be nice to read something about
OOP in java.
stats_editor.putInt (); - Last time I already talked about this,
but again, these methods put in the application settings file that I
use to store data, our variables with the key.
stats_editor.commit (); - the method saves the variables that we
previously wrote with putInt, putBoolean, putString ... etc.
It is important to remember that, in a good way, the
configuration file can only be open for writing only once, otherwise having two
variables with the settings, after commit () we risk getting later on
the output is what exactly the last variable recorded for which
the commit () method was called;
I’m chewing a little:
Let's say in the settings there are already two integers integer_one and
integer_two, both equal 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 ();
We run the risk of getting integer_one == 1 and integer_two == 2 at the output.
We build , run, everything worked for me.
Add the ability to reset the timers.
You need to create a menu that is called by pressing the <> button on the device, and
there should be a button that will reset the timers.
To start, the button must be called. Recall how we last
edited the file res / values / strings.xml, and add there and, if you
did localization, into a file with localization.
Reset timers
My string will be called button_reset.
Digression: now you have witnessed evidence
that localization is most conveniently done at the end of the line. Just
estimate that you have not 5 lines, but 200 odd lines.
Then in the main, and so far the only class, two methods must be added:
public boolean onCreateOptionsMenu (Menu menu)
and
public boolean onOptionsItemSelected (MenuItem item)
The first - generates a menu, the second handles clicking on its elements.
Then we write the code to generate the menu:
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 - the variable contains the button itself in the menu. The first
line we only denote it.
menu.add - firstly, it is taken from a method call - look,
it is indicated there as an input parameter, secondly, this method
returns the MenuItem type, which we shove into the menuItem variable.
The following values are transferred in the parameters:
1. Button group. I have Menu.NONE, I don’t use it.
2. The identifier of the item. In general, in a good way, you need to put
different everywhere , it is easiest to put this part of the code into a loop and pass
the loop counter there. It is necessary in order to understand in the click handler
which button we clicked on.
3. The sequence.
4. The name of the button. Refers to button_reset in res / values / strings.xml.
setIcon - sets the icon for the menu item.
Please note that I did not upload ic_menu_delete.png to the drawable folder
, and that we are not accessing as usual - R.drawable, but
android.R.drawable. This means that the system will look for this picture
in the pictures of the system itself, and not specifically our application.
As always a retreat. Why use pictures from the
system itself ? Firstly, there is no need to draw or draw out what has
long been drawn. Secondly, this is the most ideal option to
create a guide system. Thirdly, for example, take a look at my
RoboJournal - a client for LiveJournal (available in the market, again search by
phrase pub: "Nixan"). In 1.5 and 1.6, the animation in the pop-up window was one,
but in 2.0 it was updated. As a result, on different systems we have different
pictures.
Let's put this case together and see what happened.
Here is my option.
It remains for complete joy only to write a handler for clicking on the menu.
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. We stop the counters.
2. Zero the text on the timers.
3. Zero the counters themselves.
4. Zero the settings where they are contained.
A little optimization
I confess, but it directly flew out of my head that the line with the counter can be set via String.format ();, which I did.
To do this, we go to TimerTask, remove all String from the list of variables, delete sections of code where zeros are added before seconds and minutes, and
rest_timer.setText (hours_ind_r + ":" + minutes_ind_r + ":" + seconds_ind_r);
change to
rest_timer.setText (String.format ("% d:% 02d:% 02d", hours_r, minutes_r, seconds_r));
We collect, try. Everything works!
We remove the need to keep the application running.
As I said earlier, to block the service in this case is not advisable. I don’t argue, I really wanted to write something about this, but the fact that I didn’t do it also to some extent howto). In addition, on the hub there is a wonderful article “Good night” by 3fonov , which perfectly describes services, namely, that there are two types of them:
1. Created, it worked, it stopped.
2. Connected, executed the code under the command of the connecting activity, stopped.
3fonovdescribed in detail the second part, there are very subtle points with the interface and aidl file, on which I once spent a day working hours. In the first type, everything is quite simple. Take, for example, our class, namely its onCreate () and onDestroy methods. All work in such a service takes place in them.
A little gag. Where am I to use what type of service?
The second type of service is very convenient for applications that imply a fairly active user communication with the server, for example, the same Google Talk, the service is constantly hanging in memory. When the onBind command is called, Activity connects to this service and asks, say, who is online, to which the service responds with data downloaded at the time of the last update. There are probably methods in the same team that allow you to send messages and set statuses. The advantage of this is that for the background work and for the user we have one socket, one connection, implemented just in this service.
The first type, in my opinion, is much more convenient for applications where the user affects the server much less, I mean, say, the content search function. At the moment I am writing an application in which the service has just a TimerTask for content updates, when updating, it saves all the information in the local database, and sends a broadcast message (in the name of Broadcast) about the interface update. At the same time, if the Activity is running, then it will pick up this message and re-read the database, displaying all new elements, otherwise Broadcast will simply do nothing. The advantage of this type of service is that, according to my observations, the time for its development and implementation, well ... I think 3 times less than the services with the ability to connect Activity to them.
So, after the retreat, we continue our realcoding)
In order for the application to continue to count the time, even when it is not active, I came up with such an algorithm.
When the application closes, it saves the current time in the settings, two work and rest timers and two Boolean values whether these timers are running. Then at startup, we read these parameters, and we subtract the value of the current time from the settings from the actual current time (It would be more correct to call this parameter the date of the last launch, or something like that).
All this is done in a few lines of code.
We add this line to the onStop () method (I recall, the method called when activity is killed and once again referring to the Activity lifecycle ) before the method that saves the settings.
stats_editor.putLong ("key_last_used_date", System.currentTimeMillis ());
I will refresh in memory. stats_editor is a variable of the SharedPreferences.Editor class that saves application settings, but we use it as data storage. There is also the SharedPreferences class, but it is only for reading these settings. Two variables String are reported to the putLong method - the settings key and long - the value. There are also putString, putInt methods, and so on, they have, respectively, the second parameter will be String, int ... They are read by the getString, getInt method, etc., also with two parameters: the key and the default value, for example, if the settings are if the system does not find, then it will return the default value.
System.currentTimeMillis () - returns the long value of the current time in milliseconds.
To all the variables that we created in the first part, add the variable int last_time; to get the difference between the current time and the time of the last launch (or rather stop) of the application.
Then we go to the onCreate () method and add there:
last_time = (int) ((System.currentTimeMillis () - saved_stats.getLong ("key_last_used_date", System.currentTimeMillis ())) / 1000);
Attention! IMHO it is worth mentioning, although I hope everyone understands that this line needs to be added after we initialized the saved_stats variable, otherwise we will catch a NullPointerException.
I chew: The current time is the time of the last start (more precisely, stop) and divide all this by 1000 to get from milliseconds a second, the whole int construct is returned.
Division? This is an integer variable! - you say, in Java, when dividing the integer by the integer, the integer part of the division will return.
It remains to add these seconds to the active counter. We are looking in the same onCreate () construct, which, depending on the Boolean variables resting and working, increases the text size of the counters and appends this increase there.
if (resting)
{
rest_timer.startAnimation (magnify_rest);
rest_time + = last_time;
}
if (working)
{
work_timer.startAnimation (magnify_work);
work_time + = last_time;
}
We start, try, everything works for me, so it should be with you.
Notifications
The algorithm is this: if the counter is turned on, then we display a notification when the applet stops, and we send a command to hide notifications in any case when the program starts.
We declare and initialize the variable of the NotificationManager class, which just controls notifications.
notificationManager = (NotificationManager) getSystemService (NOTIFICATION_SERVICE);
For this variable, we will need two methods cancel () and notify (). The first, as you probably already guessed, removes the notification. One parameter is passed to it - id, in case the application has several notifications, in order to remove some kind of notice, we use this identifier. Notify () has two parameters - id and a variable of the Notification class.
Since my application has only one notification, boldly instead of id we put 0 or any other number. It is important that those created and canceled are the same.
After initializing the notificationManager variable, you can immediately use the cancel () method, so that when the application starts, the notification is killed.
notificationManager.cancel (0);
Now go to onStop () and add there:
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. Create a variable of the Notification class.
2. We describe it - which lines and image to use, as well as what you need to run when you click on this notification.
3. We do notify ();
intent - a class variable PendingIntent - it is here that indicates which class should be run when clicked. Also pay attention to the last parameter, flags are passed there for the launched intent, I have a flag set so that the new intent updates the old one.
The notification constructor is a picture, the text that will be displayed when creating a notification, when it is the full width of the notification bar, the notification time.
setLatestEventInfo - two lines: the first is what is displayed in large print, the second is the signature in small print and Intent to run.
notification.flags - as you know, android has two types of notifications. A vivid example of this is the calendar and mounting a flash drive. One is forthcoming, the other is current. Notification.FLAG_ONGOING_EVENT - sets the status of the current notification.
By the way, you can use another flag - Notification.FLAG_AUTO_CANCEL, and remove the line that removes the notification from the onCreate () method. In general, read the explanations, there are a lot of interesting things)
notify - we are actually applying this notification.
This is how it all looks.
We collect the applet, try to run it. Everything should work out wonderfully, but for every fireman I share the source.
Zip archive (158 Kb) The
image that I used in notifications is in the updated archive with all the pictures.
Zip archive (295 Kb)
Pour into the market
Suppose you went through the developer registration procedure, paid a fee of $ 25, and now it's time to lay out the project. Well, let's go!
The first thing the application needs to set the correct version.
Open AndroidManifest.xml and find
android: versionCode = "1" android: versionName = "1.0"
versionCode - needed for the market. It compares the current installed code with the code in the market and if it offers less update.
versionName is the "readable" version number.
Since after writing the first part, the application was available in the market, it's time to update it. I increase versionCode by 1, and in versionName I set 1.1, although this is all at your discretion, at least collect your whole life 0. something very little more than one other. Something even worse.
We save the project and right-click on it Android tools - Export Signed Application Package. Enter the name of the project for export, usually everything is already set down and just click next. Then you need to select an existing one or create a new key.
Enter the path to the key and two times the password.
Fill something like this. Regarding the validity period, Google recommends exposing it almost until 2030, so we twist it to the maximum)
Click next and sign it, getting the apk file with the application.
Application apk (46 Kb)
Just in case, I recommend that you run the adb install TimeManager_2.apk command, which will install the already signed version on the phone and once again check the operation.
We go to the admin panel of the market and fill in our application. In principle, everything is clear there too, you just need to explain how to take screenshots.
I need an installed SDK.
In the terminal, do ddms, select your handset or emulator on the left, Device - Screen Capture.
And do not forget to write a description of the program and its name in Russian)
Especially for MiXeR about folding and so on, I’ll probably send you again to android.com, namely to the article from the blog of the development team , there are a lot of interesting things.
And about runOnUiThread ().
By creating activity without threads, we mean that all code will be executed in one thread of the so-called. UI Thread, because it draws the entire interface. Why is the interface slowing down when doing long calculations? That's right, because everything works in one thread, and until the calculations end, the interface will not be rendered further. For this, streams are used, although this is far from their main purpose. But it is not possible to draw an interface in UI Thread. For these tasks, there are Handlers that are called from the thread. In them, drawing takes place. In general, they provide a lot of usefulness, for example, you can make one thread at the end cause another, well, this is an example. But they are very bulky, and it’s much easier to make the runOnUiThread () method with the Runnable class in the parameters, where the run () method describes everything that needs to be done in the interface thread. I apologize in advance, but I'm a shitty speaker, but something like that in general)