Month: January 2014

Saving and Deleting images via PhoneGap’s File API

The PhoneGap File API is a bit confusing to me and quite a few other people. I spent a fair amount of time looking for some “plain English” explanations and examples of storing images  but most tutorials and blog posts covered text files only. That’s great when you need to append or edit a data file but I needed to save images captured on an iPhone. It took a day of wrangling but I finally got my head wrapped around the basic API. Spent the next day building a set of methods. Let’s take a look.

I’m using PhoneGap (Cordova) 3.3 for this demo. It’s the latest stable build at the time of this writing. Seeing as PhoneGap will always be playing catch up with any changes to iOS it’s pretty important to check the docs and see if there’s been any major changes.

The Objective
We want to hit a button in the app and access the iOS photo gallery. From there we can navigate the gallery, edit a selected image, and then associate that image with a record. Pretty standard stuff for apps like Contacts, etc. This use case will need to use two of of the main API’s, Camera and File.

The Restrictions
When you call the Apple Photos App from your application the selected image file is stored in the host application’s tmp directory. This gives you a path to the file that looks something like this when you run the app in the simulator.

file:///Users/[name]/Library/Application%20Support/iPhone%20Simulator/7.0.3-64/Applications/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/tmp/cdv_photo_001.jpg.

Note the “tmp” part. That means that the file is now stored in a directory that is erased fairly often by iOS or by direct user action. If you have a reference to that file in a src attribute and the directory has been emptied to clear memory then the image breaks because it is nonexistent. When the user chooses an image we need to move (not copy) it to a persistent directory, grab the new URI, then store that new URI in the database. I say not copy because we’re looking to save drive space so it’s nice to move the file to a new location instead of creating a double.

The Code
The Phonegap best practice is to fire off your own code within the DOM ready event so we’ll start with that. When the event is fired it will log the event then run the requestFileSystem() method on line 13. The details are covered through comments.

// deviceready Event Handler
    //
    // The scope of 'this' is the event. In order to call the 'receivedEvent'
    // function, we must explicity call 'app.receivedEvent(...);'
    onDeviceReady: function() {
        app.receivedEvent('deviceready');
    },
    // Update DOM on a Received Event
    receivedEvent: function(id) {
        console.log('Received Event: ' + id);
        // request a new file system object then pass that to the gotFS() method.
        // gotFS() is a simple setter for a global var that the FileIO methods will use later.
        window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, FileIO.gotFS, FileIO.errorHandler);
    }

The second code snippet is directed to the Camera API and has three simple methods. Line 1 assigns a JQuery event handler to our button. In this case it’s been given a class of “.thumbnail-button”. When the DOM element is clicked it will execute the getPhoto() method defined on line 5. Passing the (pictureSource) parameter into the method makes the call more flexible. We’re using (pictureSource.PHOTOLIBRARY) to go directly to the iOS photo gallery and choose a photo. Had the objective been to take a new photo then we would pass (pictureSource.CAMERA) which opens the Apple Camera App instead of the Apple Photos App.

The getPhoto() method has a single call inside of it, navigator.camera.getPicture(), which takes three arguments; success callback, failure callback, and optional parameters.

I won’t get into the optional parameter set of the navigator.camera.getPicture() method because they should be fairly self explanatory. I would like to call attention to the allowEdit : true on line 11. This makes sure that we get a an image cropped to a perfect square by forcing the user into the edit mode after they tap on an image within the photo gallery and gives the user a chance to really zoom in on an area. Since we’re using these images for thumbnails we want square images with clear faces.

If the image is successfully saved the URI is passed to the onPhotoURISuccess() method on line 15 which passes that URI over to the FileIO portion of the task. We’re halfway done!


$(".thumbnail-button").on('click', function() {
getPhoto(pictureSource.PHOTOLIBRARY);
});

function getPhoto(source) {
    // Retrieve image file location from specified source
    navigator.camera.getPicture(onPhotoURISuccess, onFail, { quality: 50,
                                destinationType: destinationType.FILE_URI,
                                saveToPhotoAlbum: false,
                                sourceType: source,
                                allowEdit: true });
}

