Friday, December 9, 2011

New Android Utility - find orphan references

I decided to write a new utility to help clean up my Android project. It is a simple Java command line only program, no JFrame / no GUI, that takes a base directory as its only parameter. From that it builds a list of all sub-directories (ignoring .svn directories) then finds the Android generated R.java file.

R.java is parsed based on "public static final class" and "public static final int" to build a list of ID strings.

Each .JAVA and .XML file is then scanned in the directory tree looking for resource IDs in their various formats such as @string/ in XML or R.string. in Java. The reference count is incremented. At the end of the run I print out all the items that have a reference count of zero. I was able to clear up a dozen strings, a couple of layouts and 10 images from the project. Strings and layouts don't amount to much but images sure can when it comes to download size. Some of the images were in multiple directories to handle various Android sizes. Getting rid of layouts is always good as you might open one, edit it a bunch, the find out you edited something that is never used. Killing a layout might kill other image and string references too.

I wrote something similar to this at a previous job to find orphan strings before we sent the Java and C++ programs out to be localized. That saved us from paying money to localize a string that was never used in the program. I also checked for orphan images against the Java code and some other orphan resources against the C++ code. It would be nice if this was just built into Eclipse.

Now I need to do something similar for the iOS side. It will not be as straight forward as the images are directly referenced in the code as string names and I need to add a special check for @2x images to make sure they have a non-retina mate. I guess this would be a good time for me to write my first non-iPhone based Objective C program. Might put a GUI around it with a [Browse..] button to pick the directory and a list control to show the results. Seems to be cheating to write just a command line program on the Mac.

I want to run the program a few more times against various XML / Java code formats before I release it to the wild. It appears to work like a champ against the code I write but I am the only author and follow a consistent code format which does not make for a well rounded test. I kept getting false positives until I tweaked various bits of the parser. Trust but Verify was the rule until the final run when it was 100% accurate for my coding style.

Anyone have a big interest in this program and want to be a beta tester? Leave a comment and I can send you the source code (it is all of 288 lines in a single file) so you can see what you think and request improvements.

 ** Update ** Code appears below for anyone to run and check out

package org.peck;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * Simple command line program to find the R.java Android generated code and
 * look for orphan references against *.java and *.XML files  
 * 
 * @author kevin.peck
 * @date December 9, 2011
 */
public class AndroidResourceScan {
    
    private static List<String> directories = new ArrayList<String>();
    private static List<RefInfo> references = new ArrayList<RefInfo>();
    
    private static List<XmlMapping> xmlMapping = new ArrayList<XmlMapping>();
    
    private static final String RESOURCE_FILE = "R.java";
    private static final String CLASS_START = "public static final class";
    private static final String ITEM_START = "public static final int";
    
    /**
     * Android resource scanner
     * 
     * Find and parse the R.java file and see if there items in that file
     * that are not referenced in any *.java or *.xml files
     * 
     * @param args  Arguments, base directory
     */
    public static void main(String[] args) {
        if (args.length == 0) {
            System.out.println("Android Resource Scan by Kevin Peck");
            System.out.println();
            System.out.println("Parse the Android generated R.java file then look");
            System.out.println("for orphan references - any defined IDs that don't");
            System.out.println("appear in a JAVA or XML file in same directory tree.");
            System.out.println("Comment lines are ignore in all files");
            System.out.println("NOTE: .svn directories are ignored.");
            
            System.out.println();
            System.out.println("Usage: AndroidResourceScan {base path}");
            return;
        }
        
        xmlMapping.add(new XmlMapping("@string/", "R.string."));
        xmlMapping.add(new XmlMapping("@drawable/", "R.drawable."));
        xmlMapping.add(new XmlMapping("@color/", "R.color."));
        xmlMapping.add(new XmlMapping("@style/", "R.style."));
        xmlMapping.add(new XmlMapping("@id/", "R.id."));
        xmlMapping.add(new XmlMapping("@+id/", "R.id."));
        xmlMapping.add(new XmlMapping("@anim/", "R.anim."));
        xmlMapping.add(new XmlMapping("style name=\"", "R.style."));
        xmlMapping.add(new XmlMapping("<attr name=\"", "R.attr."));
        System.out.println("Scanning " + args[0] + " for R.java file");
        buildDirTree(args[0]);
        
        boolean haveResFile = false;
        File resFile = null;
        for (String dirName : directories) {
            resFile = new File(dirName + RESOURCE_FILE);
            if (resFile.exists()) {
                haveResFile = true;
                break;
            }
        }
        
        if (!haveResFile || (resFile == null)) {
            System.out.println("Unable to find " + RESOURCE_FILE + " in " + args[0] + " directory tree");
            return;
        }
        System.out.println("Found resource file " + resFile.getAbsolutePath());
        buildRefList(resFile.getAbsolutePath());
        
        System.out.println("Scanning JAVA files for references");
        scanFiles();
        
        int orphanCount = 0;
        System.out.println("Orphans found");
        for (RefInfo refInfo : references) {
            if (refInfo.refCount == 0) {
                orphanCount++;
                System.out.println(refInfo.name);
            }
        }
        System.out.println("total " + orphanCount);
    }
    
