<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>bdunagan</title>
	<atom:link href="http://www.bdunagan.com/feed/" rel="self" type="application/rss+xml" />
	<link>http://www.bdunagan.com</link>
	<description>fill the void</description>
	<lastBuildDate>Thu, 17 May 2012 14:07:53 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.5.1</generator>
		<item>
		<title>Writing Retrospect for iOS</title>
		<link>http://www.bdunagan.com/2012/05/03/writing-retrospect-for-ios/</link>
		<comments>http://www.bdunagan.com/2012/05/03/writing-retrospect-for-ios/#comments</comments>
		<pubDate>Thu, 03 May 2012 13:12:22 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[design]]></category>
		<category><![CDATA[ideas]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=2007</guid>
		<description><![CDATA[At WWDC 2009, Eric Hope talked about crafting the best iPhone apps at his iPhone User Interface Design session (the one where Cultured Code likened public speaking to kryptonite). The first step was coming up with an Application Definition Statement. It should define what your app is and what it isn&#8217;t. The statement should include [...]]]></description>
				<content:encoded><![CDATA[<p><img src="http://bdunagan.com/files/writing_retrotouch.png"/></p>
<p>At WWDC 2009, Eric Hope talked about crafting the best iPhone apps at his iPhone User Interface Design session (the one where Cultured Code likened public speaking to kryptonite). The first step was coming up with an Application Definition Statement. It should define what your app is and what it isn&#8217;t. The statement should include a differentiator (your app&#8217;s strength), a solution (the problem you&#8217;re solving), and an audience (the people who care), or as my Ruby-esque notes read, &#8220;#{differentiator} #{solution} for #{audience}&#8221;. The goal of the statement was to focus your app. Mac apps could have a long list of features, but the iPhone was a far more constrained environment. Hope&#8217;s example was iPhoto. On the Mac, it views, edits, shares, prints, and what not; on the iPhone, it shares, nothing else.</p>
<p>That talk was almost three years ago. Things have moved forward. The iOS environment is less constrained, particularly with the iPad. Apps can do more, and iPhoto is a great example. In March, Apple released an iPhoto for iOS that contained editing abilities.</p>
<p>Retrospect for iOS has followed a similar path. What started as a very simple app has grown up a bit.</p>
<h3>Retrospect for iOS 1.0</h3>
<p>I wrote Retrospect for iOS 1.0 in December 2009, just for fun. For the Application Definition Statement, I came up with &#8220;a serious tool to monitor multiple Retrospect servers&#8221;. No control, just monitoring. I focused on activities. Viewing activities for a Retrospect server gives administrators a high-level view into what&#8217;s going on. If everything looks good, no action required; if not, they should launch Retrospect for Mac to dig deeper. Our designer hand-crafted a set of awesome icons, and we submitted it to the App Store.</p>
<p><img src="http://bdunagan.com/files/Retrospect_for_iOS_1.0.png"/></p>
<p>The app was a moderate success. It was downloaded around 5,000 times and received a glowing review from Economist writer Glenn Fleishman (a long-time Retrospect user), who praised its &#8220;outstanding distillation&#8221;. The app didn&#8217;t do much beyond viewing activities, but it did that task well. Still, it didn&#8217;t replace Retrospect for Mac, and it didn&#8217;t support the iPad. Just as Apple updated its definition of what iPhoto for iOS was, I recently decided it was time for Retrospect for iOS 2.0.</p>
<h3>Retrospect for iOS 2.0</h3>
<p>What did the first version lack? Control. Complete monitoring. Better server support. iPad support. That&#8217;s the list I came up with in December 2011. I decided the app should display everything that the Mac version did: Activities, Past Backups, Scripts, Sources, Media Sets, Devices, and Reports. I even added badges to each report. I added full support for the latest Mac and Windows Retrospect servers and partial support for a couple versions before those. And I finally enabled control within the app: running scripts. I submitted v2.0 in February.</p>
<p><img src="http://bdunagan.com/files/Retrospect_for_iOS_2.0_iPad.png"/></p>
<p>Close but not quite. While most of the new features were significant improvements, one failure erased all of those gains: syncing. Retrospect stores quite a bit of information, and when I expanded the list of items displayed on the iOS app, it translated into syncing times that were 10x to 100x longer. The UI prevented the user from looking at a server until it was synced, so the user might launch the app but then wait a long time before being able to monitor the server. Moreover, the iPad UI did not take advantage of the space. It looked like an iPhone app at &#8220;2x&#8221; but without the pixelation.</p>
<h3>Retrospect for iOS 2.1</h3>
<p>With v2.1, I fixed those two issues. Syncing is now done by item type. Users can view activities before media sets finish syncing. I also added split-screen support to the iPad UI to make better use of the space available.</p>
<p><img src="http://bdunagan.com/files/Retrospect_for_iOS_2.1_iPad.png"/></p>
<p>This latest version of Retrospect for iOS is now available on the App Store. If you use Retrospect and have an iOS device, give it a try. It&#8217;s free.</p>
<p><a href="http://bit.ly/iosretro"><img src="http://www.bdunagan.com/files/appstore.png"/></a></p>
]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2012/05/03/writing-retrospect-for-ios/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>rescue_from RoutingError in Rails 3</title>
		<link>http://www.bdunagan.com/2012/04/27/rescue_from-routingerror-in-rails-3/</link>
		<comments>http://www.bdunagan.com/2012/04/27/rescue_from-routingerror-in-rails-3/#comments</comments>
		<pubDate>Fri, 27 Apr 2012 10:05:28 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=1992</guid>
		<description><![CDATA[Rails 3.0 handles routing errors (ActionController::RoutingError) differently than Rails 2.3. The middleware addition means the error is passed to ActionDispatch, instead of ApplicationController. A bit ago, I actually removed the catch-all route at the bottom of routes.rb because I thought the ApplicationController::rescue_from handled any routing errors. Not true. Rails 3.2 doesn&#8217;t resolved it either. There [...]]]></description>
				<content:encoded><![CDATA[<p><img style="float:left; padding-right:20px; padding-bottom:5px;" src="http://bdunagan.com/files/rails.png"/></p>
<p>Rails 3.0 handles routing errors (<tt>ActionController::RoutingError</tt>) differently than Rails 2.3. The middleware addition means the error is passed to <tt>ActionDispatch</tt>, instead of <tt>ApplicationController</tt>. A bit ago, I actually removed the catch-all route at the bottom of <tt>routes.rb</tt> because I thought the <tt>ApplicationController::rescue_from</tt> handled any routing errors. Not true. Rails 3.2 doesn&#8217;t resolved it either.</p>
<p>There is an open GitHub issue for it: <a href="https://github.com/rails/rails/issues/671">&#8220;Can no longer rescue_from ActionController::RoutingError&#8221;</a>. (In fact, it&#8217;s old enough to be <a href="https://rails.lighthouseapp.com/projects/8994/tickets/4444-can-no-longer-rescue_from-actioncontrollerroutingerror">an issue migrated from Lighthouse.</a>) There are several solutions posted inline in the issue&#8217;s comments. José Valim from the Rails core team suggests simply adding the catch-all route back to <tt>routes.rb</tt>; others suggest overriding <tt>ActionDispatch::ShowExceptions::render_exception</tt>. All seem focused on rendering the 404 page manually. However, I wanted to make <tt>rescue_from</tt> work. My solution is the catch-all route and raising the exception manually.</p>
<pre class="brush: ruby; title: ; notranslate">
# routes.rb

# Any other routes are handled here (as ActionDispatch prevents RoutingError from hitting ApplicationController::rescue_action).
match &quot;*path&quot;, :to =&gt; &quot;application#routing_error&quot;
</pre>
<pre class="brush: ruby; title: ; notranslate">
# application_controller.rb

rescue_from ActionController::RoutingError, :with =&gt; :render_not_found

def routing_error
  raise ActionController::RoutingError.new(params[:path])
end

def render_not_found
  render :template =&gt; &quot;misc/404&quot;
end
</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2012/04/27/rescue_from-routingerror-in-rails-3/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Creating UITableView badges like iOS Mail</title>
		<link>http://www.bdunagan.com/2011/12/14/creating-uitableview-badges-like-ios-mail/</link>
		<comments>http://www.bdunagan.com/2011/12/14/creating-uitableview-badges-like-ios-mail/#comments</comments>
		<pubDate>Wed, 14 Dec 2011 23:23:00 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=1966</guid>
		<description><![CDATA[Apple&#8217;s iOS Mail has a universal inbox on iOS 4. Very handy in general, but its unread badges complete the feature. I frequently glance down the list of email addresses I have and know exactly what the state of my inboxes are. I never have to wonder how many new emails I have. I just [...]]]></description>
				<content:encoded><![CDATA[<p><img style="float:left;padding-right:8px;" src="http://bdunagan.com/files/ios.mail.badges.outline.png"/></p>
<p>Apple&#8217;s iOS Mail has a universal inbox on iOS 4. Very handy in general, but its unread badges complete the feature. I frequently glance down the list of email addresses I have and know exactly what the state of my inboxes are. I never have to wonder how many new emails I have. I just check the numbers.</p>
<p>Recently, I needed to duplicate that feature for work. I found two existing solutions:</p>
<ul>
<li><a href="http://www.tuaw.com/2010/01/07/iphone-devsugar-simple-table-badges/">TDBadgedCell</a> (<a href="https://github.com/tmdvs/TDBadgedCell">GitHub repo</a>)</li>
<li><a href="http://digdog.tumblr.com/post/624498564/ddbadgeviewcell">DDBadgeViewCell</a> (<a href="https://github.com/digdog/DDBadgeViewCell">GitHub repo</a>)</li>
</ul>
<p>I ended up forking DDBadgeViewCell. It was concise, and I only needed to make a couple changes to it. Specifically, I wanted the badges to look exactly like Mail&#8217;s badges, in terms of alignment, colors, and shadows. I&#8217;ll go through my changes here, but you can check out <a href="https://github.com/bdunagan/DDBadgeViewCell">my GitHub fork</a> as well. See the image for the resulting look:</p>
<p><img src="http://bdunagan.com/files/DDBadgeViewCell.iphone.png"/></p>
<h3>Badge color</h3>
<p>I wanted to duplicate Mail&#8217;s look and feel, so I needed the exact colors that Mail uses for its badges. xScope made it easy.</p>
<pre class="brush: objc; title: ; notranslate">
UIColor *badgeColor = [UIColor colorWithRed:0.55 green:0.6 blue:0.69 alpha:1.];
UIColor *badgeShadowColor = [UIColor colorWithRed:0.45 green:0.49 blue:0.57 alpha:1.];
</pre>
<h3>Badge shadow</h3>
<p>If you look closely at Mail&#8217;s badges, you&#8217;ll notice the top part of the arc looks indented. The shadow gives the label a feeling of depth; the badge looks almost letter-pressed. To duplicate that look, I simply drew a darker oval one pixel higher than the original before drawing the real one.</p>
<pre class="brush: objc; title: ; notranslate">
// Draw the badge shadow.
CGContextSaveGState(context);
CGContextSetFillColorWithColor(context, currentBadgeShadowColor.CGColor);
CGMutablePathRef shadowPath = CGPathCreateMutable();
CGPathAddArc(shadowPath, NULL, badgeViewFrame.origin.x + badgeViewFrame.size.width - badgeViewFrame.size.height / 2, badgeViewFrame.origin.y + badgeViewFrame.size.height / 2, badgeViewFrame.size.height / 2, M_PI / 2, M_PI * 3 / 2, YES);
CGPathAddArc(shadowPath, NULL, badgeViewFrame.origin.x + badgeViewFrame.size.height / 2, badgeViewFrame.origin.y + badgeViewFrame.size.height / 2, badgeViewFrame.size.height / 2, M_PI * 3 / 2, M_PI / 2, YES);
CGContextAddPath(context, shadowPath);
CGContextDrawPath(context, kCGPathFill);
CFRelease(shadowPath);
CGContextRestoreGState(context);
</pre>
<h3>Pixel Perfect</h3>
<p>DDBadgeViewCell does an excellent job when there are three or more characters in the string. But as I wanted to display numbers, I needed to special-case the code a bit to get the pixels correctly aligned.</p>
<pre class="brush: objc; title: ; notranslate">
// Set up the badge's frame.
CGSize badgeTextSize = [self.cell.badgeText sizeWithFont:[UIFont boldSystemFontOfSize:18.]];
CGRect badgeViewFrame = CGRectIntegral(CGRectMake(rect.size.width - badgeTextSize.width - 24, (rect.size.height - badgeTextSize.height - 4) / 2, badgeTextSize.width + 14, badgeTextSize.height + 4));
if ([self.cell.badgeText length] &lt; 3) {
    // Fix the width for 1-2 characters.
    badgeViewFrame = CGRectIntegral(CGRectMake(rect.size.width - 46, 13, 34, 25));
}

// Draw the badge shadow oval and the badge oval...

// Draw the number on the badge.
CGContextSaveGState(context);
CGContextSetBlendMode(context, kCGBlendModeClear);
if ([self.cell.badgeText length] == 1) {
	// CGRectInset cuts off the label by a couple pixels, so need a bit more tweaking.
	CGRect badgeTextFrame = CGRectInset(badgeViewFrame, 11, 2);
	badgeTextFrame = CGRectMake(badgeTextFrame.origin.x + 1, badgeTextFrame.origin.y, badgeTextFrame.size.width + 2, badgeTextFrame.size.height);
	[self.cell.badgeText drawInRect:badgeTextFrame withFont:[UIFont boldSystemFontOfSize:18]];
}
else if ([self.cell.badgeText length] == 2) {
	// CGRectInset cuts off the label by a couple pixels, so need a bit more tweaking.
	CGRect badgeTextFrame = CGRectInset(badgeViewFrame, 6, 2);
	badgeTextFrame = CGRectMake(badgeTextFrame.origin.x + 1, badgeTextFrame.origin.y, badgeTextFrame.size.width + 4, badgeTextFrame.size.height);
	[self.cell.badgeText drawInRect:badgeTextFrame withFont:[UIFont boldSystemFontOfSize:18]];
}
else {
	CGRect badgeTextFrame = CGRectInset(badgeViewFrame, 7, 2);
	badgeTextFrame = CGRectMake(badgeTextFrame.origin.x, badgeTextFrame.origin.y + 1, badgeTextFrame.size.width + 4, badgeTextFrame.size.height);
    [self.cell.badgeText drawInRect:badgeTextFrame withFont:[UIFont boldSystemFontOfSize:18.]];
    CGContextRestoreGState(context);
}
CGContextRestoreGState(context);
</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2011/12/14/creating-uitableview-badges-like-ios-mail/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Modal views in universal iOS apps</title>
		<link>http://www.bdunagan.com/2011/12/14/modal-views-in-universal-ios-apps/</link>
		<comments>http://www.bdunagan.com/2011/12/14/modal-views-in-universal-ios-apps/#comments</comments>
		<pubDate>Wed, 14 Dec 2011 15:08:22 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=1953</guid>
		<description><![CDATA[On the iPhone, it&#8217;s quite normal to have an app slide a sheet up to create something or change settings. But on the iPad, that same view should be displayed in a popover. In a universal app, the key is shared code and XIBs for both approaches. The code below is all that I needed [...]]]></description>
				<content:encoded><![CDATA[<p><img style="float:left; padding-right:0px;" src="http://bdunagan.com/files/universal.link.png"/></p>
<p>On the iPhone, it&#8217;s quite normal to have an app slide a sheet up to create something or change settings. But on the iPad, that same view <a href="http://developer.apple.com/library/ios/#featuredarticles/ViewControllerPGforiPhoneOS/iPadControllers/iPadControllers.html">should be displayed in a popover</a>. In a universal app, the key is shared code and XIBs for both approaches.</p>
<p>The code below is all that I needed to have a Settings sheet appear correctly in both an iPhone and iPad app. I simply identify the device type and then either present a popover controller or a modal view controller, using the same XIB. Clean code. Consistent interface.</p>
<pre class="brush: objc; title: ; notranslate">
// Main View Controller
- (IBAction)showSheet:(id)sender {
	if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
		// iPad
		if (!self.popoverController.popoverVisible) {
			self.popoverController = [[[UIPopoverController alloc] initWithContentViewController:self.myNavigationController] autorelease];
			self.popoverController.popoverContentSize = CGSizeMake(400, 400);
			self.popoverController.contentViewController.contentSizeForViewInPopover = CGSizeMake(400, 400);
			self.popoverController.delegate = self;
			// Present the popover from the bar button.
			[self.popoverController presentPopoverFromBarButtonItem:self.navigationItem.rightBarButtonItem permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
		}
	}
	else {
		// iPhone
		[self.navigationController presentModalViewController:self.myNavigationController animated:YES];
	}
}

- (void)popoverControllerDidDismissPopover:(UIPopoverController *)aPopoverController {
	[self.popoverController dismissPopoverAnimated:YES];
}

- (MyNavigationController *)myNavigationController {
	if (myNavigationController == nil) {
		self.myNavigationController = [[UINavigationController alloc] initWithRootViewController:self.mySheetController] autorelease];
	}
	return myNavigationController;
}

- (MySheetController *)mySheetController {
	if (mySheetController == nil) {
		self.mySheetController = [[MySheetController alloc] initWithNibName:@&quot;MySheetView&quot; bundle:nil] autorelease];
	}
	return mySheetController;
}

// Sheet View Controller
- (void)viewWillAppear:(BOOL)animated {
	self.contentSizeForViewInPopover = CGMakeSize(400, 400);
	[super viewWillAppear:animated];
}
</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2011/12/14/modal-views-in-universal-ios-apps/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Ad-Hoc and App Store IPAs with xcrun</title>
		<link>http://www.bdunagan.com/2011/12/12/ad-hoc-and-app-store-ipas-with-xcrun/</link>
		<comments>http://www.bdunagan.com/2011/12/12/ad-hoc-and-app-store-ipas-with-xcrun/#comments</comments>
		<pubDate>Mon, 12 Dec 2011 01:14:19 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=1932</guid>
		<description><![CDATA[Xcode 4&#8242;s &#8220;Build and Archive&#8221; feature is extremely convenient. With it, I can build a distribution version of an app and archive it away, so that later, in Xcode Organizer, I can click on that instance and submit it to the App Store. Brilliant for individual developers. But at my work, we wanted a bit [...]]]></description>
				<content:encoded><![CDATA[<p><img style="float:left; padding-right:20px;" src="http://bdunagan.com/files/xcode.icon.png"/></p>
<p>Xcode 4&#8242;s &#8220;Build and Archive&#8221; feature is extremely convenient. With it, I can build a distribution version of an app and archive it away, so that later, in Xcode Organizer, I can click on that instance and submit it to the App Store. Brilliant for individual developers. But at my work, we wanted a bit more: automation and multiple IPAs from a single binary. Both are easy with <tt>xcrun</tt>, part of Apple&#8217;s Developer Tools. (Thanks to <a href="http://stackoverflow.com/questions/2664885/xcode-build-and-archive-from-command-line">Stackoverflow for the pointer</a>!)</p>
<h3>Standardized Keys and Certificates</h3>
<p>At work, we wanted a standard set of keys and certificates to use for official builds. I went through the standard Apple Developer Center process of creating a certificate signing request with Keychain Access to generate the local keypair, and I created a distribution certificate with that signing request and loaded that into Keychain Access. Then in Keychain Access, I exported the certificate as <tt>distribution.cer</tt> and the private key as <tt>distribution.p12</tt>. Xcode needs these two files to correctly use the provisioning profile.</p>
<p>One small hiccup was &#8220;login&#8221; versus &#8220;System&#8221; in Keychain Access. I easily imported those two exported files into the &#8220;login&#8221; keychain in Keychain Access. However, Keychain Access would not import the private key into the &#8220;System&#8221; keychain and gave the following error: &#8220;An error has occurred. Unable to import an item.&#8221; I needed to use the &#8220;System&#8221; keychain, as our build system runs with elevated privileges. To get around this error, I simply needed to import the certificate and private key via Terminal instead:</p>
<pre class="brush: plain; title: ; notranslate">
# Run in Terminal.
sudo security import /path/to/distribution.cer -k /Library/Keychains/System.keychain
sudo security import /path/to/distribution.p12 -k /Library/Keychains/System.keychain
</pre>
<p>Don&#8217;t forget to change the &#8220;Access Control&#8221; for the private key to &#8220;Allow any application to access this item&#8221; for &#8220;System.keychain&#8221;. Otherwise, you&#8217;ll get a build error about &#8220;user interaction is not allowed&#8221;.</p>
<p>At this point, I had a copy of the certificate and private key in &#8220;login&#8221; and &#8220;System&#8221; in Keychain Access. I simply needed two provisioning profiles based on that certificate: an ad-hoc profile and an app store profile. I generated both in the iOS Provisioning Profile, <tt>Adhoc.mobileprovision</tt> and <tt>Appstore.mobileprovision</tt>, and imported those into Xcode.</p>
<p>With the certificate, private key, and two provisioning profiles loaded up, automating the build was straight-forward. In fact, by selecting the correct default configuration (Xcode 3.2.6), I can simply run <tt>Xcodebuild</tt> with no arguments to generate an App Store distribution app bundle. </p>
<h3>Multiple IPAs from a single binary</h3>
<p>Automated builds are nothing new or novel. There are some great suggestions for how to <a href="http://www.benzado.com/blog/iphonedev-good-practices">structure your Xcode configurations</a> to simplify automation. What I really wanted were two files at the end: <tt>App.AdHoc.ipa</tt> and <tt>App.AppStore.ipa</tt>, ready to be tested ad-hoc or submitted to the App Store. Enter <tt>xcrun</tt>.</p>
<p>Apple distributes <tt>xcrun</tt> as part of its Xcode Developer package. The command line utility will take an app bundle and convert it into an IPA file with the supplied provisioning profile. The key part is the supplied provisioning profile can be different from the original one. Here is how I call it:</p>
<pre class="brush: plain; title: ; notranslate">
xcrun -sdk iphoneos PackageApplication 
   /path/to/bundle/AppName.app 
   -o /path/to/bundle/AppName.ipa 
   --sign &quot;iPhone Distribution&quot; 
   --embed /path/to/certificate
</pre>
<p>With this command, I can run a build and then generate two IPA files: one for ad-hoc distribution or one for App Store submission. All with the same binary. The community and the tools have progressed a bit since <a href="http://furbo.org/2008/11/12/the-final-test/">furbo&#8217;s &#8220;The final test&#8221; in 2008</a>.</p>
<h3>Over-The-Air ad-hoc distribution</h3>
<p>Apple added support for over-the-air (OTA) distribution in iOS 4. It&#8217;s the best way to distribute builds to QA, much better than syncing through iTunes. <a href="http://nachbaur.com/blog/building-ios-apps-for-over-the-air-adhoc-distribution">These instructions</a> worked perfectly for me. You don&#8217;t need an Enterprise iOS account to do this; I think that simply lets you <a href="http://stackoverflow.com/questions/5546581/how-to-distribute-ios-application-wirelessly-without-managing-udids-and-recompil">get around managing UDIDs</a>. Seriously, it&#8217;s easy and awesome.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2011/12/12/ad-hoc-and-app-store-ipas-with-xcrun/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Deleting large AWS S3 buckets</title>
		<link>http://www.bdunagan.com/2011/11/21/deleting-large-aws-s3-buckets/</link>
		<comments>http://www.bdunagan.com/2011/11/21/deleting-large-aws-s3-buckets/#comments</comments>
		<pubDate>Mon, 21 Nov 2011 18:24:07 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=1918</guid>
		<description><![CDATA[2011.12.08 Update: Amazon now supports multi-object delete in a single API request, up to 1k objects. Amazon&#8217;s AWS S3 is awesome. However, there are times when the API makes certain tasks a bit hard. Today, I wanted to delete a bucket with 100k files. AWS does not support deleting a bucket with files inside; you [...]]]></description>
				<content:encoded><![CDATA[<p><b>2011.12.08 Update</b>: Amazon now supports <a href="http://docs.amazonwebservices.com/AmazonS3/latest/API/index.html?multiobjectdeleteapi.html">multi-object delete</a> in a single API request, up to 1k objects.</p>
<p><img style="float:left;padding-right:10px;" src="http://bdunagan.com/files/aws.png"/></p>
<p>Amazon&#8217;s AWS S3 is awesome. However, there are times when the API makes certain tasks a bit hard. Today, I wanted to delete a bucket with 100k files. AWS does not support deleting a bucket with files inside; you have to delete the files first.</p>
<p>First, I tried <a href="http://amazon.rubyforge.org/"><tt>aws/s3</tt></a>. It supports <tt>:force</tt> when deleting buckets, in case they have files in them. That call was taking a long time, so I poked around the code. Unfortunately, the gem didn&#8217;t make some magic API call. It just looped:</p>
<pre class="brush: ruby; title: ; notranslate">
def delete_all
  each do |object|
    object.delete
  end
  self
end
</pre>
<p>Next, I tried to find out how geemus&#8217;s brilliant <tt>fog</tt> gem handles this need. <a href="http://groups.google.com/group/ruby-fog/browse_thread/thread/3e12a89ae899f5f2">It doesn&#8217;t</a>, because providers don&#8217;t.</p>
<p>Finally, I gave up that one API call dream and looked for what other people do. They use pagination and threads, and I ended up forking <a href="https://github.com/SFEley/s3nuke">SFEley&#8217;s <tt>s3nuke</tt> script</a> from GitHub. In the process of porting his solution from RightAWS to Fog, I discovered that fog handles auto-pagination internally. No need to mess with <tt>is_truncated</tt> or <tt>:marker</tt>. Quite nice.</p>
<p>This Ruby script deleted an S3 bucket with 100k files in 15 minutes.</p>
<p><script src="https://gist.github.com/1383301.js?file=s3-delete-bucket.rb"></script></p>
]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2011/11/21/deleting-large-aws-s3-buckets/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>iPhone Tip: Tether to your Mac</title>
		<link>http://www.bdunagan.com/2011/11/17/iphone-tip-tether-to-your-mac/</link>
		<comments>http://www.bdunagan.com/2011/11/17/iphone-tip-tether-to-your-mac/#comments</comments>
		<pubDate>Thu, 17 Nov 2011 15:25:08 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=1912</guid>
		<description><![CDATA[No, not the iOS Personal Hotspot. I meant the other direction. Here is how to share your Mac&#8217;s internet connection with your iPhone or iPad. Simply go into System Preferences and click on &#8220;Sharing&#8221;. In &#8220;Sharing&#8221;, select &#8220;Internet Sharing&#8221; and click &#8220;Wi-Fi Options&#8230;&#8221; to configure the network name. Then, just check the box for &#8220;Internet [...]]]></description>
				<content:encoded><![CDATA[<p>No, not the <a href="http://support.apple.com/kb/HT4517">iOS Personal Hotspot</a>. I meant the other direction. Here is how to share your Mac&#8217;s internet connection with your iPhone or iPad.</p>
<p>Simply go into System Preferences and click on &#8220;Sharing&#8221;. In &#8220;Sharing&#8221;, select &#8220;Internet Sharing&#8221; and click &#8220;Wi-Fi Options&#8230;&#8221; to configure the network name. Then, just check the box for &#8220;Internet Sharing&#8221;.</p>
<div style="padding:10px">
<a class="single_image" href="http://bdunagan.com/files/share_mac_internet_network.png"><img src="http://bdunagan.com/files/share_mac_internet_network.small.png"/></a>
</div>
<div style="padding:10px">
<a class="single_image" href="http://bdunagan.com/files/share_mac_internet_settings.png"><img src="http://bdunagan.com/files/share_mac_internet_settings.small.png"/></a>
</div>
<p>Very useful in some instances.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2011/11/17/iphone-tip-tether-to-your-mac/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Git Tip: Better &#8216;git log&#8217;</title>
		<link>http://www.bdunagan.com/2011/11/13/git-tip-better-git-log/</link>
		<comments>http://www.bdunagan.com/2011/11/13/git-tip-better-git-log/#comments</comments>
		<pubDate>Sun, 13 Nov 2011 19:14:56 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=1897</guid>
		<description><![CDATA[A while ago, I followed this blog&#8217;s advice and added a little color to &#8216;git log&#8217;. This original &#8216;git log&#8217; command displays useful tidbits like 7c UUID, relative date, description, tag, and user. But today, I wanted a bit more: an short absolute date and lines changed. Seeing the number of lines that changed per [...]]]></description>
				<content:encoded><![CDATA[<p>A while ago, I followed <a href="http://www.jukie.net/bart/blog/pimping-out-git-log">this blog&#8217;s advice</a> and added a little color to &#8216;git log&#8217;. This original &#8216;git log&#8217; command displays useful tidbits like 7c UUID, relative date, description, tag, and user.</p>
<p>But today, I wanted a bit more: an short absolute date and lines changed. Seeing the number of lines that changed per commit gives a better idea about the magnitude of the changes. Changing the date format was easy; adding the number of lines that changed was harder.</p>
<p>Below is a gist for the Ruby script I wrote. Instructions on setting it up as a Bash alias are embedded in it.</p>
<p><script src="https://gist.github.com/1362507.js?file=git.log.with.lines.changed.rb"></script></p>
]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2011/11/13/git-tip-better-git-log/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Migrating JIRA to Bugzilla</title>
		<link>http://www.bdunagan.com/2011/11/07/migrating-jira-to-bugzilla/</link>
		<comments>http://www.bdunagan.com/2011/11/07/migrating-jira-to-bugzilla/#comments</comments>
		<pubDate>Mon, 07 Nov 2011 21:36:05 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[development]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=1882</guid>
		<description><![CDATA[Recently, my team needed to migrate from Atlassian&#8217;s JIRA to Mozilla&#8217;s Bugzilla. I expected this process to be uncommon but not unheard of. Not so. Atlassian has a nice tool for migrating Bugzilla data into its JIRA system, but the one googleable conversation I could find about migrating JIRA to Bugzilla was in 2006. I&#8217;ll [...]]]></description>
				<content:encoded><![CDATA[<p>Recently, my team needed to migrate from Atlassian&#8217;s JIRA to Mozilla&#8217;s Bugzilla. I expected this process to be uncommon but not unheard of. Not so. Atlassian has a nice tool for migrating Bugzilla data into its JIRA system, but <a href="http://fixunix.com/mozilla/410165-migrate-jira-bugzilla.html">the <i>one</i> googleable conversation</a> I could find about migrating JIRA to Bugzilla was in 2006. </p>
<p>I&#8217;ll describe the process I used to migrate around 6K bugs.</p>
<h3>Tools</h3>
<p><i>Bugzilla&#8217;s importxml.pl</i>: Bugzilla does ship with an <a href="http://www.bugzilla.org/docs/3.0/html/api/importxml.html">import script</a>. However, it&#8217;s specifically designed for moving bugs from one Bugzilla instance to another. As input, it takes an XML file of one or more bugs, assuming you just used the XML export built into Bugzilla. Makes sense. It meant that I just extract all the issues from JIRA and reformat them into Bugzilla&#8217;s <a href="http://bzr.mozilla.org/bugzilla/3.6/annotate/head:/bugzilla.dtd">DTD</a>. I found it very helpful to add the <tt>--verbose</tt> flag for better errors.</p>
<pre class="brush: plain; title: ; notranslate">
./importxml.pl --verbose bug.xml
# The log below represents a successful import.
OK: Bug http://example.com/bugzilla/show_bug.cgi?id=1 imported as bug 1.

http://example.com/bugzilla/show_bug.cgi?id=1

</pre>
<p><i>Bugzilla&#8217;s checksetup.pl</i>: Bugzilla will check your setup with this script and let you know if you are missing any Perl modules. In particular, for importing, you will need <tt>XML::Twig</tt>. Oddly, when I started the import process, I found that I was still missing <tt>XML::Parser</tt>, so I had to install that from CPAN as well. Also, the script changed the ownership and permissions of the bugzilla directory such that it was no longer accessible on the web; I had to recursively revert those like so:</p>
<pre class="brush: plain; title: ; notranslate">
chmod -R 754 bugzilla
chown -R nobody:apache bugzilla
</pre>
<p><i>nokogiri</i>: This handy Ruby gem allowed me to use XPath to search through the JIRA XML files and extract specific fields and values. It&#8217;s extremely useful. It installs effortlessly on Lion (10.7) with a simple <tt>gem install nokogiri</tt>. Unfortunately, I was using Snow Leopard (10.6). Installing the gem on that OS was a small battle in itself. Finally, I used <a href="https://gist.github.com/746966">this gist</a> and then <a href="https://github.com/tenderlove/nokogiri/issues/381">followed steps here</a>. That worked for me. Here are some handy snippets of Nokogiri in action:</p>
<pre class="brush: ruby; title: ; notranslate">
# Element contains
doc.xpath(&quot;//h1[contains(.,'Could not find issue with issue key')]&quot;)
# Element value
doc.xpath(&quot;//item/updated&quot;)[0].text
# Attribute value
doc.xpath(&quot;//item/reporter/@username&quot;).text
# Value of subelement for element
doc.xpath(&quot;//attachment&quot;).each do |attachment|
  attachment_id = attachment.attributes[&quot;id&quot;].value
end
</pre>
<p><i>JIRA Issues</i>: There are a couple ways to export issues from JIRA. One obvious way is to simply search for the issues you want and click &#8220;View&#8221; at the top to select a different format, like XML. While straight-forward, this method has two significant downsides. First, data loss: comments are not included. Second, size: thousands of issues take a long time to process as a single XML file, and neither JIRA nor Chrome seemed happy about the size.</p>
<p>An alternative to this export is using <tt>curl</tt> to export issues individually. This process includes comments and involves lots of quick JIRA queries. As an added bonus, I can avoid any XML SAX state logic that a single large XML file would have needed, so I can focus on transforming issues into bugs in isolation. Sweet.</p>
<pre class="brush: ruby; title: ; notranslate">
# Extract JIRA issues. (Figure out how many issues to include.)
(1..1000).each do |i|
  # Use curl with credentials to extract each JIRA issue's XML.
  xml = %x[curl -u username:password https://jira.example.com/si/jira.issueviews:issue-xml/BUG-#{i}/BUG-#{i}.xml]
  # Save the XML to a file.
  f=File.new(&quot;BUG-#{i}.xml&quot;,&quot;w&quot;)
  f.write(xml)
  f.close
end
</pre>
<p><i>JIRA Attachments</i>: In addition to the issues, I wanted to extract all of the attachments. Luckily, JIRA provides a standard HTTP API for getting these files. We just need all of the attachment IDs from each extracted JIRA issue to access its attachments. We&#8217;ll save those to an <tt>attachments</tt> directory. Bugzilla actually imports attachments as embedded base64 strings in the XML files, but we&#8217;ll address that later. For now, we just want to save the attachments out of JIRA.</p>
<pre class="brush: ruby; title: ; notranslate">
# Download all the JIRA attachments. We'll need to convert them to base64 and provide them inline to Bugzilla.
Dir.mkdir(&quot;attachments&quot;) if !Dir.exists?(&quot;attachments&quot;)
(1..1000).each do |i|
  puts i
  # Open the JIRA issue.
  doc = Nokogiri::XML(open(File.expand_path(&quot;BUG-#{i}.xml&quot;)))
  # Find all the attachment references.
  doc.xpath(&quot;//attachment&quot;).each do |attachment|
    # Get the attachment ID and file name.
    attachment_id = attachment.attributes[&quot;id&quot;].value
    file_name = attachment.attributes[&quot;name&quot;].value
    # Get the attachment contents using curl.
    contents = %x[curl -u username:password https://jira.example.com/secure/attachment/#{attachment_id}/#{file_name}]
    # Save the attachment.
    Dir.mkdir(&quot;attachments/BUG-#{i}&quot;) if !Dir.exists?(&quot;attachments/BUG-#{i}&quot;)
    f=File.new(&quot;attachments/BUG-#{i}/#{attachment_id}_#{file_name}&quot;,&quot;w&quot;)
    f.write(contents)
    f.close
  end
end
</pre>
<p><i>Bugzilla Field Values</i>: Bugzilla will not automatically create people, products, versions, components, or milestones during the import process. Those need to already exist. Otherwise, Bugzilla will use the default product and component. In my case, they did already exist, but the names had been changed. To handle these changes, I map JIRA strings to Bugzilla strings in my main script.</p>
<h3>jira2bugzilla.rb</h3>
<p>At this point, we have all the tools and the data we need to tranform JIRA issues into Bugzilla bugs. I committed the full <tt>jira2bugzilla.rb</tt> Ruby script to a <a href="https://github.com/bdunagan/jira2bugzilla">GitHub repo</a>. It doesn&#8217;t work out of the box, as there are quite a few instance-specific variables, but the script provides a nice base. I&#8217;ll touch on a couple points:</p>
<p><i>Attachments</i>: Bugzilla expects attachments embedded in the XML file, so we need to convert our binary files into base64 strings and then include them inline. Below is Ruby code to convert to base64. Keep in mind that the import process takes far longer when attachments are included.</p>
<pre class="brush: ruby; title: ; notranslate">
# Get the attachment ID and name.
attachment_id = attachment.attributes[&quot;id&quot;].value
file_name = attachment.attributes[&quot;name&quot;].value
# Figure out where we saved it.
attachment_path = &quot;BUG/#{bug_id}/#{attachment_id}_#{file_name}&quot;
# Encode the file as base64.
encoded_file = Base64.encode64(IO.read(&quot;#{attachment_path}&quot;))
</pre>
<p><i>Description</i>: While JIRA gives the bug description its own element (<tt>description</tt>), Bugzilla considers it the first comment on the bug. When rewriting the JIRA issues into a Bugzilla bug, I needed to migrate the description into the first comment.</p>
<p><i>Severity</i>: JIRA doesn&#8217;t seem to have a notion of severity, so I assigned a default severity to all the bugs.</p>
<p><i>QA Contact</i>: JIRA doesn&#8217;t seem to have a notion of QA contact, so when setting up the components, be sure to assign the default QA contact correctly. The import script will assign each bug to the correct person.</p>
<p>Again, see <a href="https://github.com/bdunagan/jira2bugzilla">this GitHub repo</a> for the full <tt>jira2bugzilla.rb</tt> script. I simply run <tt>ruby jira2bugzilla.rb</tt> in the directory with all of the JIRA XML issues.</p>
<h3>Waiting for Import</h3>
<p>At this point, we have converted all the JIRA issues into Bugzilla bugs. Next, we transfer them onto the Bugzilla server into a subdirectory of the bugzilla installation, like <tt>bugzilla/bugs/</tt>. To import the bugs, I ran the following one-liner. It finds all the XML files, sorts them alphabetically, and feeds each one by one into the Bugzilla import script.</p>
<pre class="brush: plain; title: ; notranslate">
find . -name &quot;BUG-*.xml&quot; | sort -n | xargs -n 1 ./importxml.pl --verbose &amp;&gt; bug-import.log
</pre>
<p>The script imported 6K bugs in two hours. However, I didn&#8217;t include attachments. When I tried to import a single bug with a 5MB attachment, it took around thirty minutes. With multiple gigabytes of attachments, I opted to not include them. </p>
<p>Most importantly, check the log. The above one-liner pipes all of the log information to <tt>bug-import.log</tt>. Run <tt>tail -f bug-import.log</tt> while it&#8217;s running to make sure the process is working. Grep through the output afterwards for terms like &#8220;Bad&#8221; or &#8220;Error&#8221; to ensure all the bugs were imported. With 6K bugs, most imported correctly, but a few did not.</p>
<h3>Atlassian</h3>
<p>I just wanted to highlight that migrating away from Atlassian&#8217;s JIRA is not a reflection of their product. I was simply tasked with making it happen. Frankly, Atlassian has been doing an amazing job at steadily growing into a vertically integrated company. They received $60M in VC funding in 2010 to grow their business, and they&#8217;ve scooped up businesses like BitBucket and SourceTree. Just a couple weeks ago, they announced that their main products will now be available as cloud services. Atlassian seems to have a long-term strategy that they&#8217;re executing very well.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2011/11/07/migrating-jira-to-bugzilla/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>3 Lessons from Google Apps</title>
		<link>http://www.bdunagan.com/2011/11/03/3-lessons-from-google-apps/</link>
		<comments>http://www.bdunagan.com/2011/11/03/3-lessons-from-google-apps/#comments</comments>
		<pubDate>Thu, 03 Nov 2011 02:20:29 +0000</pubDate>
		<dc:creator>bdunagan</dc:creator>
				<category><![CDATA[ideas]]></category>

		<guid isPermaLink="false">http://www.bdunagan.com/?p=1852</guid>
		<description><![CDATA[I recently switched over to Google Apps at work, and one obvious move was migrating everyone to Google Talk/Jabber. Seemed easy enough. I was sure there would be a checkbox in the Domain Dashboard&#8217;s Settings for that. I just wanted every domain user to have everyone else show up in Gmail Chat, Google Talk, and [...]]]></description>
				<content:encoded><![CDATA[<p><img style="float:left; padding:4px 10px 0px 0px;" src="http://bdunagan.com/files/Google.Apps.Logo.png"/></p>
<p>I recently switched over to Google Apps at work, and one obvious move was migrating everyone to Google Talk/Jabber. Seemed easy enough. I was sure there would be a checkbox in the Domain Dashboard&#8217;s Settings for that. I just wanted every domain user to have everyone else show up in Gmail Chat, Google Talk, and Jabber. I eventually translated this goal into this search: &#8220;automatically invite and accept all shared contacts&#8221;. Nothing.</p>
<p>Google Apps does not provide that level of support for domain-based chat. The service doesn&#8217;t even let domain users see a list of the other domain users. The <a href="http://www.google.com/support/a/bin/answer.py?answer=1628009">Google Apps Directory</a> (analogous to Exchange&#8217;s global address list or GAL) is only available from search and auto-complete and from their API. (<a href="http://www.google.com/support/a/bin/answer.py?answer=60218">&#8220;Contact Sharing&#8221;</a> is enabled by default.). Their Contacts app is just not as advanced as the rest of their awesome services. Which is fine. They do provide an API and an app store: <a href="https://www.google.com/enterprise/marketplace/?pli=1">Google Apps Marketplace</a>. Marketplace has several highly-rated Contacts apps (like <a href="http://www.sherpatools.com/tour/">SherpaTools</a> and <a href="http://www.gmailsharedcontacts.com/">Shared Gmail Contacts</a>), and Jabber provides a mechanism to invite users. Problem solved.</p>
<p>Still, figuring all that out wasn&#8217;t fun. I read many help articles, forum posts, and API docs before I was able to map my problem onto Google&#8217;s solutions. Stepping back, here are three lessons to think about:</p>
<h3>1 &#8211; Design Decisions</h3>
<p>Google Apps is composed of apps and APIs. Ideally, both would be full-featured. Unfortunately, development resources are often far more finite than Product Managers and customers can bear. You have to prioritize some features (a full API) over others (a full Contacts app).</p>
<p>In this case, Google very clearly likes APIs. For years, Google Apps has provided an API for <a href="http://code.google.com/googleapps/domain/profiles/developers_guide.html">internal people (Domain Profiles)</a> and for <a href="http://code.google.com/googleapps/domain/shared_contacts/gdata_shared_contacts_api_reference.html">external people (Shared Contacts)</a>. While the Google Apps&#8217; Shared Contacts UI needs a bit of love, the API and app store allows other companies to write third-party apps to solve this problem. I&#8217;m sure the Google Apps team is working on revising the contact manager, but they&#8217;re also busy shipping features like Google+ support.</p>
<p>Moreover, I could accomplish my goal. I enabled &#8220;Automatically accept chat invitations between users&#8221; under <a href="http://www.google.com/support/a/bin/answer.py?answer=60767">Settings/Chat</a> (Premier only), and then I wrote a quick Ruby script to invite coworkers with Jabber:</p>
<p><script src="http://gist.github.com/1324585.js"></script></p>
<p>Design decisions are choices. The Google Apps team chose a complete Contacts API over a full-featured Contacts app. Customers appreciate finite resources even less than Product Managers. The key is to keep them in the loop.</p>
<h3>2 &#8211; Keep Communicating</h3>
<p>Yes, there is a way to solve my chat invitation problem and to manage Shared Contacts. No, I didn&#8217;t know what I was looking for. Searching through Google Apps&#8217; forum site, I found dozens of conversations about how woefully inadequate the contact manager was. Companies were resistant to switching from Exchange to Google Apps solely because they would miss so many nice contact management features. These thread span from <a href="http://www.google.com/support/forum/p/Google%20Apps/thread?tid=1d107b4ac884d25b&#038;hl=en">mid-2009</a> to <a href="http://www.google.com/support/forum/p/Google+Apps/thread?tid=14c1814eaa39b480&#038;hl=en">last week</a>.</p>
<p>However, I didn&#8217;t find a single reply from a Google employee, pointing the thread in the right direction. Google Apps&#8217; Help <a href="http://www.google.com/support/a/bin/answer.py?answer=1628009">states their position plainly enough</a>: &#8220;As an alternative to using the APIs, explore the Google Apps Marketplace for products that can help you manage Contacts through a user-friendly interface.&#8221; But all those customers need an official voice to guide them. The Google Apps team chose APIs over apps, but it hasn&#8217;t been telling anyone on its own forums. I only figured out what I should be searching for by reading those dozens of posts.</p>
<p>You might have a great service, a great API, great documentation, and great forums. But if you&#8217;re not in the forums, answering questions, pointing out documentation, how will your customers understand how to solve their problems? It&#8217;s your job to translate their problems into your solutions and to set expectations.</p>
<h3>3 &#8211; Manage Expectations</h3>
<p>Google Apps is an interesting service. It is a clear example of how difficult it is these days for corporate services to keep up with consumer ones, as Google is behind both of them. When Google+ debuted, a number of Google Apps customers were upset that the feature wasn&#8217;t immediately available on their Apps accounts as well. But Google Apps is a different service. It&#8217;s geared at businesses but used by a wide variety of people with wildly different expectations. The team needs to manage expectations better.</p>
<p><a class="single_image" href="http://bdunagan.com/files/Google_Apps_Whats_New.png"><img src="http://bdunagan.com/files/Google_Apps_Whats_New.small.png"/></a></p>
<p>The Google Apps team is trying. In March 2011, they revealed their <a href="http://whatsnew.googleapps.com/">release schedule</a>. Moreover, customers can now choose whether to get features immediately or on a scheduled cycle, and they can find out what&#8217;s next. Frankly, flipping through the schedules makes me appreciate how many services Google Apps covers now. Think about <a href="http://googleenterprise.blogspot.com/2010/11/ten-times-more-applications-for-google.html">all those services</a> they added a year ago. I bet it was a monumental task.</p>
<p>And yet, when I started my search for auto-invites for chat, I expected Contacts to be a full-featured app in the feature set, rather than a third-party tool. I resisted using the third-party tools, and it took me a bit to accept that the Google Apps team really wanted me to head in that direction, especially since they didn&#8217;t indicate it very clearly. My expectations were wrong.</p>
<p>Think about what expectations your customers have. Give them a forum to vent. You&#8217;ll quickly find out what they expect. Then do your best to manage those expectations.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.bdunagan.com/2011/11/03/3-lessons-from-google-apps/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