// Called when a photo is successfully retrieved
function onPhotoURISuccess(imageURI) {
    FileIO.updateCameraImages(imageURI);
}

// Called if something bad happens.
function onFail(message) {
    console.log('Failed because: ' + message);
}

Now we’ve got a new image sitting in the app’s tmp directory waiting for us to move it to a persistent location. We’ve taken the file URI of that image file and passed it over to another set of methods in the FIleIO name space. I’ve added comments to run through the sequence.

// set some globals
var gImageURI = '';
var gFileSystem = {};

var FileIO = {

// sets the filesystem to the global var gFileSystem
 gotFS : function(fileSystem) {
      gFileSystem = fileSystem;
 },

// pickup the URI from the Camera edit and assign it to the global var gImageURI
// create a filesystem object called a 'file entry' based on the image URI
// pass that file entry over to gotImageURI()
updateCameraImages : function(imageURI) {
        gImageURI = imageURI;
        window.resolveLocalFileSystemURI(imageURI, FileIO.gotImageURI, FileIO.errorHandler);
    },

// pickup the file entry, rename it, and move the file to the app's root directory.
// on success run the movedImageSuccess() method
 gotImageURI : function(fileEntry) {
       var newName = "thumbnail_" + gCurrentFlo + ".jpg";
       fileEntry.moveTo(gFileSystem.root, newName, FileIO.movedImageSuccess, FileIO.errorHandler);
 },

// send the full URI of the moved image to the updateImageSrc() method which does some DOM manipulation
 movedImageSuccess : function(fileEntry) {
      updateImageSrc(fileEntry.fullPath);
 },

// get a new file entry for the moved image when the user hits the delete button
// pass the file entry to removeFile()
 removeDeletedImage : function(imageURI){
      window.resolveLocalFileSystemURI(imageURI, FileIO.removeFile, FileIO.errorHandler);
 },

// delete the file
 removeFile : function(fileEntry){
      fileEntry.remove();
 },

// simple error handler
 errorHandler : function(e) {
       var msg = '';
       switch (e.code) {
       case FileError.QUOTA_EXCEEDED_ERR:
               msg = 'QUOTA_EXCEEDED_ERR';
               break;
        case FileError.NOT_FOUND_ERR:
               msg = 'NOT_FOUND_ERR';
               break;
        case FileError.SECURITY_ERR:
               msg = 'SECURITY_ERR';
               break;
        case FileError.INVALID_MODIFICATION_ERR:
               msg = 'INVALID_MODIFICATION_ERR';
               break;
        case FileError.INVALID_STATE_ERR:
               msg = 'INVALID_STATE_ERR';
               break;
        default:
               msg = e.code;
        break;
 };
       console.log('Error: ' + msg);
 }
}
Advertisements

Refactoring the Flo methods to OOP

I had an interview yesterday that was a disaster. For whatever reason I totally blanked on basic OOP in Javascript… deer in headlights. So I decided to refresh my brain by converting the Flojuggler methods to an OOP class paradigm. The original was just name-spaced.

Original Flo file:

