Tuesday, November 17, 2009

Star Wars

Guy English points out that Apple's review process let Star Wars Trench Run onto the App Store despite its use of an "Apple-owned graphic symbol" on the instructions page. I know some of you are expecting me to defend Apple on this one, but I don't think there's any reason to do so. Somewhere or another, it looks like they screwed up, and it was a screwup with especially bad timing. It makes it look like a) LucasArts is getting special privileges, and/or b) the inconsistency of the review process isn't improving, and neither puts them in a very good light.

Of course, I don't know if LucasArts has an "express written trademark agreement" in place with Apple. It's possible. If they do, that would explain the discrepancy and provide justification for allowing Trench Run on the App Store, but my gut says this was just a garden-variety review process screwup. In other words, the reviewer missed it, just like the reviewer of the first edition of Airfoil Speakers Touch. As Heinlein said, don't attribute to villainy that which can be adequately explained by stupidity.

Assuming there isn't trademark agreement in place between Apple and Lucas, the only correct course of action here is for Apple to either reverse their previous decision on Airfoil, or else reject any future updates to Trench Run unless LucasArts removes the iPhone image you can see at the link above.

I don't think Apple should remove the existing App since they haven't removed earlier apps that have made it through the review process with similar images in place. They did not remove 1.0 of Airfoil Speaker Touch, for example, nor many other apps that got through the review process with an iPod or other Apple-owned graphic image in them. But, if they continue to allow LucasArts to update Trench Run with this image, that will completely undermine any argument they had for keeping Airfoil out of the App Store.

In both cases, an image is composited on top of an Apple-owned graphic image, so either both should be allowed, or neither should be. If Apple has a valid trademark reason to keep out Airfoil Speakers Touch, then they have a valid reason to keep Star Wars: Trench Run out as well. If Trench Run's use is okay, then there's no valid reason (absent an express written agreement between Apple and LucasArts) that similar uses by smaller developers shouldn't also be okay.

Monday, November 16, 2009

A Few Questions

I'm happy to let this die when you all are. But, a few responses to questions I've received.

Q: Didn't Bayer actually lose the rights to the Asprin trademark as a result of losing World War I?

A: Yes and no. Wikipedia is wonderful, but it's not exactly designed for legal research. The Treaty of Versailles was definitely a key factor in the decision, and there is a logical chain of events from that treaty to the loss of the trademark rights in Bayer v. United Drug. But the treaty itself didn't invalidate the trademark.

The Treaty of Versailles resulted in the term "aspirin" being considered generic, and that was later used as the factual basis for the Court decision that ruled that Bayer had lost its trademark. The Treaty of Versailles provided conclusive evidence that the term was used generically, which otherwise would have been a question of fact for the judge or jury to answer. The treaty definitely was a detriment to Bayer in the court case, but there was five year gap between the Treaty of Versailles and the conclusive loss of the the brand name.

There are ountries that were signatories to the Treaty of Versailles where Bayer still, to this day, has a trademark in the brand name Aspirin with a capital "A", even though the term is considered generic when used with a lower-case "a". In the U.S., trademark law is clear (now) that if a brand name is allowed to become generic, it is no longer protectable as a trademark (at least, unless a secondary, non-generic meaning arises, which would be a truly odd case, but definitely a possibility under current law).

Q: Couldn't you have picked a more factually case?

A: Probably not. My goal wasn't to write a treatise on U.S. trademark law. That would have been much longer and would have required a lot more research. The reason I picked Bayer from the various milestone trademark court cases is because it's recognizable and has clear modern parallels in Advil and Tylenol. You can buy both ibuprofen and acetaminophen in generic form, yet people still pay more for Advil and Tylenol. Why? Brands have value. Apple's brands have a lot of value. They're consistently ranked among the world's most recognized and valuable brands.

If today, the terms "iPhone" or "Apple Computer" became generic, it would be catastrophic for Apple as a company.

Is that likely? Of course not. But the reason that it's unlikely is because the IP section of Corporate legal departments are 99% prophylactic. They spend most of their time reviewing, and often nixing, materials that are sent to them. Because the stakes are so high, and there are few clear-cut, black and white rules, corporate legal departments will always err on the side of being too aggressive when it comes to trademarks. It's why a company like Disney will risk bad public sentiment and take action against a day care center over the use of their trademarks. It would take way more than a few negative stories in the press to equal the loss of trademark protection for Mickey Mouse.