    /**
     * Scan the directory tree .java and .XML files 
     * Send them to proper parser as found
     */
    private static void scanFiles() {
        for (String dir : directories) {
            File fDir = new File(dir);
            String [] files = fDir.list();
            for (String fileName : files) {
                if (fileName.endsWith(".java")) {
                    scanJavaFile(dir + '/' + fileName);
                } else if (fileName.endsWith(".xml")) {
                    scanXmlFile(dir + '/' + fileName);
                }
            }
        }
    }
    
    /**
     * Open a java file and scan it for R. references
     *  
     * @param fileName
     */
    private static void scanJavaFile(String fileName) {
        BufferedReader reader;
        try {
            reader = new BufferedReader(new FileReader(fileName));
            String line;
            boolean inComment = false;
            
            while ((line = reader.readLine()) != null) {
                line = line.trim();
                if (inComment && line.endsWith("*/")) {
                    inComment = false;
                } else if (!inComment && line.startsWith("/*")) {
                    inComment = true;
                } else if (!line.startsWith("//")) {
                    int iStart = line.indexOf("R.");
                    while (iStart != -1) {
                        if (iStart != 0 && " (=".indexOf(line.charAt(iStart - 1)) != -1) {
                            int iEnd = iStart;
                            while (iEnd < line.length() && " ,);".indexOf(line.charAt(iEnd)) == -1) {
                                iEnd++;
                            }
                            String refName = line.substring(iStart, iEnd);
                            for (RefInfo refInfo : references) {
                                if (refInfo.name.equals(refName)) {
                                    refInfo.refCount++;
                                    break;
                                }
                            }
                        }
                        iStart = line.indexOf("R.", iStart + 1);
                    }
                }
            }
            reader.close();
        } catch (IOException exp) {
            exp.printStackTrace();
        }
    }
    
    /**
     * Scan XML file looking for XML mapped entries
     * 
     * @param fileName  Name of file to scan
     */
    private static void scanXmlFile(String fileName) {
        BufferedReader reader;
        try {
            reader = new BufferedReader(new FileReader(fileName));
            String line;
            
            while ((line = reader.readLine()) != null) {
                line = line.trim();
                if (!line.startsWith("<!--")) {
                    for (XmlMapping xmlMap : xmlMapping) {
                        int iStart = line.indexOf(xmlMap.xmlName);
                        if (iStart != -1) {
                            int iEnd = iStart + xmlMap.xmlName.length();
                            while (iEnd < line.length() && " \"><".indexOf(line.charAt(iEnd)) == -1) {
                                iEnd++;
                            }
                            String refName = xmlMap.resName + line.substring(iStart + xmlMap.xmlName.length(), iEnd).trim();
                            for (RefInfo refInfo : references) {
                                if (refInfo.name.equals(refName)) {
                                    refInfo.refCount++;
                                    break;
                                }
                            }
                        }
                    }
                }
            }
            reader.close();
        } catch (IOException exp) {
            exp.printStackTrace();
        }
    }