Flos = {
    
    helloWorld : function(){
        return ("Hello flos!");
    },

    deleteFlo : function(id) {
        Model.deleteFlo(id);
    },

    initFlos : function(tx,rs,test) {
         currentFlo = 'reset';
        globalFlos = [];
        if (rs.rows.length >= 1) {
            for(i=0; i < rs.rows.length; i++) {
                var row = rs.rows.item(i);
                globalFlos.push(row);
            }
        }
        
        if (!test) {
            displayResultSet();
        }
        return globalFlos;
    },

    detectFlo : function(time,startDate,cycle){
        console.log(startDate);
        var myTime = new Date(time).getTime();
        var myDate = new Date(startDate).getTime();
        var status = ( secToDays(myTime) - secToDays(myDate) ) % (cycle);
        return status;
    },

    getStatus : function(time,startDate,cycle,long) {
        var flo = Flos.detectFlo(time,startDate,cycle);
        var sDayDisplay = " days";
        var floStatus = {};
        
        if (flo < long) {
            var days = long - flo;
            days <= 1 ? sDayDisplay = " day" : sDayDisplay = " days";
            floStatus.code = 'status-red';
            floStatus.text = 'on her flo for ' + days + ' more' + sDayDisplay;
        } else {
            days = cycle - flo;
            days <= 1 ? sDayDisplay = " day" : sDayDisplay = " days";
            if (days <= 3) {
                floStatus.code = "status-yellow";
                floStatus.text = days + ' more' + sDayDisplay + ' to her flo';
            } else {
                floStatus.code = "status-green";
                floStatus.text = days + sDayDisplay + ' from her next flo';
            }
            
        }
        return floStatus;
    }
}

OOP Flo with Hungarian notation (just to be extra obnoxious):


function Flo() {
		this.name = 'Unnamed';
		this.cycle = 30;
		this.long = 7;
		this.startDate = new Date().getTime();
		this.thumbnail = "images/thumbnail.svg";
		this.state = 'new';
	}

	Flo.prototype.getStatus = function() {
		var statusDays = detectFlo(this);
		return getStatus(this, statusDays);
	};

	Flo.prototype.save = function() {
		Model.addFlo(this);
	};

	Flo.prototype.update = function() {
		Model.updateFlo(this);
	};

	Flo.prototype.delete = function() {
		Model.deleteFlo(this.id);
	};

	Flo.prototype.toString = function() {
		return this.name + ", " + this.cycle + ", " + this.long + ", " + this.startDate;
	};

	Flo.prototype.valueOf = function() {
		return detectFlo(this);
	};

	Flo.prototype.equals = function(e) {
		return (this.name == e.name && this.cycle == e.cycle && this.long == e.long && this.startDate == e.startDate);
	};

	function getStatus(oFlo, nStatusDays) {
		var that = oFlo;
		var oFloStatus = {};
		var nDays;
		var sDayDisplay;

		if (nStatusDays < that.long) {
			nDays = that.long - nStatusDays;
			nDays <= 1 ? sDayDisplay = " day" : sDayDisplay = " days";
			oFloStatus.code = 'status-red';
			oFloStatus.text = 'on her flo for ' + nDays + ' more' + sDayDisplay;
		} else {
			nDays = that.cycle - nStatusDays;
			nDays <= 1 ? sDayDisplay = " day" : sDayDisplay = " days";
			if (days <= 3) {
				oFloStatus.code = "status-yellow";
				oFloStatus.text = nDays + ' more' + sDayDisplay + ' to her flo';
			} else {
				oFloStatus.code = "status-green";
				oFloStatus.text = nDays + sDayDisplay + ' from her next flo';
			}            
		}

		return oFloStatus;
	}

	var gCurrentFlo = new Flo();
	var gTotalFlos = [];

	function initFlos(tx,rs,test) {
		gCurrentFlo.state = 'new';
        if (rs.rows.length >= 1) {
            for(i=0; i < rs.rows.length; i++) {
                gTotalFlos.push(rs.rows.item(i));
            }
        } 
        if (!test) {
            displayResultSet();
        }
        return gTotalFlos;
    }

	function detectFlo(oFlo) {
		var nNow = new Date().getTime();
		var nMyStartDate = new Date(oFlo.startDate).getTime();
		return ( secToDays(nNow) - secToDays(nMyStartDate) ) % (oFlo.cycle);
	}

	function secToDays(nTime) {
		var nDays = nTime/1000/60/60/24;
		return Math.round(nDays);
	}

	function helloWorld(){
        return ("Hello flos!");
    }

The new one is much more encapsulated and testable.

Redesigning Cloudforge with Lean UX

Cloudforge by CollabNetUI/UX Design and implementation, HTML, CSS, and Javascript all in a RoR & Bootstrap environment.