As programmers, we often code defensively. We check for conditions that are extremely unlikely to happen, because we know that if they do, the consequences could be drastic. That's basically what corporate IP attorneys do, only they don't have the luxury of knowing exactly what the consequences are, or what exactly they have to do to make sure the consequences don't happen.

I'm not saying that's necessarily a good thing. I think that the U.S. copyright, trademark, and patent laws are in drastic need of an overhaul, but as long as they are the way they are, corporations are going to be extremely aggressive when it comes to trademark enforcement. Thinking that any corporation won't is just being silly. It's especially true with highly-recognizable, high-value brands, like Apple.

Think it's unnecessary? Ask the Psion company what they think. They're on the verge of losing the rights to the trademarked name "NetBook". They're being sued by Dell because Dell wants to be able to continue calling their low-end portables "netbooks". So, Dell is claiming that the term "netbook" has become generic (and I think they're actually right). Don't think for a second that Michael Dell wouldn't sue Apple if he thought he had a chance of prying one of Apple's valuable brands into a generic term.

MPMediaItemCollection Category

I'm currently working on the chapter for More iPhone 3 Development on accessing the iPod Library. I thought I'd throw out another bit of teaser code in the form of a category. MPMediaItemCollection is the class that is used to specify queues of songs to be played. Unfortunately, it's an immutable collection object, and there is no mutable counterpart to it. That makes it kind of a pain when you have to add, delete, or insert items into a collection, or when you want to join multiple collections together.

This category adds a bunch of methods to MPMediaItemCollection that make it easier to manipulate play queues. It doesn't make MPMediaItemCollection mutable, but it does the next best thing. It makes it easier to create new collections based on existing collections. There are methods that create new collections by inserting or deleting songs from an existing collection.

I plan to add a few additional methods for sorting and re-ordering collections for the final version of this category, so look for an update to this in the next few days.

Note: I fixed a couple of leaks after the initial posting

MPMediaItemCollection-Utils.h
#import <Foundation/Foundation.h>
#import <MediaPlayer/MediaPlayer.h>

@interface MPMediaItemCollection(Utils)
/** Returns the first media item in the collection
**/

- (MPMediaItem *)firstMediaItem;

/** Returns the last media item in the collection
**/

- (MPMediaItem *)lastMediaItem;

/** This method will return the item in this media collection at a specific index
**/

- (MPMediaItem *)mediaItemAtIndex:(NSUInteger)index;

/** Given a particular media item, this method will return the next media item in the collection.
If there are multiple copies of the same media item in the list, it will return the one
after the first occurrence.
**/

- (MPMediaItem *)mediaItemAfterItem:(MPMediaItem *)compare;

/** Returns the title of the media item at a given index.
**/

- (NSString *)titleForMediaItemAtIndex:(NSUInteger)index;

/** Returns YES if the given media item occurs at least once in this collection
**/

- (BOOL)containsItem:(MPMediaItem *)compare;

/** Creates a new collection by appending otherCollection to the end of this collection
**/

- (MPMediaItemCollection *)collectionByAppendingCollection:(MPMediaItemCollection *)otherCollection;

/** Creates a new collection by appending an array of media items to the end of this collection
**/

- (MPMediaItemCollection *)collectionByAppendingMediaItems:(NSArray *)items;

/** Creates a new collection by appending a single media item to the end of this collection
**/

- (MPMediaItemCollection *)collectionByAppendingMediaItem:(MPMediaItem *)item;

/** Creates a new collection based on this collection, but excluding the specified items.
**/

- (MPMediaItemCollection *)collectionByDeletingMediaItems:(NSArray *)itemsToRemove;

/** Creates a new collection based on this collection, but which doesn't include the specified media item.
**/

- (MPMediaItemCollection *)collectionByDeletingMediaItem:(MPMediaItem *)itemToRemove;

/** Creates a new collection based on this collection, but excluding the media item at the specified index
**/

- (MPMediaItemCollection *)collectionByDeletingMediaItemAtIndex:(NSUInteger)index;

