To format dates in an Android application, you must keep in mind that dates formatted using the Android SDK take into account the locale, which includes the country and language (this is also called a culture). The locale is configured in the Settings application of the device. In general using the locale of the device is the best option, but this may be unacceptable for enterprise applications where all users need to see the same format, regardless of the language of the device. Also, dates that are saved to a file or a database should always have the same format.
Here is a list of the many classes involved in handling and formatting dates :
java.util.Date : represents a single date and compares dates. In Java, the date object also includes the time, so if you only need the date the time should always be the same (for example midnight) to make sure comparisons will return the expected result.
java.util.Calendar : extracts the data for the day, month and year from dates and handles mathematical operations between dates.
android.text.format.DateFormat : gets the date and time format according the current locale of the device. The format is returned as a java.text.Format that can be used with the Java format classes.
java.text.DateFormat : represents a date format. It should not be used directly since it does not manage the Android locale.
java.text.SimpleDateFormat : derived from java.text.DateFormat, format a date according to the specified format.
java.sql.Date/java.sql.Time : two classes derived from the java.util.Date class used to handle dates in the SQL format to write to a database or read from it. Those classes are used to split the java.util.Date into a SQL Date and SQL Time. They will not be used in this article, but since they derive from java.util.Date, everything that works with a java.util.Date will work with a java.sql.Date or a java.sql.Time.
The java.util.Date is a specific point on a timeline : it can check if its date is before or after another date, but it has no idea of how it fits in a month or in a year. The java.util.Calendar class is the one that handles how a calendar for a year behaves: for example, you need a Calendar object to get the current date or time, to know when a month start and ends or to check what is the next year. So, to get a java.util.Date for the current date, you must get an instance of the java.util.Calendar, which is initialised by default at the current date and time.
Date currentDate = Calendar.getInstance().getTime();
After that, if you want to display the date retrieved from the calendar, you need to get the date format for the current locale of the device with the android.text.format.DateFomat class that returns a java.text.DateFormat. For example, to display the current date and time in the current locale, you can use the following code :
If your application has many time-intensive operations, here are some tricks to improve the performance and provide a better experience for your users.
Operations that can take a long time should run on their own thread and not in the main (UI) thread. If an operation takes too long while it runs on the main thread, the Android OS may show an Application not responding (ANR) dialog : from there, the user may choose to wait or to close your application. This message is not very user-friendly and your application should never have an occasion to trigger it. In particular, web services calls to an external API are especially sensitive to this and should always be on their own thread, since a network slowdown or a problem on their end can trigger an ANR, blocking the execution of your application. You can also taken advantages of threads to pre-calculate graphics that are displayed later on on the main thread.
If your application requires a lot of call to external APIs, avoid sending the calls again and again if the wifi and cellular networks are not available. It is a waste of resources to prepare the whole request, send it off and wait for a timeout when it is sure to fail. You can pool the status of the connexion regularly, switch to an offline mode if no network is available, and reactivate it as soon as the network comes back.
Take advantage of caching to reduce the impact of expensive operations. Calculations that are long but for which the result won’t change or graphics that will be reused can be kept in memory. You can also cache the result of calls to external APIs to a local database so you won’t depend on that resource being available at all times. A call to a local database can be faster, will not use up your users’ data plan and will work even it the device is offline. On the other hand, you should plan for a way to fresh that data from time to time, for example keeping a time and date stamp and refreshing it when it’s getting old.
Save the current state of your activities to avoid having to recalculate it when the application is opened again. The data loaded by your activities or the result of any long-running operation should be saved when the onSaveInstanceState event is raised and restored when the onRestoreInstanceState event is raised.Since the state is saved with a serializable Bundle object, the easiest way to manage state is to have a serializable state object containing all the information needed to restore the activity so only this object needs to be saved. The information entered by the user in View controls is already saved automatically by the Android SDK and does not need to be kept in the state. Remember, the activity state may be lost when the user leaves your application or rotates the screen, not only when the user navigates to another activity.
Make sure your layouts are as simple as possible, without unnecessary layout elements. When the view hierarchy gets too deep, the UI engine have trouble traversing all the views and calculating the position of all elements. For example, if you create a custom control and include it in another layout element, it can add an extra view that is not necessary to display the UI and that will slightly slow down the appication. You can analyse your view hierarchy to see where your layout can be flattened with the Hierarchy Viewer tool. The tool can be opened from Eclipse using the Dump View Hierarchy for UI Automator icon in the DDMS perspective, or launch the standalone tool hierarchyviewer in the \tools\ directory.
If you have other unexplained slowdown in your application, you should profile it to help identify bottlenecks. In that case, you should take a look at my article about profiling Android applications.
For Android applications, logging is handled by the android.util.Log class, which is a basic logging class that stores the logs in a circular buffer for the whole device. All logs for the device can be seen in the LogCat tab in Eclipse, or read using the logcat command. Here is a standard log output for the LogCat tab in Eclipse :
There are five levels of logs you can use in your application, for the most verbose to the least verbose :
Verbose:For extra information messages that are only compiled in a debug application, but never included in a release application.
Debug: For debug log messages that are always compiled, but stripped at runtime in a release application.
Info: For an information in the logs that will be written in debug and release.
Warning: For a warning in the logs that will be written in debug and release.
Error: For an error in the logs that will be written in debug and release.
A log message includes a tag identifying a group of messages and the message. By default the log level of all tags is Info, which means than messages that are of level Debug and Verbose should never shown unless the setprop command is used to change the log level. So, to write a verbose message to the log, you should call the isLoggable method to check if the message can be logged, and call the logging method :
if (!Log.isLoggable(logMessageTag, Log.Verbose))
Log.v("MyApplicationTag", logMessage);
And to show the Debug and Verbose log message for a specific tag, run the setprop command while your device is plugged in. If you reboot the device you will have to run the command again.
Unfortunately, starting with Android 4.0, an application can only read its own logs. It was useful for debugging to be able to read logs from another application, but in some cases sensitive information was written in those logs, and malicious apps were created to retrieve them. So if you need to have logs files send to you for debugging, you will need to create your own log class using the methods from the android.util.Log class. Remember, only Info, Warning and Error logs should be shown when the application is not run in debug mode. Here is an example of a simple logger wrapping the call to isLoggable and storing the logs messages on the primary storage of the device (requires the permission WRITE_EXTERNAL_STORAGE) and to the standard buffer :
/**
* A logger that uses the standard Android Log class to log exceptions, and also logs them to a
* file on the device. Requires permission WRITE_EXTERNAL_STORAGE in AndroidManifest.xml.
* @author Cindy Potvin
*/
public class Logger
{
/**
* Sends an error message to LogCat and to a log file.
* @param context The context of the application.
* @param logMessageTag A tag identifying a group of log messages. Should be a constant in the
* class calling the logger.
* @param logMessage The message to add to the log.
*/
public static void e(Context context, String logMessageTag, String logMessage)
{
if (!Log.isLoggable(logMessageTag, Log.ERROR))
return;
int logResult = Log.e(logMessageTag, logMessage);
if (logResult > 0)
logToFile(context, logMessageTag, logMessage);
}
/**
* Sends an error message and the exception to LogCat and to a log file.
* @param context The context of the application.
* @param logMessageTag A tag identifying a group of log messages. Should be a constant in the
* class calling the logger.
* @param logMessage The message to add to the log.
* @param throwableException An exception to log
*/
public static void e(Context context, String logMessageTag, String logMessage, Throwable throwableException)
{
if (!Log.isLoggable(logMessageTag, Log.ERROR))
return;
int logResult = Log.e(logMessageTag, logMessage, throwableException);
if (logResult > 0)
logToFile(context, logMessageTag, logMessage + "\r\n" + Log.getStackTraceString(throwableException));
}
// The i and w method for info and warning logs should be implemented in the same way as the e method for error logs.
/**
* Sends a message to LogCat and to a log file.
* @param context The context of the application.
* @param logMessageTag A tag identifying a group of log messages. Should be a constant in the
* class calling the logger.
* @param logMessage The message to add to the log.
*/
public static void v(Context context, String logMessageTag, String logMessage)
{
// If the build is not debug, do not try to log, the logcat be
// stripped at compilation.
if (!BuildConfig.DEBUG || !Log.isLoggable(logMessageTag, Log.VERBOSE))
return;
int logResult = Log.v(logMessageTag, logMessage);
if (logResult > 0)
logToFile(context, logMessageTag, logMessage);
}
/**
* Sends a message and the exception to LogCat and to a log file.
* @param logMessageTag A tag identifying a group of log messages. Should be a constant in the
* class calling the logger.
* @param logMessage The message to add to the log.
* @param throwableException An exception to log
*/
public static void v(Context context,String logMessageTag, String logMessage, Throwable throwableException)
{
// If the build is not debug, do not try to log, the logcat be
// stripped at compilation.
if (!BuildConfig.DEBUG || !Log.isLoggable(logMessageTag, Log.VERBOSE))
return;
int logResult = Log.v(logMessageTag, logMessage, throwableException);
if (logResult > 0)
logToFile(context, logMessageTag, logMessage + "\r\n" + Log.getStackTraceString(throwableException));
}
// The d method for debug logs should be implemented in the same way as the v method for verbose logs.
/**
* Gets a stamp containing the current date and time to write to the log.
* @return The stamp for the current date and time.
*/
private static String getDateTimeStamp()
{
Date dateNow = Calendar.getInstance().getTime();
// My locale, so all the log files have the same date and time format
return (DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.CANADA_FRENCH).format(dateNow));
}
/**
* Writes a message to the log file on the device.
* @param logMessageTag A tag identifying a group of log messages.
* @param logMessage The message to add to the log.
*/
private static void logToFile(Context context, String logMessageTag, String logMessage)
{
try
{
// Gets the log file from the root of the primary storage. If it does
// not exist, the file is created.
File logFile = new File(Environment.getExternalStorageDirectory(), "TestApplicationLog.txt");
if (!logFile.exists())
logFile.createNewFile();
// Write the message to the log with a timestamp
BufferedWriter writer = new BufferedWriter(new FileWriter(logFile, true));
writer.write(String.format("%1s [%2s]:%3s\r\n", getDateTimeStamp(), logMessageTag, logMessage));
writer.close();
// Refresh the data so it can seen when the device is plugged in a
// computer. You may have to unplug and replug to see the latest
// changes
MediaScannerConnection.scanFile(context,
new String[] { logFile.toString() },
null,
null);
}
catch (IOException e)
{
Log.e("com.cindypotvin.Logger", "Unable to log exception to file.");
}
}
}
If you release an application to the app store or to a client with this kind of logger, you should disable logging by default and add a switch in the preferences to enable logging on demand. If the logger is always enabled, your application will often write to the primary storage and to the logcat, which is an unnecessary overhead when everything works correctly. Also, the size of the log file should be limited in some way to avoid filling up the primary storage.