This was a complete redesign of the entire core application UI/UX. Collabnet Inc was looking to bring their Perl and custom JS based framework into the modern age of rapid application development. Strict SCRUM processes and agile processes.

This “green-field” project was a ton of fun. They had an existing product with thousands of users. Many of those users were major fortune 500 company accounts. We had to come up with a UI/UX that accommodated consumer, enterprise, and contractor user stories.

The end result was night and day compared to the original. Proud of the work we did here with a great team and an amazing manager.

Cloudforge was previously CVSDude

When we walked into the product Collabnet had already owned Codesion (formerly CVSDude) for a year. They had a Perl backend with a custom Javascript client. The assignment was to bring the entire product out of both of those technologies as much as possible, rebrand it, and create and platform that any dev could work in and extend. The lead developer chose Ruby on Rails and while he started building the foundational application I got to work with a hired design firm.

Google ChromeScreenSnapz005Six months later we landed here. The designs from the firm had been dropped, we committed to the Bootstrap look and feel after three full explorations, and most of the application was off of the older platform. We had a new name, a new brand, and new customers.

From a UI/UX stand point the idea was to pull a more consumer experience into the development tool (think what Github did for a terminal tool) without losing credibility with our engineering based customers. We had thousands of existing users that didn’t love the old UI but did know it. We needed to pull them into a new paradigm without shocking them.

I  moved in the more modern approaches to UI, especially mobile paradigms. I got rid of as many checkboxes and drop downs as possible. We needed to step out of the 90’s. While the Bootstrap library was an outside restriction, it did provide us with a great excuse to leave many established but antiquated elements on the cutting room floor. Luckily the team agreed and we ended up with a product that was a major step ahead for Collabnet. In fact, they soon began to adopt many of the patterns and UI elements into their flagship product, Teamforge.

While our visual styles came directly from Twitter Bootstrap, the design patterns on the UI widgets were designated in a living style guide. This allowed engineers to create without the need for a designer at every turn.

Learned skills.

The CloudForge project allowed me to explore some new tools; Bootstrap, JasmineJS, HighchartsJS, Balsamiq Mockups, and MindNode. This was also the first real Lean UX project that was handed to me. Personas, user stories, wireframes, all of it was learned in the fires of live sprints building a real product. I had one hell of a manager.

What would I do differently?

First thing I would suggest is moving into an MVVM framework like Angular or Backbone. I always thought that CVSDude had the right idea about decoupling the UI from the controller and model side but the custom JS… man, that was a monster. Now that the Perl side is gone it would be great to take the final step and move the presentation off of Rails and turn it into a Restful service accessible from mobile, web, etc.

The dashboard seems to be built more for pushing features than for quickly communicating relevant information. Current best practices suggest that dashboards should be more about giving the user information than for being springboards for or containers for actions, purchases, etc. I should have stood my ground more on this but two different product managers wanted more “bells and whistles” and insisted on designing the dashboard themselves.

I’d also pull out a lot of the PNG’s and start using SVG’s. I’ve noticed a lot of anti-aliasing issues as I’ve been revisiting the application after my exit. There’s lots of jaggies on the icons that come from resizing images outside of the 8bit scaling rules. SVG’s would allow them to use any size without worrying about such issues and start playing with low cost animations.

Project Detail 2.1

Project Detail 2.1 is an imagining of iterations on the UI/UX. It uses the updated Bootstrap lib, a more consumer grid layout, and updated iconic treatments.

While we pushed for a more flat design aesthetic, it just did not fit within the short term plan. It would have stood out against the other products in the CollabNet family so we stuck with the beveled look and feel for a lot of widgets.

It would have been nice to create a responsive layout but the owners decided not to focus on mobile. Shame given where all the growth in internet use is and this was really a “manager’s tool”.

Teamforge Orchestrate in Agile

Teamforge Orchestrate

Teamforge Orchestrate keeps track of every commit, build, and deployment a team makes and presents a human readable interface.

Lean UI/UX implementation in a Ruby on Rails front end within a strict SCRUM team environment.