    /**
     * Build directory tree based on a root node
     *
     * @param baseDir  Root node to scan for sub directories
     */
    private static void buildDirTree(String baseDir) {
        String topDir = baseDir;
        if (!topDir.endsWith("/") && !topDir.endsWith("\\")) {
            topDir += "/";
        }
        File fDir = new File(topDir);
        if (fDir.isDirectory()) {
            directories.add(topDir.replace("//", "/"));
            String [] files = fDir.list();
            for (String fileName : files) {
                String subDir = topDir + '/' + fileName;
                File testDir = new File(subDir);
                if (testDir.isDirectory() && !subDir.contains("/.svn")) {
                    buildDirTree(subDir);
                }
            }
        }
    }
    
    /**
     * Build a list of public static final class references found in given file
     * 
     * @param fileName  Name of file to process ({path}/R.java)
     */
    private static void buildRefList(String fileName) {
        BufferedReader reader;
        try {
            reader = new BufferedReader(new FileReader(fileName));
            String line;
            String className = "";
            boolean inComment = false;
            boolean inClass = false;
            
            while ((line = reader.readLine()) != null) {
                line = line.trim();
                if (inComment && line.endsWith("*/")) {
                    inComment = false;
                } else if (!inComment && line.startsWith("/*")) {
                    inComment = true;
                } else if (!line.startsWith("//")) {
                    if (!inClass && line.startsWith(CLASS_START)) {
                        className = line.substring(CLASS_START.length()).trim();
                        if (className.endsWith("{")) {
                            className = className.substring(0, className.length() - 2);
                        }
                        inClass = true;
                    } else if (inClass) {
                        if (line.startsWith(ITEM_START)) {
                            int nameEnd = line.lastIndexOf('=');
                            if (nameEnd != -1) {
                                references.add(new RefInfo("R." + className + '.' + line.substring(ITEM_START.length() + 1, nameEnd)));
                            }
                        }
                        if (line.endsWith("}")) {
                            inClass = false;
                        }
                    }
                }
            }
            reader.close();
        } catch (IOException exp) {
            exp.printStackTrace();
        }
    }
    
    /**
     * XML to Resource format mapping 
     */
    static class XmlMapping {
        public String xmlName;
        public String resName;
        
        public XmlMapping(String xmlName, String resName) {
            this.xmlName = xmlName;
            this.resName = resName;
        }
    }

    /**
     * Resource name and reference count (all we care about is count != 0) 
     */
    static class RefInfo {
        public String name;
        public int refCount;
        
        public RefInfo(String name) {
            this.name = name;
        }
    }
}

Feel free to report any bugs you find back to me.

Wednesday, December 7, 2011

New machine built - data xfer time

I built a new machine as an early Christmas present for both my son and I over the past couple of days. My son helped me build it which was a cool experience for him. He is getting my current machine as a nice upgrade. His older AMD machine has been having all sorts of issues from not wanting to boot to booting but then not seeing the NIC or having the sound be bad. It could be a flaky power supply but I was due an upgrade anyway. Since I program for a living a 5 year old machine running a 32bit OS was starting to show its age.

It was a fun experience for him. I have a feeling we are not very far off from never building again so he may never use these skills. I looked for a pre-built box just to avoid the potential build headaches but could not find anything set up the way I wanted for a good price. If the price was good it was some crappy brand. If the parts were good it was $350-$500 more than the parts. I also drive a manual transmission. Cars and computers are like my coding - closer to the bare metal that many like to go. I write custom controls and never shy away from writing gut level code.

Here is the configuration

Intel Core i5 2500K
Thermaltake TR2 RX 750W Modular Power Supply
Samsung 22X DVD‘RW Burner with Dual Layer Support
Gigabyte GA-Z68XP-UD3 LGA 1155 Z68 ATX Intel Motherboard
Sentey Extreme Division Optimus GS-6000R Mid Tower
OCZ Technology Vertex 3 Series VTX3-25SAT3-120G
Zotac ZT-50303-10M GTX 560 Ti Overclocked 1024MB
ASUS Xonar DG 5.1 PCI Sound Card
Corsair 8GB DDR3-1600 (PC3-12800)

I went i5 instead of i7 after seeing in various websites that the i5 is a better gaming CPU due the hyperthreading getting in the way on the i7. I am not one of those "must have the top of everything" guys. I want a system that is stable and comes at a reasonable price. This is also why I did not get the $600 video card or the $1,200 dual video card set up. I bought all of it at the local MicroCenter. I normally order off NewEgg but they had a very limited processor selection and the price was higher than just picking it up local. NewEgg were cheaper on the sound card and DVD player but only by a few bucks. I just wanted to get it all at the same time ready to build.

