Apple, Cross Platform Toolkits, and the Long Tail

I have a lot of problems with Apple’s recent policy decisions on a number of levels. But I wanted to explore how Apple’s decision affects them and their users in more detail. One of the reasons Amazon has been so successful is that they service the needs of the ‘long tail’, a term that describes how large portion of revenue comes from products with limited appeal or low volume sales. Amazon makes a large portion of its revenue from bestsellers clearly, but a big reason people come to Amazon is because they also have thousands or millions of other producs available that aren’t bestsellers. Books only a mechanical engineer or chemist would find interesting, or books only an orphan liberal secessionist immigrant Texan fireman would find appealing. If Netflix only offered the top 1000 movies, they wouldn’t be where the are today. Bing has also recently admitted that focusing only on high volume searches in an attempt tp compete with Google was a mistake, that they ignored the long tail of search questions is what is costing them now.

What Apple is doing is chopping off a huge chunk of that tail. Products that have limited appeal and low volume have a tendency to be of lower quality or polish precisely because of their small market. But to the people who need or want those products, functions or features, they are worth every penny and ‘polish’ barely enters into the equation.  Look at the continued use and effectiveness of command line/terminal tools: If polish and ease of use were the bell weathers, then they would have disappeared a long time ago. Obviously you can hide them away and make them less obvious, but making them inaccessible is a mistake in my opinion.

And Apple can make money and be happy with the large percentage of people who are only interested in bestsellers and say books sold exclusively at Barnes & Noble (not Borders or Amazon), to keep the analogy going.  But to the extent that they can get ‘authors’ to write books only available exclusively at Barnes & Noble, everyone else is worse off.

I’m not saying Apple is doing anything illegal, but it just seems wrong. Let your products succeed on their merits, not on whether or not you can craft the rules to benefit you more.

 

Posted by slaingod Thu, 20 May 2010 09:38:00 GMT


Adobe Flex Multiline CheckBox and RadioButton

This code was adapted from various ones I’ve seen around the interwebs, most notably spy6.blogspot.com/2008/09/flex-multiline-checkbox.html

However this one suffered from resizing issues( you had to explicitly set the width), and it did not work properly in the event that the font style changed on the text because there was another place that created the underlying textField which was not getting the mutliline handling executed, in commitProperties. I instead took a slightly different tack, to overload createInFontContext() which gets called by the Button superclass, and substitute a MultilineUITextField in the class factory instead. (Note createInFontContext() is probably newer than the original code from the blog post above, and wasn’t available to the original author of this hack.)

There is also a flex font styling component included that takes a target to assign the font styling. In that is the custom Font combo box that shows the actual fonts that I reworked from some other location on the webs…

 

Click thru for source view:

Example SWF

 

Adjust the slider to see it resize.

 

Posted by slaingod Sun, 11 Apr 2010 08:35:00 GMT


SimplerXMLEncoder: Flex's SimpleXMLEncoder made Array Safe

I was recently annoyed by the asymmetry between Flex’s SimpleXMLDecoder and SimpleXMlEncoder.  Specifically in the way that the encoder mangles ArrayCollections:

<answers><answer>1</answer><answer>2</answer></answers>

becomes:

<answers><answer><list><item><source>1</source></item><item><source>2</source></item></list></answer></answers>  

This behavior completely broke the purpose of my code, to use data binding from the XML input by the user. Basically, this was a Poll application that had a very simple UI config interface for use by internal, that lets you put in the XML required by this or other similar apps. I had also seen this behavior when working on Zeeb, and had worked around it from the decoding side (storing the config for the app to XML, then reading it back and doing some convulted hacking to work around the ArrayCollection issues).

Below is my solution, SimplerXMLEncoder, which handles ArrayCollections just like arrays, and changes how arrays are displayed so that it creates multiple nodes rather than multiple sub ‘items’. My tweaks are underlined and in bold since I couldn’t find the color changer. Note, that this served my purposes, but has not been tested on a bunch of different XML types, so YMMV.

 

