Saturday, April 4, 2009

Robotic Space Rock and other games fell victims of the DMCA

Today, I received a notification from google stating:

This is a notification that the application, Robotic Space Rock with package ID org.teacake.monolith.apk has been removed from Android Market due to a violation of the Developer Content Policy. Please review the Content Policies and Business and Program Policies before you create or upload additional applications. Note that repeated violations may result in a suspension of your Android Market Publisher account.

For more information, or to contact us, please reply to this email, or visit the Android Market Help Center.

Thanks,

The Android Market Team


Now my application looks like this in the developer console:



Additionaly they forwarded me a four page fax they received from Norris, McNaughlin &
Marcus P.A. who represent the Tetris Company.

Here's the fax:





As you can see there are six applications listed for infringement:
Blocks, Cubik, Net Tetris, Netblocks, Robotics Space Rock, Tetroid

Actually, my application is called "Robotic Space Rock", not Robotics Space Rock. However I do not use the tetris name anywhere in my application, and I do not copy or reuse tetris related art or sound. The game mechanics are also quite different. And the application is free.

The question is, what should I do? I am not an american citizen nor do I live in the U.S. What's your take on this?

Thursday, February 26, 2009

Some lessons learned from the android market

Sometime in December, I released a game I wrote, Robotic Space Rock (RSR for short) in the android market. This lead to a small stream of emails from angry users, claiming that the app I published was not working. I unpublished RSR and started scratching my head and trying to figure out what went wrong. My first mistake was that I only tested RSR on the emulator, because at the time I had no access to a T-Mobile G1. As a matter of fact, I still don't because, Android Developer Phone (ADP) is not yet available in Greece where I live, and Google has not given a timeframe for the release of ADP in my region. So I searched the internet in order to find out why my users got a blank screen. A few days later, I found the problem, and corrected it. I asked for volunteers to test the corrected application before I resubmit it to the android market. Of course, my app was already buried (under 2 stars rating), so when I resubmitted, most guys were reluctant to try it out. Despite this, I continued to release updates. After a few days, I got an email from google. They asked me to rename it. Initially it was called MonolithAndroid but it seems that you are not allowed to have the term android in your application title, because it violates android market policies. So I had to rename to RSR and re-release. As days passed by, I got everything from 1 to 5 stars, and users saying that they hate or love RSR and everything in between. Some claimed that the background music slowed the game down, and others that RSR kept force closing. One of the worst things about the android market is that you cannot see the user comments of the users in the developer console, as someone would expect. This void was filled by the excellent cyrket web site by saurik. When the paid apps were introduced about a week ago, a flood of apps appeared and my app was further pushed down in the popularity rank. A few days ago, some apps with screenshots became visible through the android market website, but they were only the top rated apps. No chance for RSR appearing on the android market website, it seems...
So what are my conclusions for now:
1. Never, ever release an app, unless you thoroughly test it on a real device. If you don't get it passably right the first time, your app will be buried.
2. The phone memory limit is a severe limitation. My application has some original music. I wanted to add a couple of songs or more but I do not want to, because that would make it bigger and big apps, especially games are not going to stay on the phone for long. So keep your app small (under 1 MB) in order to maximize the chance of your application staying on the phone. Google should permit program installation on the SD card, increase the APK size limit and maybe increase the amount of internal storage to 1 GB or more.
3. Games are not that popular on the G1. It seems that geeks don't like gaming so much, or the games currently available are not so hot. So focus on applications, especially if you want to make some $$$.
4. Sound support for android is not good yet. If you use SoundPool, you increase the chance of your application hanging, making the user unhappy. I think that sound problems should be corrected immediately, because that affects all kinds of applications and especially games.
In my next blog entry I will try to compare android to iphone and symbian developer tools and market publishing. Stay tuned!

Saturday, February 7, 2009

Robotic Space Rock 1.0.8 beta released!