I did go with a sound card even though the MB has built in sound. I have never been super impressed with motherboard sound and research shows the ASUS $30 to be a nice upgrade for a really good price. It is the 5.1 card, not 7.1, but I have a 5.1 setup I am happy with so it was the proper fit.

The SSD was a bit of a splurge. I have SSD at work and it is so nice to boot quickly and move right along after that. I am also moving over a normal HD to this setup to store music, images, etc. The case has a ton of fans but they are all pretty large and spinning slowly making the setup very quiet. The sound of a HD spinning is so familiar this just seems like it is not working at all. Guess I will even hear more of my game sounds when I get the chance to play.

Once it burns in for a few days I will fill out all the rebate paperwork and get that sent off. I hate rebates but I do have $100 coming back to me so it will be worth filling it out. I also need to download Assassin's Creed Brotherhood which I got for free with the video card.

I installed Win7 Ultimate 64bit and the Windows Experience comes to 7.5 with 7.8 being the highest and everything pretty much in that zone. Install was quick from DVD to SSD and boot time is faster than any machine I have ever used.

The build had two difficult areas. One was getting the stinking CPU cooling fan to seat properly. Seems I could get 3 corners in and have one pop out. The second was getting it to boot. Initially fans would kick on, single beep from post and then no video. I tried all sorts of things and gave up for the night thus making this a two day build. Finally used a screwdriver to short the CMOS jumper and then it booted. Not sure what in CMOS / BIOS was so annoyed it needed a reset especially since this is a brand new board and setup but that worked.

I turned off on-board sound in BIOS then watched the CPU health for a bit. CPU temp kept climbing so I shut it down and reseated the CPU fan again. Fired it up and CPU stayed at 45C for 20 minutes so I was happy and did the OS install.

I copied various files over from my server, installed updated video, sound and MB drivers along with Java and Eclipse. I still have a ton of other work to do to get it all in shape. I need to pull the HD out of my son's original machine and put it in my old machine so he can copy over files as he needs them. He will get to reinstall some of his games too. I did a backup of my Steam games and will move that over.

The new MB does not have IDE on it and I was planning on moving my secondary IDE drive into this machine. Guess I need a IDE / SATA adapter to do that. Right now all HD prices are up due to flooding so I hate to buy a SATA drive when I have usable drives to store music and what not. They might not be super new, fast and shiny but they spin up and hold data. I have access to what I need on our shared server drive so it is not a critical item but I hate to get in and out of the case too many times. I need to go in again to route power supply cables in a cleaner manner at the very least.

Going to be a number of days / weeks before I figure out everything I am missing. Always some utility you use rarely but need from time to time. Since my old machine is just going to my son and we are not reformatting it I will be able to move across anything I need with ease. That machine has dual HD right now so I will copy over everything I can think of from one drive to the other and put that drive in my new machine then put his old drive in there as his D: drive so he has access to his stuff.

Now I am ready to play some new games to see how this thing really works. I have Gears of War that I got for $20 a long time back. It always stuttered on the old box so I could never get into playing it. Now it should run just fine. Might be the first thing I install to prove this new beast was worth it.

Means I wont have much to open under the tree this year but really the holidays are all about watching other people open stuff you bought them. Shame I did not have the new setup over Thanksgiving when I got a second copy of Portal 2 during the Steam sale. My son and I played the co-op all day long finishing all levels. Near the end his machine would not show where the white goo was on the level so he could not place portals. My machine did things just fine. Could be a reboot of his machine would have kicked it in, could be his flaky machine would have never worked or his video card was just too old or memory shy to every handle it. He is so ready to play single player Portal 2 on a "real" machine that getting him to do homework is going to be rough. When daddy gets a new machine everyone wins.

* UPDATE *
Did a ton of data transfer last night. I now have my backed up Steam games installed (overnight process) and am downloading my free copy of Assassin's Creed Brotherhood today as it refused to accept $0.00 payment last night but did allow it this morning following exact same steps.

