Monday, August 29, 2011

Android Listview and OnItemClickListener GOTCHA


Android ListView and Custom View Gotchas


I first used the Android ListView to attach to some a list of objects and display them using the Android default/simple list view:
int layoutID = android.R.layout.simple_list_item_1;
ArrayList<Location> myList = fillLocationList();
ArrayAdapter<Location> aaBasic = new ArrayAdapter<Location>(this, layoutID, myList);

In order for it to show the string that I wanted, I had to override Location's toString, thats all fine. I also wanted to perform an action when the user selected an item in the list. This also worked well.
this.locationListView.setOnItemClickListener(new OnItemClickListener()
        {
			@Override
			public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
					long arg3)
			{
				onLocationSelected(locations.get(pos));
				
			}      	
        });  

Next I created my own ArrayAdapter and my own view. That view had multiple TextView's so that I could show more information in each list element. Inside of my ArrayAdapter, I load my custom view and set the text of each TextView to a property of my object. This also works fine and the user can still select an item and my listener gets called.
int layoutID = R.mylocationitem;
ArrayList<Location> myList = fillLocationList();
MyArrayAdapter aa = new MyArrayAdapter(this, layoutID, myList);
I'm not showing the details of MyArrayAdapter here. But it is the standard example that you see. It loads the layout and accesses the widgets inside the inflated view and sets the widget properties to match those of my object (Location). So now instead of a simple string list, I have a more complicated list.

Next, I decide that I want my custom view to contain both a TextView and a RatingBar. I want the user to see a name of a location as well as its "rating". The ratingbar is readonly so I have to disable it. (Since then, I found out that RatingBar has a property named 'isIndicator' and when this is true, the rating bar is readonly. This is better than disabling it but does not change the issue here.)

Problem 1 - my listener stops working as soon as I add the RatingBar.


I know that what is happening is that my RatingBar is forcing the view to take focus so that the user can interact with the rating bar. Thus the custom view sees the user click rather than the list adapter. But I am not able to "tip off" android that I dont want this to happen. I see that some android templates specify that a list item control should not be "Clickable" but this does not work for me. Neither does the disabling of the rating bar. I tried things like this:
view.setFocusable(false);
//			ratingBar.setFocusable(false);
//			ratingBar.setFocusableInTouchMode(false);
//			ratingBar.setClickable(false);