I just released Robotic Space Rock 1.0.8 beta on the android market. Here's the changelog:
1. Added easy mode. Now the screen spins in Normal and Expert Difficulty. The screen does not move in Easy Difficulty.
2. Changed texture size to 32x32 from 64x64 in order to improve performance.
3. Changed the way explosions are drawn.
4. Added a game over sound effect.
5. Changed the way that playfield rotation is calculated by introducing a new LinearInterpolator class.
As always your remarks, bug reports and suggestions are welcome!

Thursday, February 5, 2009

A class for playing music and sound effects revisited

A while back, I published a class that could be used for playing music and sound effects. That class used the android.media.MediaPlayer class and was developed against a pre 1.0 android SDK version. The media player object is good for playing music, but it is not suitable for playing short sound effects. It is a heavyweight object and a bit unstable, too. But a new class called SoundPool (found in android.media package again) is available. This class is good for playing sound effects, but currently does not support .ogg or .mp3 files. Since I wanted my game to have both music and sound, I decided to design a class called SoundPoolManager which utilises both MediaPlayer and SoundPool objects. SoundPool objects are used to play sound effects and MediaPlayer objects are used to play back looping music.
SoundPoolManager creates a new thread and uses a LinkedList to queue sound events. So when you want to play a sound, you create a SoundPoolEvent and push it to the sound event queue. The SoundPoolManager thread picks up the queued events and plays them back for you. Time to show you some code:

 

This is the Sound interface used to abstract soundplaying functions.

package org.teacake.monolith.apk;
public interface Sound {

  public void addSound(int resid, boolean isLooping);
  public void startSound();
  public void stopSound();
 public void stopSound(int resid);
  public void playSound(int resid);
  public void startMusic(int resid);
  public void stopMusic(int resid);
  public void pauseMusic(int resid);
  public void resumeMusic(int resid);
}


And now for the main dish, SoundPoolManager class

