Wednesday, February 13, 2013

Localization of date/time using Java

I recently encountered a requirement for an Android application to do the following date/time manipulation:

  • Receive a USA formatted date/time from a web service
  • Reformat the date/time for display on an Android device in the USA, Canada (French & English) and the UK
  • The display might be just the date, or the date and time
  • The year must always display 4 digits

I'm trying to avoid the use of 3rd party libraries to keep the footprint of my Android app to a minimum, but this is a surprisingly convoluted task using raw Java.  The solution I came up with involves the following steps:

  1. Convert the original USA date/time string into a Java Date using a new SimpleDateFormat
  2. Create a localized String Format pattern using a new SimpleDateFormat returned from DateFormat.getInstance
  3. Return the localized date/time string using another new SimpleDateFormat 

Here is the source code:

@SuppressLint("SimpleDateFormat")

public String getLocalizedDateAndTime(String origUsaDateTime, boolean isDateOnly, Locale dateLocale, Locale timeLocale) {

    

    Date origDate;

    try {           

        origDate = new SimpleDateFormat("MM/dd/yyyy hh:mm a", Locale.US).parse(origUsaDateTime);            

    } catch (ParseException e) {

        throw new IllegalArgumentException("The supplied Date/Time is invalid.", e);

    }

    

    String dateString, timeString;

    String datePattern = ((SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT, dateLocale))

        .toLocalizedPattern().replaceAll("\\byy\\b", "yyyy");

    dateString = new SimpleDateFormat(datePattern).format(origDate);

 

    if (!isDateOnly) {

        

        String timePattern = ((SimpleDateFormat) DateFormat.getTimeInstance(DateFormat.SHORT, timeLocale))

            .toLocalizedPattern();

        timeString = " " + new SimpleDateFormat(timePattern).format(origDate);

    }

    else timeString = "";

            

    return dateString + timeString;     

}

 

I provide separate Locale instances for date and time because this process throws an invalid format exception for Locale.CANADA_FRENCH when attempting to generate the dateString value.  The  line:

String datePattern = ((SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT, dateLocale))

    .toLocalizedPattern().replaceAll("\\byy\\b", "yyyy");

 
returns an invalid pattern String for CANADA_FRENCH so for this particular scenario I can provide Locale.CANADA for the date and Locale.CANADA_FRENCH for the time.

 


Tuesday, February 12, 2013

Android SDK Manager crash caused by jar files in classpath

My OS X Android SDK Manager was crashing whenever I tried to load a new API.  The log entry is shown below.

  1. Open the Android SDK Manager
  2. Select an API package (any package)
  3. Click "Install Packages"
  4. Get the error after a minute or so.

I tried restarting my Mac, running OnyX to fix permissions and purge unused memory, and running the SDK manager as administrator (sudo ./android).  I also checked for Eclipse updates but none were available.

The cause of this problem apparently was having one or more of these jar files in my classpath (/Library/Java/Extensions) - as soon as I removed all of these, the error went away:

  • commons-logging-1.1.1.jar
  • jul-to-slf4j-1.7.2.jar
  • junit-4.10.jar
  • log4j-over-slf4j-1.7.2.jar
  • slf4j-api-1.7.2.jar
  • slf4j-ext-1.7.2.jar
  • slf4j-jcl-1.7.2.jar
  • slf4j-migrator-1.7.2.jar

I had installed these files as part of my attempt to configure Eclipse for Robolectric framework testing of Android projects.  The Robolectric framework has undocumented dependencies on the Simple Logging Facade for Java (SLF4J) and the Apache Commons Logging Component.

Fetching URL: http://developer.android.com/guide/developing/tools/emulator.html
Done loading packages.
Fetching https://dl-ssl.google.com/android/repository/addons_list-2.xml
Preparing to install archives
Downloading SDK Platform Android 2.3.3, API 10, revision 2
Validate XML
Parse XML
Fetched Add-ons List successfully
Fetching URL: https://dl-ssl.google.com/android/repository/repository-7.xml
Unexpected Error installing 'SDK Platform Android 2.3.3, API 10, revision 2': java.lang.OutOfMemoryError: Java heap space
Downloading Samples for SDK API 10, revision 1
Done loading packages.
Unexpected Error installing 'Samples for SDK API 10, revision 1': java.lang.OutOfMemoryError: Java heap space
Skipping 'Intel x86 Atom System Image, Android API 10, revision 1'; it depends on 'SDK Platform Android 2.3.3, API 10, revision 2' which was not installed.
Skipping 'Google APIs, Android API 10, revision 2'; it depends on 'SDK Platform Android 2.3.3, API 10, revision 2' which was not installed.
Done. Nothing was installed.