I have my son's old IDE drive in his new / my old machine and I copied over all his documents to the C: drive. As he reinstalls games we will move over the save game data from that drive and eventually we will remove it from the system as it is a waste of power to spin it up. I set up a new account for him and left my account so I can copy up to the network if I would happen to need something I forgot over there.

There were two IDE DVD drives in the case but only one is set to run right now as I stole the other IDE channel for the old HD. He is running without a burner at this point but with a slot load reader. Lets him install games but I need to get the burner back in place so he can burn videos and other school projects in the future. Of course we can burn them from the server, my wife's laptop, my machine or my other son's laptop meaning it is a hassle but not a big deal.

I installed Gears of War. It would not run saying I had a modified EXE. I installed the patch and it ran but it will not let me leave the video set up screen. I assume it is a Win7 64bit issue but gave up and went to bed instead of trying over and over. I will try a Steam game tonight but the ones I have use the old source engine so I doubt they show off the graphics card. I need to break down and by Skyrim but I really want it to go on sale. I have never paid $60 for a game and hate to start now.

Thursday, December 1, 2011

Android Out of Memory issues

The dreaded "Out of Memory error" with corresponding "Force Close" on your Android device is very frustrating. It was happening to our users from time to time but today I think I may have found the issue. I won't know for sure if this is a true fix until it is out in the field.

I have a couple of ways to acquire images in our app - you can take a picture with the camera or grab an existing image from the gallery. In either case I have to check out the image size and shrink it down to less than 1 meg of data via scaling, quality compression or both. It has to be less than 1 meg as I am holding the images in a SQLite blob to be sent to the server at a later time. I can't leave things in the gallery due to HIPAA rules and I can't trust the user to not delete the original image between pick it and transmit it time frames.

You can also view the image at any time within my app using a viewer I wrote that allows pinch zoom, drag scrolling, rotation and fit to window functionality. This is where things started to go awry.

If I opened and existing image, exited that activity then tried to capture an image from the camera there was a good chance I would get the out of memory issue. If I just took pictures but never viewed them or waited some amount of time before viewing them it might work just fine. This also depended on the device and version of the OS I was using. To find the issue I opened an image in the viewer then closed the viewer and did a heap dump via DDMS in Eclipse, converted that dump via hprof-conv in the android sdk\tools directory and loaded it up in MAT (memory analysis tool) under Eclipse. The Dominator Tree showed the ImageView of the closed activity to still be holding on to a weak referenced large bitmap. I guess the reference is not weak enough for the GC to kick it out of memory during the camera processing even though I was calling System.gc() before I started the camera bitmap manipulation.

To solve the issue I added the following to the image viewing activity:


/**
 * On the way out dump our images
 */
@Override
protected void onDestroy() {
  imageView.setOnTouchListener(null);
  imageView.setImageBitmap(null);
  System.gc();
  super.onDestroy();
}


Now after hitting the back button from the image activity I don't have a large weak reference to an image floating about in the heap. I have been able to view, take pictures, view, lather, rinse, repeat without any issues. I was running these test on a ASUS Transformer tablet running OS 3.2.1

I am also making sure I call recycle on the bitmaps I use during my scale / quality reduction routines during the post camera processing.

Probably a good idea to manually release any large images you use in your application as activities go away to avoid these problems. Right now I am not releasing all the smaller images I am using but it might be a good idea to make a sweep through the heap dump to see what else is sneaking around.

Not sure if the System.gc() call is even necessary and I know they recommend you don't call it but at this point I can't see it hurting anything and I am only doing it on activity destroy so I am leaving it in there for good luck and hoping I don't hear of any more OOM crashes in the field.

Wednesday, November 30, 2011

iOS Scope Bar for searches

I was cheating a bit for searching in our app. There are two columns in many of the tables - Code and Description. Most of the time the Doctors would search on the code but occasionally they want to search on description to find something like "liver abscess". To keep screen space at a premium I had the hint text tell you to type a leading SPACE to search by description. Of course that is not totally obvious and many don't read the text.

When I hit the tablet side of things I did two search fields on the Android like this:

Code: [           ]      Description: [                                   ]  [x]

You could fill in one or the other and I blank the one you are not using. Works nicely and there is plenty of horizontal space in either portrait or landscape to pull that off. Codes are small so that entry field eats up less horizontal space.