package org.teacake.monolith.apk;
import android.media.SoundPool;
import android.util.Log;
class SoundPoolEvent
{
public SoundPoolEvent(int eventType,int eventSound)
{
this.eventType = eventType;
this.eventSound = eventSound;
}
public int eventType;
public int eventSound;

public static final int SOUND_PLAY=0;
public static final int SOUND_STOP=1;
public static final int SOUND_MUSIC_PLAY=2;
public static final int SOUND_MUSIC_PAUSE=3;
public static final int SOUND_MUSIC_STOP=4;
public static final int SOUND_MUSIC_RESUME=5;
}
class SoundStatus
{
public SoundStatus()
{

}
public static final int STATUS_LOOPING_NOT_STARTED=0;
public static final int STATUS_LOOPING_PAUSED=1;
public static final int STATUS_LOOPING_PLAYING=2;


}
public class SoundPoolManager extends Thread implements Sound
{
SoundPoolManager(android.content.Context context)
{
this.context = context;
soundEvents = new java.util.LinkedList();
sounds = new java.util.HashMap();
handles = new java.util.HashMap();
mediaPlayers = new java.util.HashMap();
isRunning = false;

}
public void addSound(int resid, boolean isLooping)
{

sounds.put(resid, new Boolean(isLooping));
if(isLooping)
{
try
{
android.media.MediaPlayer mp = android.media.MediaPlayer.create(context, resid);
mp.setLooping(true);
mp.seekTo(0);
//mp.prepare();
mediaPlayers.put(resid, mp);


//mp.seekTo(0);
//mp.setAudioStreamType(android.media.AudioManager.STREAM_MUSIC);
}
catch(Exception e)
{
Log.d("ERROR",e.getMessage());
}



}
}

public void run()
{
android.media.MediaPlayer mp = null;
while(isRunning)
{
try
{
while(soundEvents.size()>0)
{
SoundPoolEvent event = soundEvents.remove();
if(event!=null)
{
switch(event.eventType)
{
case SoundPoolEvent.SOUND_PLAY:
android.media.AudioManager mgr = (android.media.AudioManager) context.getSystemService(android.content.Context.AUDIO_SERVICE);
int streamVolume = mgr.getStreamVolume(android.media.AudioManager.STREAM_MUSIC);
soundPool.play(handles.get( event.eventSound).intValue(), streamVolume, streamVolume, 1, 0, 1.0f);

break;
case SoundPoolEvent.SOUND_STOP:

break;
case SoundPoolEvent.SOUND_MUSIC_PLAY:
currentPlayer = event.eventSound;
if(sounds.get(currentPlayer)!=null)
{
mp = mediaPlayers.get(currentPlayer);

if(!mp.isPlaying())
{
mp.seekTo(0);
mp.start();
}
}

break;
case SoundPoolEvent.SOUND_MUSIC_STOP:
currentPlayer = event.eventSound;
mp = mediaPlayers.get(currentPlayer);
mp.pause();
break;
case SoundPoolEvent.SOUND_MUSIC_PAUSE:
currentPlayer = event.eventSound;
mp = mediaPlayers.get(currentPlayer);
mp.pause();
break;
case SoundPoolEvent.SOUND_MUSIC_RESUME:
currentPlayer = event.eventSound;
mp = mediaPlayers.get(currentPlayer);
mp.start();
break;

}

}
}
}
catch(Exception e)
{
//Log.d("Error",e.getMessage());
}

try
{
this.wait(100);
//java.lang.Thread.currentThread().sleep(100);
}
catch(Exception e)
{

}
}
}


public void startSound()
{
this.soundPool = new android.media.SoundPool(this.sounds.size(),android.media.AudioManager.STREAM_MUSIC,100);
java.util.Iterator iterator = sounds.keySet().iterator();

while(iterator.hasNext())
{
int soundid = iterator.next().intValue();
int soundhandle = this.soundPool.load(this.context, soundid, 1);
handles.put(new Integer(soundid), new Integer(soundhandle));
}

isRunning=true;
this.start();
}
public void stopSound()
{
java.util.Iterator iterator = sounds.keySet().iterator();

while(iterator.hasNext())
{

int soundid = iterator.next().intValue();
if(this.sounds.get(soundid).booleanValue())
{
android.media.MediaPlayer mp = mediaPlayers.get(soundid);
mp.stop();
mp.release();
mp=null;
}
else
{
this.soundPool.pause( this.handles.get(soundid).intValue());
this.soundPool.stop(this.handles.get(soundid).intValue());

}


}

isRunning=false;
this.soundPool.release();
}

public int currentPlayer;
private boolean isRunning;
private java.util.HashMap sounds;
private java.util.HashMap handles;
private android.content.Context context;
private java.util.LinkedList soundEvents;
private java.util.HashMap mediaPlayers;
public void stopSound(int resid)
{
if(soundEvents!=null)
{
try
{
soundEvents.add(new SoundPoolEvent(SoundPoolEvent.SOUND_STOP,resid));
}
catch(Exception e)
{

}
}
}
public void playSound(int resid)
{
if(soundEvents!=null)
{
try
{
soundEvents.add(new SoundPoolEvent(SoundPoolEvent.SOUND_PLAY,resid));
this.notify();
}
catch(Exception e)
{

}
}
}
public void startMusic(int resid)
{
if(soundEvents!=null)
{
try
{
soundEvents.add(new SoundPoolEvent(SoundPoolEvent.SOUND_MUSIC_PLAY,resid));
this.notify();
}
catch(Exception e)
{

}
}
}
public void stopMusic(int resid)
{
if(soundEvents!=null)
{
try
{
soundEvents.add(new SoundPoolEvent(SoundPoolEvent.SOUND_MUSIC_STOP,resid));
this.notify();
}
catch(Exception e)
{

}
}
}
public void pauseMusic(int resid)
{
if(soundEvents!=null)
{
try
{
soundEvents.add(new SoundPoolEvent(SoundPoolEvent.SOUND_MUSIC_PAUSE,resid));
this.notify();
}
catch(Exception e)
{

}
}
}
public void resumeMusic(int resid)
{
if(soundEvents!=null)
{
try
{
soundEvents.add(new SoundPoolEvent(SoundPoolEvent.SOUND_MUSIC_RESUME,resid));
this.notify();
}
catch(Exception e)
{

}
}
}
SoundPool soundPool;


}