Unit testing a custom Android view

Introduction:

Unit testing a custom View in Android can be accomplished without having to run the tests in an emulator or having to rely on a framework such as Robolectric. The trick is to structure the source code to avoid View constructors that would lead to unit test failures. This example makes use of JUnit and EasyMock. It demonstrates unit testing of a custom view that overlays two ImageView elements inside of a RelativeLayout. The top image is hard-coded to be a small symbol. The bottom image is inserted in code.

 

Layout:

<?xmlversion="1.0"encoding="utf-8"?>

<mergexmlns:android="http://schemas.android.com/apk/res/android">

<RelativeLayout

  android:id="@+id/image_with_remove_cell"

  android:layout_width="wrap_content"

  android:layout_height="wrap_content">

    

    <!-- bottom ImageView is inserted here using code -->

 

    <ImageView

      android:id="@+id/remove_image_icon"

      android:layout_width="wrap_content"

      android:layout_height="wrap_content"

      android:src="@drawable/delete"

      android:layout_alignParentTop="true"

      android:layout_alignParentRight="true"

      android:contentDescription="@string/delete_thumbnail_contentDescription"/>

    

</RelativeLayout>

</merge>

 

Source Code:

The source code is structured so that the functionality can be unit tested without having to instantiate the ImageGridView class.  Subtypes are employed to reduce the number of class files required.

package com.crdn.android.app;

 

import com.gravityworksdesign.util.IView;

 

import android.content.Context;

import android.util.AttributeSet;

import android.view.View;

import android.view.ViewGroup;

import android.widget.ImageView;

import android.widget.RelativeLayout;

 

public class ImageRemoveGridCell extends RelativeLayout implements IView {

 

    private final ImageRemoveGridCellUtil _util;

    

    public ImageRemoveGridCell(Context context) {

        this(context, null);

    }

 

    public ImageRemoveGridCell(Context contextAttributeSet attrs) {

        this(contextattrs, 0);

    }

 

    public ImageRemoveGridCell(Context contextAttributeSet attrs, int defStyle) {

        super(contextattrsdefStyle);

        

        _util = new ImageRemoveGridCellUtil(context, this, null);

    }

    

    public ImageRemoveGridCellUtil getUtil() {

        return _util;

    }

    

    

    public interface IInitializer {

        void inflate(Context context);

        RelativeLayout getLayoutFromGridCell();

    }

    

    public class Initializer implements IInitializer {

        

        private final IView _view;

        

        public Initializer(IView view) {

            _view view;

        }

 

        @Override

        public void inflate(Context context) {

            View.inflate(contextR.layout.widget_image_with_remove_cell, (ViewGroup_view);

        }

 

        @Override

        public RelativeLayout getLayoutFromGridCell() {

            return (RelativeLayout_view.findViewById(R.id.image_with_remove_cell);

        }

        

    }

    

    public class ImageRemoveGridCellUtil {

        private final IInitializer _initializer;

        private final RelativeLayout _cell;

        private ImageView _image;

        

        public ImageRemoveGridCellUtil(Context contextIView imageRemoveGridCellIInitializer initializer) {

            

            _initializer initializer == null ? new Initializer(imageRemoveGridCell) initializer;

            _initializer.inflate(context);

            _cell _initializer.getLayoutFromGridCell();

        }

        

        public void setLayoutParams(ViewGroup.LayoutParams params) {

            

            RelativeLayout.LayoutParams = (RelativeLayout.LayoutParams_image.getLayoutParams();

            p.height params.height;

            p.width params.width;

            _image.setLayoutParams(p);

        }

        

        public void addImage(ImageView imageView.OnClickListener imageOnClickListener)  

            

            setImage(image);

            _image.setOnClickListener(imageOnClickListener);

            _cell.addView(_image, 0);

        }

 

        public ImageView getImage() {

 

            return _image;

        }

        

        public void setImage(ImageView image) {

            _image image;

        }

    }

}

 

Unit Test Code:

package com.crdn.android.app.test;

 

import static org.junit.Assert.*;

import static org.easymock.EasyMock.*;

 

import org.easymock.EasyMock;

import org.easymock.IArgumentMatcher;

import org.junit.After;

import org.junit.AfterClass;

import org.junit.Before;

import org.junit.BeforeClass;

import org.junit.Test;

 

import com.crdn.android.app.ImageRemoveGridCell;

 

import android.content.Context;

import android.view.View;

import android.view.ViewGroup;

import android.widget.ImageView;

import android.widget.RelativeLayout;

 

public class ImageRemoveGridCellTests {

 