On the iOS side I am using the UISearchBar and it supports only one entry field. I was going to give up on doing the dual search on the iPad but then I discovered the scopeBar and associated methods on UISearchBar. You can set various button titles and that allows the user to toggle the type of search they are doing.

It works and I have converted the code to use it but there is a bit of oddness to it. First the toggle buttons always appear below the search bar. It would be nice to move the buttons to the right of the search bar on the iPad as I only have two buttons in most cases and plenty of screen space. Might as well show more search results. This makes for some very long toggle buttons on the iPad.

Second you can toggle the search mode then tap to type the text but once you do that the toggle buttons are disabled. You have to cancel the search, change the toggle button, restart typing your search if you entered in the wrong mode. Seems this is all one control so why disable part of it?

To save space on the iPhone it would be nice if the toggle buttons only appeared after you invoked the typing of search text. Right now they waste a lot of screen space on that small screen for the ability to toggle which is probably not used very often.

Finally there is only one keyboard mode for the entire search bar. I want it to be in numeric mode if they are doing a Code search and in alpha mode if doing a Description search but I can't do that. If my search involves Code / Description instead of Last Name / First Name I start in numeric mode but they have to manually toggle back to alpha mode to type description. I covered the 95% case here but wish I could cover 100% instead.

While initially I was happy with Apple for providing a generic search control I ended up frustrated due to its lack of flexibility. On the Android I started out thinking "Where is a generic search widget?" Implementing one was easy enough but I should not have needed to do that. I ended up with an exact layout for my needs and more flexibility in the end under Android. Typical programming issue that occurs when you are developing on multiple platforms.

Tuesday, November 29, 2011

Xcode is still crappy

I keep crashing Xcode. I have an M file open and intellisense pops up. I do the three finger swipe to switch to the H file and it crashes. I lose the work since my last save. This happens at least once a week. So stop doing that the Doctor says. I switch between my PC using Eclipse and Xcode on the MacBook Pro multiple times per day.

I don't remember crashing Eclipse in a long time. I just use the IDE and it works. Then I get on the Mac to convert over some Android code I just wrote and I forget it crashes and boom I crash it yet again. Would be nice for Apple to fix this one but I have doubts it will happen as Xcode does not get updated very often. Sure they tie in a new iOS release but the IDE itself does not change much.

Just rebooted the Mac after an update install and crash, did not notice update was ready until Xcode died. Time to go back over and see how much of my work it lost this time.

Wednesday, November 23, 2011

iOS 5.01 and WiFi - not happy

The iPad 2 I use for development and testing has been acting flaky. It is WiFi only and the connection comes at goes at will. I sit about 12' away from the wireless router. I had no issues at all until the 5.0x updates but since then it has been iffy at best. Searching the web and I found a lot of other people having the same issue. The Android tablet, also WiFi only, has had no issues.

On another Apple note the Java 1.6 update 29 caused us all sorts of memory leaks. Our clients could run our Java program on the Mac for about 15 minutes before running out of memory. No issues on the PC or Linux with update 29, just on the Mac. My boss dug into their bug fix and fixed their fix to release the objects properly and we are back in business. We checked in our overridden code to our project so our users are not hosed. Others on the web are complaining about this issue too.

I am also able to crash Xcode 4 at will. Edit M file, have intellisense / code complete / whatever they call it pop-up, three finger swipe to got to the H file and crash city. I have used the reporting tool to send this off to Apple. Happens to me at least once a week. They tend not to release fixes to Xcode unless they are doing a new iOS release so I bet this is not fixed in the near future.

Low hopes of the Java bug being fixed soon either. It has been a long time for a Java update - going from 26 to 29 - so I bet this hangs out there for a long time too. We don't find Apple to be all that developer friendly so you just suffer along and know how to work around things.

Winding down the round of mobile app development. I pretty much have the two in sync again. Pushing a gig of code on both sides. I added long press to do some default actions. My graphic artist / Mac buddies find most users have no idea a long press exists so I made sure there is a way to get to same functionality via another part of the UI. I just consider the long press a short cut but once you get used to it being there you really like it.

User beta feedback has been very positive. Almost no bug reports, just feature requests. Some of the features make sense and I have been able to quickly add them. Some are borderline insane so they are on a "do it later if ever" list.