How do you use it?
First create an instance of the class.
Then add the resources to the SoundPoolManager (Sounds and music tracks)
Then start the SoundManager thread.
Then use the playSound() method to play a sound effect or playMusic() to play a song.

SoundPoolManager m = new SoundPoolManager(context);
m.addSound(R.raw.bang, false);
m.addSound(R.raw.mysong,true);
m.startSound();
m.playSound(R.raw.bang);
m.playMusic(R.raw.mysong);

If you want to pause the music use the pause call.

Use the stopSound() method to stop the sound thread.
The code is crude and the method names are not quite appropriate but it is published under the assumption that you may find it useful. You can take a look at the Robotic Space Rock code found at http://code.google.com/p/monolithandroid for actual examples.

Wednesday, January 28, 2009

MonolithAndroid renamed to Robotic Space Rock

Today, I received an email from google asking me to rename the game I published for free in the android market. The game is called MonolithAndroid. Was called, actually, because from now on, it shall be called Robotic Space Rock! Yes, they informed me that I cannot use the word android, becase users may think that it is officialy endorsed from google, so I decided to drop the MonolithAndroid name alltogether and use the new improved and b-moviey name Robotic Space Rock.

Thursday, January 22, 2009

How to display a license agreement on your android application.

It is usually good practice to present the user with a license agreement the first time it uses an app. You can show the user a preferably short text with the license agreement. A license agreement gives you some legal protection if your application causes a problem to someone. You can use a checkbox to indicate that the user accepted the license agreement and proceed to the application.

We can use the shared preference mechanism to check if the user has agreed to the license agreement during the activity. If the user has previously agreed to the license then we proceed as normal. Otherwise, we show him the EULA. If the user checks the "I accept the license agreement" checkbox and presses OK, then we write to the preferences that the user accepted the license. If the user presses Cancel, we exit the app. You can see this in the following code excerpt

private android.widget.CheckBox checkboxAcceptLicense;
private android.widget.Button buttonOK;
private android.widget.Button buttonCancel;
private android.widget.TextView textviewLicense;
public SharedPreferences.Editor prefsEditor;
public SharedPreferences prefs;
public android.view.View licenseView;
private boolean soundInitialized;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
soundInitialized = false;
prefs = this.getPreferences(android.content.Context.MODE_PRIVATE);
prefsEditor = prefs.edit();
if(!prefs.getBoolean("LicenseAccepted", false))
{

//this.licenseView = this.findViewById(R.layout.licenseagreement);
this.licenseView = View.inflate(this, R.layout.licenseagreement, null);
this.setContentView(licenseView);
this.checkboxAcceptLicense = (android.widget.CheckBox)this.findViewById(R.id.checkLicenseAgreement);
this.textviewLicense = (android.widget.TextView)this.findViewById(R.id.textviewLicenseAgreement);
this.buttonOK= (android.widget.Button)this.findViewById(R.id.buttonOK);
this.buttonCancel = (android.widget.Button)this.findViewById(R.id.buttonCancel);
this.buttonCancel.setOnClickListener(
new View.OnClickListener()
{
public void onClick(View view)
{
exitNotAcceptedApplication();
}
}
);
this.buttonOK.setOnClickListener(
new View.OnClickListener()
{
public void onClick(View view)
{
if(checkboxAcceptLicense.isChecked())
{
prefsEditor.putBoolean("LicenseAccepted", true);
prefsEditor.commit();
licenseView.setVisibility(View.INVISIBLE);

initActivity();
}

}
}
);
}
else
{
initActivity();
}



}