    private static ImageRemoveGridCell.IInitializer _stubInitializer;

    private static RelativeLayout _mockCell;

    private static ImageView _mockImage;

    private ImageRemoveGridCell.ImageRemoveGridCellUtil _obj;

    

    @BeforeClass

    public static void setUpBeforeClass() throws Exception {

        _stubInitializer createNiceMock(ImageRemoveGridCell.IInitializer.class);

        _mockCell createStrictMock(RelativeLayout.class);

        _mockImage createStrictMock(ImageView.class);

    }

 

    @AfterClass

    public static void tearDownAfterClass() throws Exception {

    }

 

    @Before

    public void setUp() throws Exception {

        reset(_stubInitializer);

        reset(_mockCell);

        reset(_mockImage);

        

        Context dummyContext createNiceMock(Context.class);

        expect(_stubInitializer.getLayoutFromGridCell()).andReturn(_mockCell);      

        replay(_stubInitializer);

        

        _obj createNiceMock(ImageRemoveGridCell.class)

                .new ImageRemoveGridCellUtil(dummyContext, null_stubInitializer);

    }

 

    @After

    public void tearDown() throws Exception {

    }

    

    @Test

    public void construct_inflate_before_getLayoutFromGridCell() {

        

        Context dummyContext createNiceMock(Context.class);

        ImageRemoveGridCell.IInitializer mockInitializer createStrictMock(ImageRemoveGridCell.IInitializer.class);

        mockInitializer.inflate(dummyContext);

        expectLastCall().times(1);

        expect(mockInitializer.getLayoutFromGridCell()).andReturn(_mockCell);

        

        replay(mockInitializer);

        

        createNiceMock(ImageRemoveGridCell.class)

                .new ImageRemoveGridCellUtil(dummyContext, nullmockInitializer);

        

        verify(mockInitializer);

    }

 

    @Test

    public void addImage_sets_image_onClickListener_adds_image_view_to_cell() {

        

        View.OnClickListener dummyListener createNiceMock(View.OnClickListener.class);

        _mockImage.setOnClickListener(dummyListener);

        expectLastCall().times(1);

        

        _mockCell.addView(_mockImage, 0);

        expectLastCall().times(1);  

        

        replay(_mockImage);

        replay(_mockCell);

        

        _obj.addImage(_mockImagedummyListener);

        

        verify(_mockImage);

        verify(_mockCell);

    }

 

    @Test

    public void getImage_image_not_set_returns_null() {

        assertNull(_obj.getImage());

    }

    

    @Test

    public void getImage_after_addImage_returns_image() {

        

        _obj.addImage(_mockImagecreateNiceMock(View.OnClickListener.class));

        assertEquals(_mockImage_obj.getImage());

    }

    

    @Test

    public void setLayoutParams_transfers_height_and_width_from_provided_LayoutParams_to_image() {

        

        ViewGroup.LayoutParams stubParams createNiceMock(ViewGroup.LayoutParams.class);

        stubParams.height = 30;

        stubParams.width = 20;

        

        RelativeLayout.LayoutParams actualP createNiceMock(RelativeLayout.LayoutParams.class);

        

        expect(_mockImage.getLayoutParams()).andReturn(actualP);

        _mockImage.setLayoutParams(eqLayoutParams(stubParams));

        expectLastCall().times(1);

        replay(_mockImage);

        

        _obj.setImage(_mockImage);

        _obj.setLayoutParams(stubParams);

        

        verify(_mockImage);

    }

    

    public static <extends ViewGroup.LayoutParamsT eqLayoutParams(T in) {

        EasyMock.reportMatcher(new ImageRemoveGridCellTests().new ParamsEquals(in));

        return null;

    }

 

    public class ParamsEquals implements IArgumentMatcher {

    

        private ViewGroup.LayoutParams _expected;

        

        public ParamsEquals(ViewGroup.LayoutParams expected) {

            _expected expected;

        }

        

        @Override

        public boolean matches(Object argument) {

            if (argument == null || !(argument instanceof RelativeLayout.LayoutParams))

                return false;

            RelativeLayout.LayoutParams actual = (RelativeLayout.LayoutParamsargument;

            return actual.height == _expected.height && actual.width == _expected.width;

        }

    

        @Override

        public void appendTo(StringBuffer buffer) {

               buffer.append("eqLayoutParams(");

                buffer.append(_expected.getClass().getName());

                buffer.append(" with height '");

                buffer.append(_expected.height);

                buffer.append("' and with width '");

                buffer.append(_expected.width);

                buffer.append("'");

        }

    }

}