The Orchestrate project brings a consumer grade UI to the automated test/deployment process. It’s a tool for admins and managers to monitor and react to the build process.

This was a six month project with a 10+ person team located in Bellvue, WA. I was brought on after the group realized the need for someone to sit between the lead designer, the product owner, and the rest of the engineering team. We would review and plan before he executed designs. Once they passed review I made them into reality.

Orchestrate Pipeline Builder

Teams can create their on “pipelines” which tell the system which steps are involved in a build or deployment.

The big takeaway for me on this project was the process itself. This was a team comprised of mostly Java engineers turned Ruby on Rails. The group ran a very tight form of Agile, unlike Cloudforge which brought me into the company. We ran scheduled nighty deployments along with automated Jenkins builds with every Git push. If you broke the build you knew it within 30 seconds when someone yelled your name from a dark corner. It was a much more transparent and structured environment and it slapped me into shape. I was forced to learn to write tests because I was forced to break tests as I made change requests.

Orchestrator became a plug in for the Teamforge product as opposed to the original plan of being stand alone. That decision really squelched a lot of the ideas that the team had for a really dynamic and current user interface. We ended up with a solid running application.

Orchestrate Detail View

Each node on the left represents a step in the deployment process. This includes commits, code reviews, build results, etc. There could be anywhere from one to hundreds of steps and the UI has to respond.

Building Myi… a few times

myiSome products are destined for glory. Some are destined for the scrap heap. Myi was one such product. The brainchild of Nominum Inc’s former CEO, the project saw a two year development cycle but was never launched. Regardless, it involved some top notch people across the spectrum of departments so I have to say… Myi was pretty cool. No, really!

Nominum is a DNS company. Some would argue that they are the DNS company as Paul Mockapetris, the inventor of the Domain Name System (DNS), is chief scientist and chairman. While they know a lot about providing large scale services to telecom providers they didn’t know much about creating consumer services. Myi was an attempt to package existing DNS abilities into something that the average household could relate to and benefit from. I still think the idea has legs.

nominum1

The user “dashboard” offered a styled interface for investigating, modifying, and purchasing apps.

The art director spun an unapologetically female aesthetic onto the brand. This product was being aimed dead center on educated affluent women with children, nicknamed Mominums. Come on! How is that not cute? Anyway, the undeniable success of the iTunes app store model drove us toward offering each service idea as an “app” that could be purchased a’ la carte. Need to block Facebook from 3pm to 8pm so junior can finish homework? Myi had an app for that.

I was the UX lead on this project which meant that I worked with the designer to come up with layouts and widgets then lead a small team to execute those designs. Honestly, these were all top notch talent. The work this team turned out was awesome. The thing is we kept turning it out… over and over.

The first was a fully functional prototype we built for the CEO to present at a conference. We locked ourselves in a room for three days and built an entire working front end in PHP. It was some serious start up action in there and was pretty damn fun.

nominum3

The UI featured a HTML5 carousel style selector which populated the “drawer” space below it. Not particularly scalable but it had a certain “wow” factor.

Then we built it again in Rails figuring that this would be the front end that would actually be used to connect to the supporting services. This process was beyond “agile” as features flew in and out like pigeons at the park. While it was argued that we have more of a restful service and make AJAX calls to a Rails API, we ended up just making a straight forward Rails view based application. It worked well.

Then we moved over to Codeigniter. I’ll be honest here. I have a secret love for PHP. We secretly meet in Summer and write letters during the rest of the year. I still love you, PHP!!!

Then we decided to throw a Javascript framework on top of that. Yes, I’m talking about the fourth version of the same exact application front end. It’s important to realize that at this time MVVC was just starting to infiltrate the UI/UX world and best practices had not yet been solidified. There was no JS template system yet so we ended up with a lot… and mean a lot… of data-xxx attributes all over the place so the app could pass data around.

Learned skills.

The Myi project allowed me to explore some new tools; CSS3, HTML5 Video, Web Fonts (@font-face, licensing process), Codeigniter, SASS, Agile Development.