I sat down and rethought the way I was doing favorites and got it working the same on both iOS and Android. Pretty happy with the final results and ready to see what the beta crowd thinks of it. I have also made some graphical tweaks to the new menu system to make it look better.

I cleaned up some bugs in the iToast code I got off the web to do Toast notifications on the iPhone. If I set the gravity to top or bottom it was not painting correctly in landscape mode - the mode I generally have the iPad in. I kind of cheated here. The Android Toast code handles multiple Toasts by queuing them up and showing them one at a time. The iOS version just painted them on top of each other. I was going to write the queue code and then found I could just set the screen position instead so I have the refresh message show up on the bottom of the screen and other messages in the middle. Avoids the overlap without a ton of work on my part. This only happens in one situation where you save a case and are coming back to the case list screen. One message is saying "Case Saved" and another "Case List Refreshed". No need to introduce complexity to solve one off needs.

More than ready for the Thanksgiving break. Thus all the polish work for this short week. Might as well leave it in a very stable state so I don't think about it over vacation. I ran the memory leak detection of Xcode and cleaned up all the issues I found. Always good to run that from time to time.

Wednesday, November 16, 2011

Android / iOS sync continues

I sure thought I would be done with the next version of our application by this time but we came up with a number of new features to add before it leaves the Beta stage. I have been adding support for favorites so the Dr. can toggle which CPT / ASA / Diagnosis and other fields quickly just using the ones they use the most.

One of the benefits of dual development is it forces you to look at problems from different angles. I had the favorites all set up on the Android side of things and the Beta clients for those devices have been using it and it appears liking it. What I did on the Android does not fit naturally on the iPhone as far as UI goes.

First off I had to take some time to write a new menu system for iOS to emulate the one I am using for Android. After getting past the initial scare of creating a full blown custom menu under iOS it went rather smoothly. My draft version proved I could do a menu that pops up over another view - including tables - but the graphics design did not pan out well on an iPhone, it looked fine on the iPad. I took a step back and redesigned the menu GUI so it worked great on both device form factors in both orientations. This is a universal binary so the same code is running on both devices.

Many tweaks later I have the menu system running in a lot of places in the UI. Without that I was not going to be able to pull of the favorites as designed. Here is where I ran into the next issue. I was using my menu in two ways on the Android to do favorites, one menu to select a mode and one menu to select which items to be in the favorites list. This would not play well under iOS. I also had allow the user to control the font size under iOS and its current UI left something to be desired. After taking a step back, many head scratches and many pen scratches on paper I came up with a new single menu system that is much cleaner and easier for the user to grasp and solved the font sizing issue too. I just finished phase one of my implementation on iOS and am pretty happy with it. Now I have to convert those ideas back over to the Android which technically should not be difficult but will be time consuming as I have to touch a lot of code.

Because I needed to make something work on the iPhone my Android users will have a better experience. This has happened in both directions as there are areas I wrote on the iPhone first and during my Android conversion I discovered better ways to accomplish the same thing. Some of it comes from just wanting to get something running but when you have to write the code a second time you think it through again and make it better. That has happened over and over during this process. I have deleted, modified and bug fixed a lot of code as I go back and forth between the two platforms. At times it feels like you are just spinning your wheels and will never get it all done. Heck it is not all done but I am in the home stretch.

I often wonder what it would be like if there was another developer working on iOS. Would we be able to come up with ideas together that work well on both devices? If you don't code on both do you have blinders on for the one you don't code under? Is it better for one developer to hash out an idea and get a working prototype and then show it to the other so they can shoot holes in it and make it better? You could have each developer tackle a non-overlapping area then swap ideas. Working on the same area at the same time, unless you are following a foregone UI pattern, seems a waste of effort.

There are a lot of "Oh man, I have to write this AGAIN!" moments when you are the sole mobile developer. At times you dread writing a tough hunk of code in another language using another IDE. Not sure if I am sold on the universal binary idea for iOS. Sure it beats a 2x version of your iPhone app on the iPad but Xcode / Interface Builder don't make is very easy to have NIB files per platform. It is easy to do on Android, just use same named XML files in different directories and everything is handled for you. I have a lot of IF checks for the iPad in the code already and am not looking forward to more of them. We might be getting close to the point of doing dual development there which would make my work load even more insane.