/** Creates a new collection, based on this collection, but excluding the media items starting with
(and including) the objects at index from and ending with (and including) to.
**/

- (MPMediaItemCollection *)collectionByDeletingMediaItemsFromIndex:(NSUInteger)from toIndex:(NSUInteger)to;
@end



MPMediaItemCollection-Utils.m
#import "Simple_PlayerViewController.h"
#import "MPMediaItemCollection-Utils.h"

@implementation Simple_PlayerViewController
@synthesize titleSearch;
@synthesize playPauseButton;
@synthesize tableView;
@synthesize player;
@synthesize collection;
@synthesize nowPlaying;

#pragma mark -
- (IBAction)doTitleSearch {
if ([titleSearch.text length] == 0)
return;
MPMediaPropertyPredicate *titlePredicate =
[MPMediaPropertyPredicate predicateWithValue: titleSearch.text
forProperty: MPMediaItemPropertyTitle
comparisonType:MPMediaPredicateComparisonContains
]
;
MPMediaQuery *query = [[MPMediaQuery alloc] initWithFilterPredicates:[NSSet setWithObject:titlePredicate]];

if ([[query items] count] > 0) {
if (collection)
self.collection = [collection collectionByAppendingMediaItems:[query items]];
else
self.collection = [MPMediaItemCollection collectionWithItems:[query items]];

collectionModified = YES;
[self.tableView reloadData];
}

[query release];
titleSearch.text = @"";
[titleSearch resignFirstResponder];

}


- (IBAction)showMediaPicker {
MPMediaPickerController *picker = [[MPMediaPickerController alloc] initWithMediaTypes:MPMediaTypeMusic];
picker.delegate = self;
[picker setAllowsPickingMultipleItems:YES];
picker.prompt = NSLocalizedString(@"Select items to play", @"Select items to play");
[self presentModalViewController:picker animated:YES];
[picker release];
}


- (IBAction)backgroundClick {
[titleSearch resignFirstResponder];
}


- (IBAction)previousTrack {
[player skipToPreviousItem];
}


- (IBAction)nextTrack {
[player skipToNextItem];
}


- (IBAction)playOrPause {
if (player.playbackState == MPMusicPlaybackStatePlaying) {
[player pause];
[playPauseButton setBackgroundImage:[UIImage imageNamed:@"play.png"] forState:UIControlStateNormal];
}

else {
[player play];
[playPauseButton setBackgroundImage:[UIImage imageNamed:@"pause.png"] forState:UIControlStateNormal];
}

[self.tableView reloadData];
}


- (IBAction)removeTrack:(id)sender {
NSUInteger index = [sender tag];
MPMediaItem *itemToDelete = [collection mediaItemAtIndex:index];
if ([itemToDelete isEqual:nowPlaying])
[player skipToNextItem];
MPMediaItemCollection *newCollection = [collection collectionByDeletingMediaItemAtIndex:index];
self.collection = newCollection;
collectionModified = YES;

NSUInteger indices[] = {0, index};
NSIndexPath *deletePath = [NSIndexPath indexPathWithIndexes:indices length:2];
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:deletePath] withRowAnimation:UITableViewRowAnimationFade];
}


#pragma mark -
- (void)viewDidLoad {
MPMusicPlayerController *thePlayer = [MPMusicPlayerController iPodMusicPlayer];
self.player = thePlayer;
[thePlayer release];

if (player.playbackState == MPMusicPlaybackStatePlaying) {
[playPauseButton setBackgroundImage:[UIImage imageNamed:@"pause.png"] forState:UIControlStateNormal];
MPMediaItemCollection *newCollection = [MPMediaItemCollection collectionWithItems:[NSArray arrayWithObject:[player nowPlayingItem]]];
self.collection = newCollection;
self.nowPlaying = [player nowPlayingItem];
}

else {
[playPauseButton setBackgroundImage:[UIImage imageNamed:@"play.png"] forState:UIControlStateNormal];
}





NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver: self
selector: @selector (nowPlayingItemChanged:)
name: MPMusicPlayerControllerNowPlayingItemDidChangeNotification
object: player
]
;

[player beginGeneratingPlaybackNotifications];
}