But none of that works. Next I give up on getting OnItemClick to work and decide to handle the click event in my item view element itself. Naively, I first try to use aOnClickListener.
			ratingBar.setOnClickListener(new OnClickListener()
			{
This was naive, reading the android documentation more and I realize it is the processing of user-touch events which lead to a click event. The user has to tap down and then up to cause a click. But the widgets are handling the touch events. So I need to handle touch events instead.
			OnTouchListener touchListener =		new OnTouchListener()
			{
				@Override
				public boolean onTouch(View arg0, MotionEvent arg1)
				{
					HomeActivity ha = (HomeActivity) getContext();
                                        ha.OnItemSelected(position);
					return true; //If I dont return true, I stop getting Touch events
				}
			
			};
                       newView.setOnTouchListener(touchListener);
This is starting to look good. Note that my listener is now on the entire view rather than any of the individual widgets. Ideally, I'd like to raise the OnItemSelected event myself but I dont know how to do that, so I just access the parent activity directly and make a call. Okay, dont yell at me yet, I know this isnt right but lets make some comments:
  • position is a final member of the adapter. I have to make it final so that I can access it in the anonymous listener class.
  • I added a method named "OnItemSelected" to my calling activity. This takes place of the OnItemSelected listener which I could not get to work.
  • NOTE: In testing, I found that if I returned false from onTouch, I would no longer get any user touch events. So I return true.
  • Okay, this code basically accomplishes the goal: now when the user taps an item in my list, the above listener gets called and my activity gets notified and it goes ahead and works correctly. This happens when the user taps an item. 'BUT' it also happens if the user drags his/her finger (e.g. when scrolling the list). OOPS!

Problem 2 - now my item select code works, but the user cannot scroll

Ooops, it seems I am not paying attention to the MotionEvent argument. Here are the applicable touch events:
  1. MotionEvent.ACTION_DOWN - the user has pressed down on the screen.
  2. MotionEvent.ACTION_UP - the user has taken their finger off the screen.
  3. MotionEvent.ACTION_MOVE - the user is dragging
  4. MotionEvent.ACTION_CANCEL - in my experience, I see CANCEL once the user drags outside of the window. At the part, the list starts to scroll and I get anACTION_CANCEL. And I get no further messages after this.
  5. MotionEvent.ACTION_OUTSIDE - I did not see this in my testing.

So, I change my logic to look for both a ACTION_DOWN and a ACTION_UP before I perform the OnItemSelected action. In testing, I found that no matter how crisply I 'tapped', I always got some intermediate ACTION_MOVE events. So I used logic to allow at most 8 of these. If the users does any more than that, then I assume they are just screwing around. HA.

Okay, so now things work well. And I'm looking for a nice way to centralize this code so that it easy for me to reuse this code. But in testing, I found one more problem.

Problem 3 - unexpected list events. I would see that often when I closed the activity which held the list, my OnItemSelect logic was being called. In my app, that meant that a new activity was launched. Spooky. I caught this in the debugger and it seemed that indeed list custom view events were being fired from a routine named "Die" (I think it is Activity.Die - no kidding). Strange, but really after my activity is no longer active, I should not listen to these events any more and this was an easy fix:

  1. In Activity.OnPause - I deactivate my event notifications (alternatively you can let them fire and just ignore them)
  2. In Activity.OnResume - I reactivate/activate my event notifications.

Wednesday, August 24, 2011

Android Development Gotchas

Android Gotchas

Here are some Android development gotchas which I have found


ResumeException

I got this because I overrided onResume but forgot to call super.onResume() DUHH
@Override
protected void onResume() {
super.onResume(); //this was missing
}

Strange exception when starting app.

I added a new activity and found my entire app would not start! And I wasn't even using that app! Things to check
  1. Be sure that your resource ids are not duplicated. This causes an error when the app starts, even though it might not yet use the resource.
    1. I have started every resource id with the activity name (e.g. id=myactivity_btnOk)
  2. Look for other errors in your resource layout files. If you just made a change, back out the change.

Error loading my new activity

  1. Oops, the activity was not declared in the manifest
  2. Or oops, it was declared but I did not use the correct namespace

I can't get a non-null location manager

  1. If you do not have the correct privileges in your manifest, there is no error but no manager comes back.

Some activity launches when I navigate away from a screen.

  1. Check your event handlers to see if they are causing it. I am trying to deregister event handlers when my activity pauses.

My app raises an exception but I am having trouble seeing why.

  1. Yuch, I dont love the exception reporting. Try running LOGCAT from ABD. You get much more details.
ABD LOGCAT

LOGCAT is clipping my text.

  1. Run it from ABD, then the text all wraps

I put data into an intent, but its different when I pull it out!! The reason: makek sure that you know the type of data that you are storing and use the correct get routine to retrieve it. In my case, I was storing a long but using getIntExtra instead of getLongExtra. Duh.

Intent data = new Intent();
      data.putExtra("NumberOfCats", numberOfCats);
//
//
     int numStored = data.getIntExtra("NumberOfCats"); //gets wrong value because numberOfCats is a long and not an int!

I made a custom list adapter with EditText and such but now it doesn't select or scroll.

Yuch, I am going to blog on this. If the view inside the list can accept clicks/touches, then it screws up the OnItemSelected listener. So you are better off to use things likeTextViews. But I got this to work - see upcoming blog entry.

Monday, August 8, 2011

Fine Tuning the App

Well, I am now on version 3. Version 2 came maybe four days after the initial release and Version 3 came almost right away. Here are some comments:


  1. I love testing/debugging directly on my phone rather than using the emulator. BUT I have to remember that there are big differences between a plugged in phone and a disconnected phone. When I debug on my phone, the cable is attached to my USB port and the phone is charging. So it does not dim or go to sleep and so I was missing those test cases. The end result is that when I finally tested release 2 on my disconnected phone, it did not work as well.
  2. Wake Locks - Wake locks can be used to keep the display from dimming or the CPU from going to sleep. I was loathe to use them but it is just to inconvenient as a user to have to keep turning on the phone to see how much time is left or to look at my task list. Some notes:
    1. I chose to let the phone dim slightly instead of stay completely bright.
    2. Wake lock is per activity, so I had to remember to use it on both my home screen and my "running" screen.
    3. remember to release the wake lock when the activity is paused. I made sure that once the user left my activity, that the wake lock stopped working and the phone would dim/sleep while in other tasks.
  3. Intents - when a task is finished, i add a notification to the top of the screen. I thought it would be cool to let them see that a task had finished (even though it should be obvious) and then start the app from there. Good news is that it worked, the bad news is that it always started a new instance of my application. I didn't want this, i wanted it to join the running app. This is something I just have to get used to with Android: it controls when an app starts and I just can't expect that the app will still be running. The best I could do was change the activity launch mode to "singleTop" so that Android would try to use the current instance if it is at the top of the task list. I am sure I don't yet understand this correctly but it helped.

Wednesday, August 3, 2011

Can't find my published app on Android Market

I published my first app under the name "My Pomodoro Time Management". I wanted the name to be just "My Pomodoro" (which is the app name) but I was concerned people would not know what Pomodoro is.

Anyhow, so I wait a while and then I go to Android Market and search for "Pomodoro". My app doesn't show up. But I get 21 other hits including an app named MyPomodoro. I then search for "My Pomodoro" and I find nothing. I search for "Time Management" and I find my app!!!

Yuch. I was really disheartened that I finally had my app out there but no one would be able to find it! Why can't it find "My Pomodoro"?? Anyhow, I changed the app name to "Pomodoro" and waited a while. Later that day, a search for "My Pomodoro" found my app under the new name!! So I waited another day and now I can search for "Pomodoro" and there is my app.

So one thing I learned is that I have to be a little patient. Wait at least a day. But I still dont think the search was working that well. It should have found my app right away when I searched for "Pomodoro" since it could find it under "Time Management". Lesson is, don't take the search for granted!

Monday, August 1, 2011

Publishing my First Andoid App to the Android Marketplace

Publishing my first app to Android Marketplace

I wish I could say that I could whip this off in a day or even a weekend. Truth is, it took alot of preparation. I referred to numerous Android books, web sites as well as the android developers site: http://developer.android.com/guide/publishing/preparing.html.

Application icon

You have to have an icon and I am no artist so this was not a trivial step for me. In the end, I downloaded Inkscape from inkscape.org and made some silly icon for the drawable-hdpi folder (which should be 72x72 pixels). I used the same image for the other densities since I knew they could still be displayed.

Minor changes to the appliction

I commented out my logging code and turned off my internal debugging switch (which does simple things like speed up wait times for testing). I then made the following changes to my manifest file.
  1. I made sure to include a versionCode and versionName manifest attribute. versionCode starts at 1. versionName is what I want the user to see.
  2. I removed the debuggable=true attribute from the application tag.
  3. I made sure the application tag had both an icon and name attribute.
  4. Because I did not want to support small screens, I included a "support-screens" tag and turned off support for small screens.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.androidmocha.mypomodoro"
      android:versionCode="1"
      android:versionName="0.1 Beta">
    <uses-sdk android:minSdkVersion="5" />
    <uses-permission android:name="android.permission.VIBRATE" />
 <supports-screens
  android:smallScreens="false" 
  android:normalScreens="true"
  android:largeScreens="true"
  android:anyDensity="true"
  />
 <application 
        android:icon="@drawable/app_launcher" 
     android:name=".PomodoroApp"
     android:label="@string/app_name">
I then convinced myself that I HAD to add an ABOUT screen to the application where I show the application name, the version and some simple text on how to use the application. I show that screen on startup unless the user clears a checkbox to the contrary. It is also viewable from the home screen menu.

Testing

I repeated my testing:
  1. On my phone (landscape and portrait)
  2. On my normal emulator
  3. On my emulator running a "small" screen resolution of 320x480. I was not happy with these results in landscape mode as text was clipped but in the end I looked at what android devices are on the market (http://developer.android.com/guide/practices/screens_support.html) and decided I didn't care.

Adding of the EULA

The addition of a EULA is suggested in multiple places but I didn't see many apps doing it. Further, I was loathe to include some giant amount of legal text. In the end, I took the time to create a screen which is shown once with some very simple text about how this software might not work and the user assumes all risks. I tie the showing of the screen to a version dependent preference parameter (e.g. ShowUserAgreement_V1 ) and if the user agrees to the screen then I turn off the showing of that screen. Otherwise, there is no way for the user to move further.

Making screenshots.

I know that I will need screenshots of the application in Android Market so I do this while testing. From Eclipse, I open the "DDMS" perspective and click on the "Screen Capture" button when the app is running on my phone from Eclipse.

Making the release APK

Finally, I was ready to make the APK. As documented, the APK which Eclipse normally makes is a DEBUG APK which is signed with a debug key. This is not acceptable by Android Market. So I have to compile a release APK, create a new key store, a key for my package, then I have to sign the APK and optimize the APK with ZipAlign.exe. Fortunately, Eclipse has a wizard which does all of this for you. It is in File | Export | Android |Export Android Application. Still I had to enter alot of information.
  1. I had to specify the name of a new keystore, I chose a name specific to the URL which I made specifically for my android apps. It is called AndroidMocha.com and I registered the URL from Google - lol. So my keystore is AndroidMocha.Keystore and is located in the same folder as the debug keystore which on Windows 7 is <My Documents>\.android.
  2. I choose a strong password for my keystore.
  3. Next I had to generate a key. As discussed in the documentation, I want to use the same key on all future versions of this product and for any related products which might run together. Therefore, I named it after my URL and gave it a validity of 25 years (as recommended in the documentation). I left all the organization fields blank except for "Organization" which i made the same as my website name (AndroidMocha.com)

The wizard finished without error and gave me an APK which is supposedly ready for publishing!!

Retesting the APK.

Well now that I have a new release APK, I have to retest it right? But how do I install the APK on my phone? Normally Eclipse does that for me. I google it and the documentation says I should:
  1. Hook up my phone
  2. Run ADB.EXE (Its in platform-tools folder of the sdk)
    1. ADB.EXE INSTALL <Path-To-APK>

And that works! Note that I first uninstalled my previous build from my phone just to make sure it worked correctly. Now I do a couple simple tests and I am ready to go further.

Getting an Android Market Developer Account

To do this, I have to go to the following address and sign in with my google account. Then I have to pay $25 through google-checkout so now they have my credit card which is awesome. I am forced to fill out a profile which I do. For the developer name and website, I give them my website. For an email address, I give them my website email address. (At this point, I am very happy with the $10 investment in registering a URL). They make me give them my phone number and so I do. I submit the form and they promptly charge me and activate my android market account!

https://market.android.com/publish/Home

I am so close now!! I can almost smell it. How much longer should this take?


Uploading the APK to Android Market and Publishing

From Android Market, I have an option to Upload Applications. Great, this is what I want, but when I see the next screen, I know it won't be done this minute.
  1. First,, they ask me for two screenshots. I am prepared for that because I made previously (see above). I give them two.
  2. Next, they want a 512x512 "High resolution application icon" and it is REQUIRED. What the heck?? It also has a bunch of art-speak I don't understand what it even means much less how to do it. (Drop shadow: black. What is a drop shadow??) I did not know about this and please see my comments above about my very limited artistic skills. I google this and everyone else is similarly confused and annoyed by this requirement. One guy uploaded a 512x512 white square. People say you can just upload your launcher icon. But I go back into inkscape and make a 512x512 PNG of my icon. Fine.

Uploading the APK - Second Try.


APK Upload

Now I have my APK, my screenshots and my "High Resolution Application Icon". So I am ready to go. I browse for my APK and press UPLOAD. There is an error right away. I do it again, same thing. Then I realize that I am using "Internet Explorer" and I laugh. I start up the Google Chrome and it works right away. hahahahahah. It is happy with my APK! It tells me the correct version number (both code and name), my minimum API level as well. It then tells me that I have required one special permission (this is true, I need android.permission.VIBRATE to vibrate the phone if the user has the sound off). It ALSO tells me that I have one feature that can be used as a filter in android market, it is "android.hardware.touchscreen". Not sure about that but yeah they have to have a touchscreen so I move on.

Upload Assets

I upload two of my screenshots since two are required.
Next I get to the "High Resolution Application Icon" and thankfully it accepts it because this is required. I skip the optional "Promotional Graphic", "Feature Graphic" and "Promotional Video". I am not sure whether to check the "Marketing Opt-Out". I google this and nobody else seems to know. So I leave it unchecked.

Listing Details

  1. Title - I only get 30 characters so I go with my app name plus its category (My Pomodor - time management)
  2. Description - I write a description off the cuff
  3. Recent Changes - I just say its the first release.
  4. Promo Text - i wing it.
  5. Application Type = Applications
  6. Category = Productivity

Publishing options

  1. Copy Protection = off (feature is deprecated)
  2. Content Rating = Everyone
  3. Pricing = Free
  4. Countries = all countries

Supported devices - at the bottom of this section I am reminded that I specified that my app is only for screen layouts of NORMAL, LARGE and XLARGE. This is correct, I excluded SMALL. It then reminds me that my required device features are "android.hardware.touchscreen". I don't know how that was specified, I assume it is part of android because android devices have touch screens. Then my heart skips a beat. It tells me: This application is available to over 0 devices. What? Did I mess something up? Did my exclusion of SMALL screens exclude all devices? I google this and other people are complaining about it so I decide to press on.

Contact Information

  1. Website - got it
  2. Email - I use a mailbox at my website.
  3. Phone - yeah, I was warned against putting anything here so I leave it blank!!

Consent - I check both boxes.


Press the "Publish" button.

Arrgh, it tells me that I don't have an active APK. I move back to the "APK Files" tab and i see there is an option to "Activate" the APK. Well, what else do I want to do with it. I activate it and now my APK says "active". I go back and try again.

Progress. Now it tells me that my application is available to over 474 devices!! I hope that means device models. Ha. But I have an error. I had filled in some promotional text but apparently I don't get to provide promotional text if I dont also provide a promotional image. So I delete the text and try again.

SUCCESS it says that the app is published!! 

Here it is: https://market.android.com/details?id=com.androidmocha.mypomodoro

and I am too emotionally drained to do more today!