////////////////////////////////////////////////////////////////////////////////
//
//  ADOBE SYSTEMS INCORPORATED
//  Copyright 2005-2007 Adobe Systems Incorporated
//  All Rights Reserved.
//
//  NOTICE: Adobe permits you to use, modify, and distribute this file
//  in accordance with the terms of the license agreement accompanying it.
//
////////////////////////////////////////////////////////////////////////////////

package com.vitrue.shared.util
{

import flash.xml.XMLDocument;
import flash.xml.XMLNode;

import mx.collections.ArrayCollection;
import mx.utils.ObjectUtil;

/**
 * The SimpleXMLEncoder class takes ActionScript Objects and encodes them to XML
 * using default serialization.
 */
public class SimplerXMLEncoder
{
    //--------------------------------------------------------------------------
    //
    //  Class Methods
    //
    //--------------------------------------------------------------------------
    /**
     * @private
     */
    static internal function encodeDate(rawDate:Date, dateType:String):String
    {
        var s:String = new String();
        var n:Number;

        if (dateType == "dateTime" || dateType == "date")
        {
            s = s.concat(rawDate.getUTCFullYear(), "-");

            n = rawDate.getUTCMonth()+1;
            if (n < 10) s = s.concat("0");
            s = s.concat(n, "-");

            n = rawDate.getUTCDate();
            if (n < 10) s = s.concat("0");
            s = s.concat(n);
        }

        if (dateType == "dateTime")
        {
            s = s.concat("T");
        }

        if (dateType == "dateTime" || dateType == "time")
        {
            n = rawDate.getUTCHours();
            if (n < 10) s = s.concat("0");
            s = s.concat(n, ":");

            n = rawDate.getUTCMinutes();
            if (n < 10) s = s.concat("0");
            s = s.concat(n, ":");

            n = rawDate.getUTCSeconds();
            if (n < 10) s = s.concat("0");
            s = s.concat(n, ".");

            n = rawDate.getUTCMilliseconds();
            if (n < 10) s = s.concat("00");
            else if (n < 100) s = s.concat("0");
            s = s.concat(n);
        }

        s = s.concat("Z");

        return s;
    }

    //--------------------------------------------------------------------------
    //
    //  Constructor
    //
    //--------------------------------------------------------------------------

    /**
     *  Constructor.
     *
     *  @param myXML The XML object.
     */
    public function SimplerXMLEncoder(myXML:XMLDocument)
    {
        super();

        this.myXMLDoc = myXML ? myXML : new XMLDocument();
    }

    //--------------------------------------------------------------------------
    //
    //  Variables
    //
    //--------------------------------------------------------------------------

    private var myXMLDoc:XMLDocument;

    //--------------------------------------------------------------------------
    //
    //  Methods
    //
    //--------------------------------------------------------------------------

    /**
     *  Encodes an ActionScript object to XML using default serialization.
     *  
     *  @param obj The ActionScript object to encode.
     *  
     *  @param qname The qualified name of the child node.
     *  
     *  @param parentNode An XMLNode under which to put the encoded
     *  value.
     *
     *  @return The XMLNode object. 
     */
    public function encodeValue(obj:Object, qname:QName, parentNode:XMLNode):XMLNode
    {
        var myElement:XMLNode;

        if (obj == null)
            return null;

        // Skip properties that are functions
        var typeType:uint = getDataTypeFromObject(obj);
        if (typeType == SimplerXMLEncoder.FUNCTION_TYPE)
            return null;

        if (typeType == SimplerXMLEncoder.XML_TYPE)
        {
            myElement = obj.cloneNode(true);
            parentNode.appendChild(myElement);
            return myElement;
        }

        myElement = myXMLDoc.createElement("foo");
        myElement.nodeName = qname.localName;
        parentNode.appendChild(myElement);

        if (typeType == SimplerXMLEncoder.OBJECT_TYPE)
        {
            var classInfo:Object = ObjectUtil.getClassInfo(obj, null, CLASS_INFO_OPTIONS);
            var properties:Array = classInfo.properties;
            var pCount:uint = properties.length;
            for (var p:uint = 0; p < pCount; p++)
            {
                var fieldName:String = properties[p];
                var propQName:QName = new QName("", fieldName);
                encodeValue(obj[fieldName], propQName, myElement);
            }
        }
        else if (typeType == SimplerXMLEncoder.ARRAY_TYPE)
        {
            var numMembers:uint = obj.length;
            var itemQName:QName = new QName("", qname.localName);
            myElement.removeNode();
            for (var i:uint = 0; i < numMembers; i++)             {                 encodeValue(obj[i], itemQName, parentNode);
            }         }         else         {             // Simple types fall through to here             var valueString:String;             if (typeType == SimplerXMLEncoder.DATE_TYPE)             {                 valueString = encodeDate(obj as Date, "dateTime");             }             else if (typeType == SimplerXMLEncoder.NUMBER_TYPE)             {                 if (obj == Number.POSITIVE_INFINITY)                     valueString = "INF";                 else if (obj == Number.NEGATIVE_INFINITY)                     valueString = "-INF";                 else                 {                     var rep:String = obj.toString();                     // see if its hex                     var start:String = rep.substr(0, 2);                     if (start == "0X" || start == "0x")                     {                         valueString = parseInt(rep).toString();                     }                     else                     {                         valueString = rep;                     }                 }             }             else             {                 valueString = obj.toString();             }             var valueNode:XMLNode = myXMLDoc.createTextNode(valueString);             myElement.appendChild(valueNode);         }         return myElement;     }     /**      *  @private      */     private function getDataTypeFromObject(obj:Object):uint     {         if (obj is Number)             return SimplerXMLEncoder.NUMBER_TYPE;         else if (obj is Boolean)             return SimplerXMLEncoder.BOOLEAN_TYPE;         else if (obj is String)             return SimplerXMLEncoder.STRING_TYPE;         else if (obj is XMLDocument)             return SimplerXMLEncoder.XML_TYPE;         else if (obj is Date)             return SimplerXMLEncoder.DATE_TYPE;         else if (obj is Array || obj is ArrayCollection)
            return SimplerXMLEncoder.ARRAY_TYPE;         else if (obj is Function)             return SimplerXMLEncoder.FUNCTION_TYPE;         else if (obj is Object)             return SimplerXMLEncoder.OBJECT_TYPE;         else             // Otherwise force it to string             return SimplerXMLEncoder.STRING_TYPE;     }     private static const NUMBER_TYPE:uint   = 0;     private static const STRING_TYPE:uint   = 1;     private static const OBJECT_TYPE:uint   = 2;     private static const DATE_TYPE:uint     = 3;     private static const BOOLEAN_TYPE:uint  = 4;     private static const XML_TYPE:uint      = 5;     private static const ARRAY_TYPE:uint    = 6;  // An array with a wrapper element     private static const MAP_TYPE:uint      = 7;     private static const ANY_TYPE:uint      = 8;     // We don't appear to use this type anywhere, commenting out     //private static const COLL_TYPE:uint     = 10; // A collection (no wrapper element, just maxOccurs)     private static const ROWSET_TYPE:uint   = 11;     private static const QBEAN_TYPE:uint    = 12; // CF QueryBean     private static const DOC_TYPE:uint      = 13;     private static const SCHEMA_TYPE:uint   = 14;     private static const FUNCTION_TYPE:uint = 15; // We currently do not serialize properties of type function     private static const ELEMENT_TYPE:uint  = 16;     private static const BASE64_BINARY_TYPE:uint = 17;     private static const HEX_BINARY_TYPE:uint = 18;     /**      * @private      */     private static const CLASS_INFO_OPTIONS:Object = {includeReadOnly:false, includeTransient:false}; } }


 

Posted by slaingod Tue, 30 Mar 2010 04:08:00 GMT


Zeeb: Movie Renamer

So my new open source project is mostly finished. This project is intended to help deal with the morass of naming issues encountered when managing movies on your hard drive, bringing some sanity to the process. Zeeb allows you to intelligently indicate which parts of a filename it should use to look up that movie on IMDB, which parts of the filename it should remove, and which it should save and leave in as saved parts in the final filename(typically things like 720p, 1080p, DTS, Director’s Cut, etc. that indicate some important aspect of the movie or format). Other aspects of managing these files are covered as well, such as renaming external subtitles, and the creation of a .url link that allows you to double click to visit the IMDB page for that movie.

Zeeb was a fun weekend project. The name was chosen for its similarity to another unfinished project I am working on, called Veed (for video feed). I chose to use Adobe AIR as the basis for this project, because I have a lot of recent experience with Adobe Flex for web-based projects, and it just makes the UI aspects that much easier, since I can leverage skills and techniques I have learned from that experience. This also allows the application to run on multiple platforms, those supported by Adobe AIR (OSX and Windows tested, Linux has not been tested). I am somewhat of a holdout on the OSX front: I have a Macbook but I use Windows primarily. I have another blog post about why I haven’t made that particular switch yet (mostly because I build my own computers, and don’t have faith in hackintosh to support all my devices). But my friends and coworkers almost uniformly use OSX, so I wanted something they could use as well.

Anyway, check it out if you get a chance:

Zeeb @ SourceForge

The license is Public Domain, which means you can do anything you want with it…sell it, use parts of it in your project without attribution, etc.

Posted by slaingod Thu, 14 Jan 2010 12:56:00 GMT


Verizon Already at its tricks

I recently updated to the Droid…on the day it was released (11/06/2009). Lo and behold, Verizon decided it was in my best interest to make my new monthly charges retroactive to the beginning of my monthly pay period, to the tune of an extra $12.

America’s Choice 450 Refund 10/29 - 11/28 -39.99 Nationwide Basic 900 10/29 - 11/28 59.99 Email & Web for SMARTPHONE 10/29 - 11/28 29.99

Posted by slaingod Fri, 04 Dec 2009 08:56:00 GMT


This is the Droid I was looking for...

I recently acquired a new Motorola Droid phone, based on the Android OS. So far I love it. I’m not saying it is the best phone in the world, but it does what I need and has a lot of potential for growth.

The main cons of the phone hardware itself:

  • The screen is almost too sensitive, especially around the special buttons at the bottom. It is sometimes too easy to press buttons inadvertently. This is particularly the case when typing with the hardware keyboard, as I have accidentally discarded emails I was typing with a slight brush and no recourse.
  • Camera could be better, though this may be a firmware/software update thing.
  • The power cable is somewht awkwardly placed, though for a reason. It is well placed for the Media Doc accessory which is pretty darn cool. But I use my phone to read ebooks so it is a slight annoyance there.

Other than that, the Droid hardware is rock solid.

As far as Android goes: It is still clearly a work in progress, but it is very good already. Here are my main issues with it:

  • It MUST get a ‘selective permissions per app’ option, so that I can disable all the over-reaching app that try to read my contacts, get my phone identity, or want ‘full network access’ unnecessarily. This is the new identity fraud and information gathering vector of the future and Google REALLY needs to get on top of this. Even the ‘My Verizon’ app is getting in on the fun: Version 1.0 just needed network access (since it is basically just a web widget to their website). Now 2.0 all of a sudden needs to be able to ‘read my browser history and bookmarks’? That is just someone’s idea of data collection gone awry. At the very least, if they are requesting these permissions the SDK should require them to provide a text explanation of WHY they want them if they are legitimate to the working of the app, like ‘Full Network Access: So AdMob can provide advertising services.’ Basically this would be my primary reason to root my device, to install custom software to manage this.
  • A full featured firewall…along the lines of above and for the similar reasons.
  • Market needs a bookmark/save for later functionality. App update needs an ‘ignore this update’ or ‘never update’ function.
  • The lock screen needs a delay like: Don’t show the lock screen if the phone has been idle for only 10 minutes.
  • Enable multitouch already…if that means you need to license it from Apple for $10 and I have to pay extra…go for it.
  • Bluetooth file transfer of files from a desktop. Obviously this is disabled because of tethering, though I’m not sure how this would make a difference since technically I could just use my wireless to download a file then transfer it with a cable.
  • VPN support for old school group authentication.

Beyond that my main complaint lies with FBReader, the main free ebook reader. It is lacking some critical features I need, like highlighting.

Posted by slaingod Fri, 20 Nov 2009 12:06:00 GMT


Cell Phone Madness

So I am using a 5 year old piece of crap Samsung flip-phone right now, that cuts out on audio all the time. I basically just use it for text messaging. My previous phone, the Verizon/Audiovox XV6700 was still doing just fine, but the USB connector broke, and though I had done some soldering on it to get it to charge again, that too eventually failed.

So I am looking into getting a new smartphone, but I am having a hard time finding the right one. My primary uses are for reading ebooks, and texting. This means that getting the best display possible and a QWERTY keyboard are high priorities. A screen keyboard might be able to suffice, but isn’t ideal.

So there are 4 major smartphone OS’s that I can look at: Windows Mobile, iPhone, Palm’s webOS, and Google’s Android. I have experience with Windows Mobile, but I am well aware of its limitations, as I have done some development for it. The other 3 are basically on par with each other in that they are modern, being actively innovated and updated, and have better development possibilities.

The real limiting factor for me is that I really want to get an AMOLED display, which is the new hotness, top-of-the-line display technology. Samsung seems to be the major provider for devices using these screens. So right away iPhone and Palm are out, since their phones do not have these displays and other manufacturer’s can’t make them for them. If either of these did have an AMOLED display option, I would grab one immediately.

That leaves Android and Windows Mobile. Android again would be the preferable choice, but to-date their phone designs have been horrendous. New phones are ‘on the way’ but they either lack the AMOLED display, or they lack a keyboard.

The Samsung i7500 looks decent, but is missing the keyboard. And is most likely never going to be released by a US carrier…the bane of all decent cell phones.

Finally there is the rumored Samsung Omnia Pro, which will have AMOLED and QWERTY… but runs Windows Mobile…sigh.

I guess I can just get a hold-over phone sans contract and just wait another year til AMOLED is on the Palm/iPhone. Makes me sad though.

Posted by slaingod Tue, 21 Jul 2009 23:10:00 GMT


Adobe Flex PopupManager.centerPopup timing bug workaround

So I had timing bug when working on a project, where I wanted to launch a PopupManage popup to allow the user to change their password if a flashvar was set. The flashvar would be set in response to a link click from the user’s email.

The problem is that no matter what I tried I couldn’t get my popup to center if it was called from an event fired in the Application.applicationCompelete event handler. Basically, I have PopupManager.centerPopUp(this) in the creation complete event handler of the Popup up component, but for whatever reasons, the Application’s screen height and width were still set at zero at this stage, presumably because the screen hadn’t been drawn yet. I tried adding a callLater around the centerPopup call, but that didn’t work.

The fix was to put the callLater around the initial dispatchEvent call in the applicationComplete handler. This provided enough of a delay for the screen to be set properly.

                 if(parameters['password_reset_code'] && parameters['password_reset_code'] != '') {
                        callLater(function():void {
                            var pw_reset_evt:GenericEvent = new GenericEvent("PASSWORD_RESET_CODE", parameters['password_reset_code']);
                            dispatchEvent(pw_reset_evt);
                        });
                    }

There really should be a ‘callIn’ method as well as the ‘callLater’ method. callIn would take a time in seconds or milliseconds to delay before calling the code, and would wrap the whole dynamic Timer object handling.

GenericEvent is just an Event wrapper with a ‘data’ field.

package com.vitrue.shared.events {

    import flash.events.Event;

    public class GenericEvent extends Event {

        public static var EVENT_PREFIX:String = "GenericEvent";

        public var eventType:String;
        public var data:*;

        public function GenericEvent(type:String, data:Object) {
            eventType = type;
            super(EVENT_PREFIX + type);
            this.data = data;
        }

        public static function EVENT(type:String):String {
            return EVENT_PREFIX + type;
        }

    }
}               

Posted by slaingod Tue, 21 Jul 2009 14:04:00 GMT


Geeking out: Best places to farm borean leather in World of Warcraft

So I’ve found my favorite place to farm for the main leatherworking materials, borean leather and arctic fur, in the lastest upgrade to World of Warcraft. Unfortunately, it seems like the best places are those where not having done some of the quests that cause phasing. In particular, I had heard that Valley of Echoes in Ice Crown had some fast respawn monsters if you hadn’t done the main quest lines there. But alas, I had already completed those, so I read that the Sons of Hodir cave in Storm Peaks had a lot Jormunger snakes and Bears. However I found the respawn rate to be lacking on these…but that may only be because I haven’t completed the quest lines for the Brunnhildar female Viking village.

What I did discover is my new favorite Borean Leather farming spot. So to reiterate, I haven’t done any of the quests in Storm Peaks, so I am in the ‘Phase 1’ where the Brunnhildar Warbears and Warmaiden riders are sent to battle the elite giants and wolves in the valley. The Brunnhildar Warbears spawn as fast as you kill them, as it seems there is a minimum number of monsters that need to be alive to keep the battle in the valley properly sustained. You can just kill the Warbears, as the Warmaidens die when they are dismounted by the bear dying.

You can manage the rate at which they come to a certain extent by choosing where you kill them: back up if they come to fast so it takes longer between kills for them to respawn and run out to where you are. You want to be positioned sort of near the cave entrance, but a little closer to the village. If things pile up too quickly, just move to the cliff wall near the cave so that a few of them pass to join the battle with the giants, rather than attacking you.

Best thing is, IMO, the Warbears don’t drop loot, they are just skinnable, so your bags don’t constantly get filled with garbage items you have to vendor or discard. Just like the boars at Oronok’s Farm in Shadow Moon Valley for Knothide Leather. And because you are in a different phase than most everyone else who has done the quest line, you are basically able to farm unmolested by other toons on PVP servers.

Posted by slaingod Fri, 10 Apr 2009 14:22:00 GMT


Case Insensitive Hpricot

So recently started dealing with Hpricot…what a mess, even tho this is supposed to be the end all be all of Ruby HTML parsers. My main issue is a complete lack of useful documentation. I ended up having to use some_element.methods.inspect to see what the hell my options were with a particular element, where I found the etags, which was what I needed to find.

Of course I wouldn’t have needed etags if Hpricot had an option to do case insensitive searches…like when I need to parse a document for the META info, I shouldn’t need to look for ‘meta’, ‘META’, ‘Meta’ and any other flavors that someone might have typed in. I know the ‘spec’ says this is the way it is supposed to work (case sensitive), but an HTML parser in particular needs to live in the real world.

Here is a method you can call like:

doc = normalize_hpricot(Hpricot(my_html))

 #deal with hpricot case sensitivity
    def normalize_hpricot(element)
    element.children.each do |child|

      if child.respond_to?(:etag=)
        child.etag = child.etag.downcase if child.etag
      end
      if child.respond_to?(:raw_attributes=)
        attribs = {}

        begin
        child.raw_attributes.each_pair do |key,value|
          attribs[key.downcase] = value if value
        end
        child.raw_attributes = attribs
          rescue
          end
      end
      normalize_hpricot(child) if child.respond_to?(:children) and child.children
    end
    return element
  end

This code was taken from http://davidsmalley.com/2008/4/24/hpricot-case-sensitivity and fixed to work and to not make everything lower case, just the tag names and the attribute names from the html tags.

Posted by slaingod Sun, 05 Apr 2009 13:19:00 GMT


Older posts: 1 2 3 4