- (void)viewDidUnload {
self.titleSearch = nil;
self.playPauseButton = nil;
self.tableView = nil;
[super viewDidUnload];
}


- (void)dealloc {
[titleSearch release];
[playPauseButton release];
[tableView release];
[player release];
[collection release];
[super dealloc];
}


#pragma mark -
#pragma mark Media Picker Delegate Methods
- (void) mediaPicker: (MPMediaPickerController *) mediaPicker
didPickMediaItems: (MPMediaItemCollection *) theCollection
{
[self dismissModalViewControllerAnimated: YES];

if (collection == nil){
self.collection = theCollection;
[player setQueueWithItemCollection:collection];
[player setNowPlayingItem:[collection firstMediaItem]];
self.nowPlaying = [collection firstMediaItem];
[player play];
[playPauseButton setBackgroundImage:[UIImage imageNamed:@"pause.png"] forState:UIControlStateNormal];
}

else {
self.collection = [collection collectionByAppendingCollection:theCollection];
}


collectionModified = YES;
[self.tableView reloadData];
}


- (void) mediaPickerDidCancel: (MPMediaPickerController *) mediaPicker {
[self dismissModalViewControllerAnimated: YES];
}


#pragma mark -
#pragma mark Player Notification Methods
- (void)nowPlayingItemChanged:(NSNotification *)notification {
if (![collection containsItem:[player nowPlayingItem]]) {
if (collectionModified) {
[player play];
[player setQueueWithItemCollection:collection];
[player setNowPlayingItem:[collection mediaItemAfterItem:nowPlaying]];
[playPauseButton setBackgroundImage:[UIImage imageNamed:@"pause.png"] forState:UIControlStateNormal];
}

else if ([player nowPlayingItem] != nil) {
MPMediaItem *nowPlayingItem = [player nowPlayingItem];
self.collection = [collection collectionByAppendingMediaItem:nowPlayingItem];
}

}

self.nowPlaying = [player nowPlayingItem];

if (nowPlaying == nil)
[playPauseButton setBackgroundImage:[UIImage imageNamed:@"play.png"] forState:UIControlStateNormal];

collectionModified = NO;
[self.tableView reloadData];
}


#pragma mark -
#pragma mark Table View Methods
- (NSInteger)tableView:(UITableView *)theTableView numberOfRowsInSection:(NSInteger)section {
return [collection count];
}


- (UITableViewCell *)tableView:(UITableView *)theTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *identifier = @"Music Queue Cell";
UITableViewCell *cell = [theTableView dequeueReusableCellWithIdentifier:identifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier] autorelease];

UIButton *removeButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *removeImage = [UIImage imageNamed:@"remove.png"];
[removeButton setBackgroundImage:removeImage forState:UIControlStateNormal];
[removeButton setFrame:CGRectMake(0.0, 0.0, removeImage.size.width, removeImage.size.height)];
[removeButton addTarget:self action:@selector(removeTrack:) forControlEvents:UIControlEventTouchUpInside];
cell.accessoryView = removeButton;
}

cell.textLabel.text = [collection titleForMediaItemAtIndex:[indexPath row]];
if ([nowPlaying isEqual:[collection mediaItemAtIndex:[indexPath row]]]) {
cell.textLabel.font = [UIFont boldSystemFontOfSize:21.0];
if (player.playbackState == MPMusicPlaybackStatePlaying)
cell.imageView.image = [UIImage imageNamed:@"play_small.png"];
else
cell.imageView.image = [UIImage imageNamed:@"pause_small.png"];

}

else {
cell.textLabel.font = [UIFont systemFontOfSize:21.0];
cell.imageView.image = [UIImage imageNamed:@"empty.png"];
}


cell.accessoryView.tag = [indexPath row];


return cell;
}


- (void)tableView:(UITableView *)theTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
MPMediaItem *selected = [collection mediaItemAtIndex:[indexPath row]];

if (collectionModified) {
[player setQueueWithItemCollection:collection];
}


[player setNowPlayingItem:selected];
[player play];

[playPauseButton setBackgroundImage:[UIImage imageNamed:@"pause.png"] forState:UIControlStateNormal];
[self.tableView reloadData];
}


- (CGFloat)tableView:(UITableView *)theTableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 34;
}


@end