the initActivity() method contains the actual code that you would normally execute on the onCreate() method of your Activity. The layout file for the license agreement is the following (licenseagreement.xml):

xml version="1.0" encoding="utf-8"?>

<LinearLayout

android:id="@+id/widget24"

android:layout_width="fill_parent"

android:layout_height="fill_parent"

android:background="#ffffffff"

android:padding="0px"

xmlns:android="http://schemas.android.com/apk/res/android"

android:orientation="vertical"

>


<TextView

android:id="@+id/textviewLicenseAgreement"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:padding="0px"

android:text="Monolith Android Licence Agreement. This software package is provided to you, the end user, by the sofware package creator, Tasos Kleisas, as is. The software package is provided free of charge by the developer. However, the means of delivering the software to your device, (mobile phone, computer or other) may induce a service fee. The developer cannot be held responsible for any damage or loss that you may suffer by the use or installation of this application package to your device. Use at your own risk. The code, sound effects, and music are copyrighted by Tasos Kleisas (C) 2007-2009."

android:textSize="14sp"

android:textColor="#000000"

android:typeface="normal"

android:textStyle="normal"

android:layout_weight="0"

android:layout_gravity="left"

>

TextView>


<CheckBox

android:id="@+id/checkLicenseAgreement"

android:layout_width="fill_parent"

android:layout_height="wrap_content"


android:text="I accept the license agreement"

android:textColor="#000000"

android:textSize="12sp"

android:typeface="normal"

android:textStyle="normal"

android:checked="false"



android:orientation="horizontal"

>

CheckBox>

<TableRow

android:id="@+id/widget32"

android:layout_width="fill_parent"


android:layout_height="wrap_content"

android:orientation="horizontal"

>

<Button

android:id="@+id/buttonOK"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:padding="10px"

android:text="OK"

android:textSize="14sp"

android:typeface="normal"

android:textStyle="normal"

android:layout_weight="0"

android:layout_gravity="left"

>

Button>

<Button

android:id="@+id/buttonCancel"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:padding="10px"

android:text="Cancel"

android:textSize="14sp"

android:typeface="normal"

android:textStyle="normal"

android:layout_weight="0"

android:layout_gravity="left"

>


Button>

TableRow>

LinearLayout>

Unfortunately, as you can see the server messed up the xml layout a bit. But you can find the full code at http://code.google.com/p/monolithandroid/source/browse/

Android 3D graphics and SurfaceView

If you want to use OpenGL/ES, you have to use a SurfaceView class. The problem is that there are various parameters for the SurfaceView class. This, for example may work fine in the emulator:

getHolder().setType(android.view.SurfaceHolder.SURFACE_TYPE_NORMAL);

on a real device, however, you may get a blank screen, or very slow redraws, so you should use this:

getHolder().setType(android.view.SurfaceHolder.
SURFACE_TYPE_GPU);

or even this:

getHolder().setType(android.view.SurfaceHolder.
SURFACE_TYPE_HARDWARE);

So i came up with this solution for the SurfaceView initialization:

public GameSurfaceView(Context context,GameOverlay overlay,Sound soundManager)
{
super(context);
this.soundManager = soundManager;
this.context = context;
this.overlay = overlay;
this.viewType = GLThread.VIEW_INTRO;
this.gameType = Game.GAME_MONOLITH;
getHolder().addCallback(this);
try
{
getHolder().setType(android.view.SurfaceHolder.
SURFACE_TYPE_GPU);
}
catch(Exception e)
{
try
{
getHolder().setType(android.view.SurfaceHolder.
SURFACE_TYPE_HARDWARE);
}
catch(Exception e2)
{
try
{
getHolder().setType(android.view.SurfaceHolder.SURFACE_TYPE_NORMAL);
}
catch(Exception e3)
{
}
}
}
}

So in the code above, we first try to get a hardware accelerated surface (fastest), if that fails a gpu memory surface(still fast) and if that fails a normal software surface.

Search the web