diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml
index 4518241..ebb6dd1 100644
--- a/android/AndroidManifest.xml
+++ b/android/AndroidManifest.xml
@@ -15,9 +15,11 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
       	</activity>
-        <service android:name=".RockboxService"/>
-
+        <activity android:name=".thememanager.ThemeDownloaderActivity" />
+        <activity android:name=".thememanager.ThemeInstallerActivity" />
+        <service android:name=".RockboxService" />
     </application>
 <uses-sdk android:minSdkVersion="4" />
-<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
+<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+<uses-permission android:name="android.permission.INTERNET" />
 </manifest> 
diff --git a/android/res/layout/theme_information.xml b/android/res/layout/theme_information.xml
new file mode 100644
index 0000000..14915dc
--- /dev/null
+++ b/android/res/layout/theme_information.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  android:layout_width="fill_parent"
+  android:layout_height="fill_parent"
+  android:orientation="vertical"
+  android:gravity="center_horizontal">
+	<ImageSwitcher
+	  android:id="@+id/mainImages"
+	  android:layout_width="wrap_content"
+	  android:layout_height="wrap_content"/>
+	<ImageView
+	  android:id="@+id/rating"
+	  android:layout_width="wrap_content"
+	  android:layout_height="wrap_content" />
+	<TextView
+	  android:id="@+id/description"
+	  android:layout_width="wrap_content"
+	  android:layout_height="wrap_content" />
+	<Button
+	  android:id="@+id/installButton"
+	  android:text="Install"
+	  android:layout_width="wrap_content"
+	  android:layout_height="wrap_content"
+	  android:layout_gravity="bottom|center_horizontal" />
+</LinearLayout>
\ No newline at end of file
diff --git a/android/res/layout/themelist_item.xml b/android/res/layout/themelist_item.xml
new file mode 100644
index 0000000..262cfbc
--- /dev/null
+++ b/android/res/layout/themelist_item.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="horizontal"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <ImageView android:id="@+id/themelist_item_icon"
+        android:layout_width="48dip"
+        android:layout_height="48dip" />
+
+    <TextView android:id="@+id/themelist_item_text"
+        android:layout_gravity="center_vertical"
+        android:layout_width="0dip"
+        android:layout_weight="1.0"
+        android:layout_height="wrap_content"
+        android:hint="&lt;No information available&gt;"
+        android:paddingLeft="5dip" />
+</LinearLayout>
\ No newline at end of file
diff --git a/android/res/menu/rockboxactivity_menu.xml b/android/res/menu/rockboxactivity_menu.xml
new file mode 100644
index 0000000..e424e33
--- /dev/null
+++ b/android/res/menu/rockboxactivity_menu.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu
+  xmlns:android="http://schemas.android.com/apk/res/android">
+<item android:title="Download themes" android:id="@+id/themedownloader"></item>
+</menu>
diff --git a/android/src/org/rockbox/RockboxActivity.java b/android/src/org/rockbox/RockboxActivity.java
index fb41b90..2736d91 100644
--- a/android/src/org/rockbox/RockboxActivity.java
+++ b/android/src/org/rockbox/RockboxActivity.java
@@ -21,13 +21,21 @@
 
 package org.rockbox;
 
+import org.rockbox.thememanager.ThemeListActivity;
+
 import android.app.Activity;
 import android.content.Intent;
 import android.os.Bundle;
 import android.util.Log;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
 import android.view.ViewGroup;
 import android.view.Window;
 import android.view.WindowManager;
+import android.view.ContextMenu.ContextMenuInfo;
 
 public class RockboxActivity extends Activity 
 {
@@ -64,8 +72,7 @@ public class RockboxActivity extends Activity
                 }
                 /* drawing needs to happen in ui thread */
                 runOnUiThread(new Runnable() 
-                {    @Override
-                    public void run() {
+                {    public void run() {
                         setContentView(RockboxService.fb);
                         RockboxService.fb.invalidate();
                     }
@@ -117,6 +124,25 @@ public class RockboxActivity extends Activity
         RockboxService.fb.suspend();
     }
 
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+    	MenuInflater menuInflater = getMenuInflater();
+    	menuInflater.inflate(R.menu.rockboxactivity_menu, menu);
+
+    	return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+    	switch (item.getItemId()) {
+    	case R.id.themedownloader:
+    		startActivity(new Intent(this, ThemeListActivity.class));
+    		return true;
+		default:
+			return super.onOptionsItemSelected(item);
+    	}
+    }
+
     private void LOG(CharSequence text)
     {
         Log.d("Rockbox", (String) text);
diff --git a/android/src/org/rockbox/RockboxFramebuffer.java b/android/src/org/rockbox/RockboxFramebuffer.java
index ddc4a4d..089729f 100644
--- a/android/src/org/rockbox/RockboxFramebuffer.java
+++ b/android/src/org/rockbox/RockboxFramebuffer.java
@@ -99,11 +99,17 @@ public class RockboxFramebuffer extends View
 
     public boolean onKeyDown(int keyCode, KeyEvent event)
     {
+    	if (keyCode == KeyEvent.KEYCODE_MENU)
+    		return false; /* HACK! */
+
         return buttonHandler(keyCode, true);
     }
 
     public boolean onKeyUp(int keyCode, KeyEvent event)
     {
+    	if (keyCode == KeyEvent.KEYCODE_MENU)
+    		return false; /* HACK! */
+
         return buttonHandler(keyCode, false);
     }
 
diff --git a/android/src/org/rockbox/RockboxPCM.java b/android/src/org/rockbox/RockboxPCM.java
index a3d09a4..bedaeb8 100644
--- a/android/src/org/rockbox/RockboxPCM.java
+++ b/android/src/org/rockbox/RockboxPCM.java
@@ -148,7 +148,6 @@ public class RockboxPCM extends AudioTrack
             buf = new byte[max_len*3/4];
             refill_mark = max_len - buf.length;
         }
-        @Override
         public void onMarkerReached(AudioTrack track) 
         {
             /* push new data to the hardware */
@@ -186,7 +185,6 @@ public class RockboxPCM extends AudioTrack
             }
         }
 
-        @Override
         public void onPeriodicNotification(AudioTrack track) 
         {            
         }
diff --git a/android/src/org/rockbox/RockboxService.java b/android/src/org/rockbox/RockboxService.java
index 1416886..d964979 100644
--- a/android/src/org/rockbox/RockboxService.java
+++ b/android/src/org/rockbox/RockboxService.java
@@ -128,40 +128,33 @@ public class RockboxService extends Service
            BufferedOutputStream dest = null;
            BufferedInputStream is = null;
            ZipEntry entry;
-           File file = new File("/data/data/org.rockbox/" +
+           File miscFile = new File("/data/data/org.rockbox/" +
            		"lib/libmisc.so");
            /* use arbitary file to determine whether extracting is needed */
-           File file2 = new File("/data/data/org.rockbox/" +
-           		"app_rockbox/rockbox/codecs/mpa.codec");
-           if (!file2.exists() || (file.lastModified() > file2.lastModified()))
+           File file2 = new File(getFilesDir(), "/.rockbox/codecs/mpa.codec");
+           if (!file2.exists() || (miscFile.lastModified() > file2.lastModified()))
            {
-               ZipFile zipfile = new ZipFile(file);
+               ZipFile zipfile = new ZipFile(miscFile);
                Enumeration<? extends ZipEntry> e = zipfile.entries();
-               File folder;
+               byte data[] = new byte[BUFFER];
                while(e.hasMoreElements()) 
                {
                   entry = (ZipEntry) e.nextElement();
                   LOG("Extracting: " +entry);
+                  File file = new File(getFilesDir(), entry.getName());
                   if (entry.isDirectory())
-                  {
-                      folder = new File(entry.getName());
-                      LOG("mkdir "+ entry);
-                      try {
-                          folder.mkdirs();
-                      } catch (SecurityException ex) {
-                          LOG(ex.getMessage());
-                      }
                       continue;
-                  }
+
+                  try {
+                      file.getParentFile().mkdirs();
+                  } catch (SecurityException ex) {
+                      LOG(ex.getMessage());
+                  }                  
+
                   is = new BufferedInputStream(zipfile.getInputStream(entry),
                           BUFFER);
                   int count;
-                  byte data[] = new byte[BUFFER];
-                  folder = new File(new File(entry.getName()).getParent());
-                  LOG("" + folder.getAbsolutePath());
-                  if (!folder.exists())
-                      folder.mkdirs();
-                  FileOutputStream fos = new FileOutputStream(entry.getName());
+                  FileOutputStream fos = new FileOutputStream(file);
                   dest = new BufferedOutputStream(fos, BUFFER);
                   while ((count = is.read(data, 0, BUFFER)) != -1)
                      dest.write(data, 0, count);
diff --git a/android/src/org/rockbox/thememanager/Theme.java b/android/src/org/rockbox/thememanager/Theme.java
new file mode 100644
index 0000000..c529366
--- /dev/null
+++ b/android/src/org/rockbox/thememanager/Theme.java
@@ -0,0 +1,84 @@
+package org.rockbox.thememanager;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class Theme implements Parcelable {
+	private String name;
+	private int fileSize;
+	private String images[];
+	private String downloadLink;
+	private HashMap<String, String> properties;
+
+	public Theme(Parcel in) {
+		name = in.readString();
+		fileSize = in.readInt();
+		images = in.createStringArray();
+		downloadLink = in.readString();
+		properties = (HashMap<String, String>) in.readSerializable();
+	}
+
+	public void writeToParcel(Parcel out, int flags) {
+		out.writeString(name);
+		out.writeInt(fileSize);
+		out.writeStringArray(images);
+		out.writeString(downloadLink);
+		out.writeSerializable(properties);
+	}
+
+	public int describeContents() {
+		return 0;
+	}
+
+    public static final Parcelable.Creator<Theme> CREATOR = new Parcelable.Creator<Theme>() {
+	     public Theme createFromParcel(Parcel in) {
+	         return new Theme(in);
+	     }
+	
+	     public Theme[] newArray(int size) {
+	         return new Theme[size];
+	     }
+	 };		
+
+	public Theme(Map<String, String> properties) {
+		name = properties.get("name");
+		fileSize = Integer.parseInt(properties.get("size"));
+		images = new String[2];
+		images[0] = ThemeListActivity.formatUrl(properties.get("image"));
+		images[1] = ThemeListActivity.formatUrl(properties.get("image2"));
+		downloadLink = ThemeListActivity.formatUrl(properties.get("archive"));
+		this.properties = new HashMap<String,String>();
+		for (String key : new String[] {"author", "version", "about", "pass_release", "pass_current"})
+			this.properties.put(key, properties.get(key));
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public int getFileSize() {
+		return fileSize;
+	}
+
+	public String getImage(int index) {
+		if (index >= images.length)
+			throw new IndexOutOfBoundsException();
+
+		return images[index];
+	}
+
+	public int getImageCount() {
+		return images.length;
+	}
+	
+	public String getDownloadLink() {
+		return downloadLink;
+	}
+
+	public String getProperty(String key) {
+		return properties.get(key);
+	}
+}
\ No newline at end of file
diff --git a/android/src/org/rockbox/thememanager/ThemeInstallerActivity.java b/android/src/org/rockbox/thememanager/ThemeInstallerActivity.java
new file mode 100644
index 0000000..68362bf
--- /dev/null
+++ b/android/src/org/rockbox/thememanager/ThemeInstallerActivity.java
@@ -0,0 +1,180 @@
+package org.rockbox.thememanager;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.rockbox.R;
+import org.rockbox.utils.DrawableManager;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Handler.Callback;
+import android.text.Html;
+import android.util.Log;
+import android.view.View;
+import android.view.Window;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.ImageSwitcher;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class ThemeInstallerActivity extends Activity {
+	private Theme theme;
+	private DrawableManager drawableManager;
+	private static final int BUFFER_SIZE = 8*1024;
+
+	public ThemeInstallerActivity() {
+		drawableManager = new DrawableManager();
+	}
+
+	@Override
+	protected void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+
+		Intent intent = getIntent();
+		if (intent != null && intent.hasExtra("theme"))
+			this.theme = intent.getParcelableExtra("theme");
+
+		setContentView(R.layout.theme_information);
+
+		ImageSwitcher mainImages = (ImageSwitcher) findViewById(R.id.mainImages);
+		ImageView rating = (ImageView) findViewById(R.id.rating);
+		TextView description = (TextView) findViewById(R.id.description);
+		Button installButton = (Button) findViewById(R.id.installButton);
+
+		StringBuilder desc = new StringBuilder();
+		desc.append("<b>Submitter</b>: ");
+		desc.append(theme.getProperty("author"));
+		desc.append("<br /><b>Submitted</b>: ");
+		desc.append("&lt;TODO&gt;");
+		desc.append("<br /><b>Description</b>: ");
+		desc.append(theme.getProperty("about"));
+
+		for (int i=0; i < theme.getImageCount(); i++) {
+			ImageView imageView = new ImageView(this);
+			drawableManager.fetchDrawableOnThread(theme.getImage(i), imageView);
+			mainImages.addView(imageView);
+		}
+		description.setText(Html.fromHtml(desc.toString()));
+		installButton.setEnabled(/*theme already installed?*/true);
+		installButton.setOnClickListener(new OnClickListener() {
+			public void onClick(View v) {
+				installTheme();
+			}
+		});
+	}
+
+	private void installTheme() {
+		final ProgressDialog progress = new ProgressDialog(this);
+		progress.requestWindowFeature(Window.FEATURE_NO_TITLE | Window.FEATURE_PROGRESS);
+		progress.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+		progress.setMessage("Installing theme " + theme.getName() + "...");
+		progress.setIndeterminate(true);
+		progress.setCancelable(false);
+		progress.show();
+
+		final Handler handler = new Handler(new Callback() {
+			public boolean handleMessage(Message msg) {
+				Bundle data = msg.getData();
+				boolean handled = false;
+
+				if (data.containsKey("incProgress")) {
+					progress.incrementProgressBy(data.getInt("incProgress"));
+					handled = true;
+				}
+
+				if (data.containsKey("setProgressMax")) {
+					progress.setIndeterminate(false);
+					progress.setMax(data.getInt("setProgressMax"));
+					handled = true;
+				}
+
+				if (data.containsKey("displayToast"))
+					Toast.makeText(ThemeInstallerActivity.this, data.getString("displayToast"), Toast.LENGTH_SHORT).show();
+
+				return handled;
+			}
+		});
+
+		new Thread(new Runnable() {
+			private void update(String key, int value) {
+				Bundle data = new Bundle();
+				Message msg = new Message();
+				data.putInt(key, value);
+				msg.setData(data);
+				handler.sendMessage(msg);
+			}
+
+			private void displayMessage(String text) {
+				Bundle data = new Bundle();
+				Message msg = new Message();
+				data.putString("displayToast", text);
+				msg.setData(data);
+				handler.sendMessage(msg);
+			}
+
+			public void run() {
+				boolean success = false;
+				DefaultHttpClient client = new DefaultHttpClient();
+				HttpGet request = new HttpGet(theme.getDownloadLink());
+				try {
+					HttpResponse response = client.execute(request);
+					HttpEntity entity = response.getEntity();
+					ZipInputStream zipIs = new ZipInputStream(entity.getContent());
+					ZipEntry entry;
+					byte buffer[] = new byte[BUFFER_SIZE];
+
+					update("setProgressMax", (int) entity.getContentLength());
+					while ((entry = zipIs.getNextEntry()) != null) {
+						if(entry.isDirectory())
+							continue;
+
+						File file = new File(getFilesDir(), entry.getName());
+						try {
+							/* Create parent directories if necessary */
+							file.getParentFile().mkdirs();
+						} catch (SecurityException e) {
+							Log.w("Rockbox", "Couldn't create parent directories!", e);
+						}
+
+						Log.d("RB", "ZipEntry[" + entry.getName() + ", " + entry.getSize() + "]: " + file.toString());
+
+		                FileOutputStream os = new FileOutputStream(file);
+		                int count;
+		                while ((count = zipIs.read(buffer, 0, BUFFER_SIZE)) != -1)
+		                	os.write(buffer, 0, count);
+		                os.flush();
+		                os.close();
+
+						update("incProgress", (int) entry.getCompressedSize());
+					}
+					zipIs.close();
+					success = true;
+				} catch (ClientProtocolException e) {
+					// TODO Auto-generated catch block
+					e.printStackTrace();
+				} catch (IOException e) {
+					// TODO Auto-generated catch block
+					e.printStackTrace();
+				}
+
+				progress.dismiss();
+				displayMessage(success ? "Theme installed succesfully!" : "Error occured during installation!");
+			}
+		}).start();
+	}
+}
\ No newline at end of file
diff --git a/android/src/org/rockbox/thememanager/ThemeListActivity.java b/android/src/org/rockbox/thememanager/ThemeListActivity.java
new file mode 100644
index 0000000..b6a2bad
--- /dev/null
+++ b/android/src/org/rockbox/thememanager/ThemeListActivity.java
@@ -0,0 +1,164 @@
+package org.rockbox.thememanager;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.rockbox.R;
+import org.rockbox.utils.DrawableManager;
+import org.rockbox.utils.IniParser;
+
+import android.app.ListActivity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+public class ThemeListActivity extends ListActivity {
+	public static final String BASE_URL = "http://themes.rockbox.org";
+
+	public static String formatUrl(String path) {
+		if (path == null)
+			return null;
+		return BASE_URL + path.replace(' ', '+');
+	}
+
+	private List<Theme> getThemes(String target) {
+		DefaultHttpClient client = new DefaultHttpClient();
+		HttpGet request = new HttpGet(formatUrl("/rbutilqt.php?release=0.9.0&target=" + target));
+		IniParser iniParser = new IniParser();
+		try {
+			HttpResponse response = client.execute(request);
+			iniParser.load(response.getEntity().getContent());
+		} catch (IOException e) {
+			e.printStackTrace();
+			Log.e("Rockbox", "Couldn't fetch theme list!", e);
+			return null;
+		}
+
+		String errorCode = iniParser.getProperty("error", "code");
+		if (errorCode == null || !errorCode.equals("0")) {
+			Log.w("Rockbox", "Theme site returned error code " + errorCode == null ? "(null)" : errorCode);
+			if (errorCode != null)
+				Log.w("Rockbox", "Error description: " + iniParser.getProperty("error", "description"));
+			return null;
+		}
+
+		ArrayList<Theme> themes = new ArrayList<Theme>();
+		for (String section : iniParser.sections()) {
+			if (section.equals("status") || section.equals("error"))
+				continue;
+
+			themes.add(new Theme(iniParser.properties(section)));
+		}
+
+		return themes;
+	}
+
+    private class ThemeListAdapter extends BaseAdapter {
+        private LayoutInflater inflater;
+        private List<Theme> themes;
+        private DrawableManager drawableManager;
+
+        public ThemeListAdapter(Context context, List<Theme> themes) {
+            /* Cache the LayoutInflate to avoid asking for a new one each time. */
+            this.inflater = LayoutInflater.from(context);
+            this.themes = themes;
+            this.drawableManager = new DrawableManager();
+        }
+
+        public int getCount() {
+            return themes.size();
+        }
+
+        public Object getItem(int position) {
+            return themes.get(position);
+        }
+
+        public long getItemId(int position) {
+            return position;
+        }
+
+        private class ViewHolder {
+            TextView text;
+            ImageView icon;
+        }
+
+        public View getView(int position, View convertView, ViewGroup parent) {
+            // A ViewHolder keeps references to children views to avoid unneccessary calls
+            // to findViewById() on each row.
+            ViewHolder holder;
+            Theme theme = themes.get(position);
+
+            // When convertView is not null, we can reuse it directly, there is no need
+            // to reinflate it. We only inflate a new View when the convertView supplied
+            // by ListView is null.
+            if (convertView == null) {
+                convertView = inflater.inflate(R.layout.themelist_item, null);
+
+                // Creates a ViewHolder and store references to the two children views
+                // we want to bind data to.
+                holder = new ViewHolder();
+                holder.text = (TextView) convertView.findViewById(R.id.themelist_item_text);
+                holder.icon = (ImageView) convertView.findViewById(R.id.themelist_item_icon);
+
+                convertView.setTag(holder);
+            } else {
+                // Get the ViewHolder back to get fast access to the TextView
+                // and the ImageView.
+                holder = (ViewHolder) convertView.getTag();
+            }
+
+            // Bind the data efficiently with the holder.
+            holder.text.setText(theme.getName() + " (" + theme.getProperty("author") + ")");
+            drawableManager.fetchDrawableOnThread(theme.getImage(0), holder.icon);
+
+            return convertView;
+        }
+    }
+
+	@Override
+	protected void onCreate(Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+
+		requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+		setProgressBarIndeterminateVisibility(true); /*FIXME*/
+
+		new Thread(new Runnable() {
+			public void run() {
+				final List<Theme> themes = getThemes("ipod3g");
+
+				ThemeListActivity.this.runOnUiThread(new Runnable() {
+					public void run() {
+						if (themes != null)
+							setListAdapter(new ThemeListAdapter(ThemeListActivity.this, themes));
+						setProgressBarIndeterminateVisibility(false);
+					}
+				});
+			}
+		}).start();	
+	}
+
+	@Override
+	protected void onListItemClick(ListView l, View v, int position, long id) {
+		super.onListItemClick(l, v, position, id);
+
+		Object item = l.getItemAtPosition(position);
+		if (item instanceof Theme) {
+			Intent intent = new Intent(this, ThemeInstallerActivity.class);
+			intent.putExtra("theme", (Theme) item);
+			startActivity(intent);
+		}
+	}
+}
diff --git a/android/src/org/rockbox/utils/DrawableManager.java b/android/src/org/rockbox/utils/DrawableManager.java
new file mode 100644
index 0000000..70dc02f
--- /dev/null
+++ b/android/src/org/rockbox/utils/DrawableManager.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * 
+ * Original author: James A Wilson
+ * http://stackoverflow.com/questions/541966/android-how-do-i-do-a-lazy-load-of-images-in-listview
+ * 
+ * Slightly modified by Maurus Cuelenaere
+*/
+
+package org.rockbox.utils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.widget.ImageView;
+
+public class DrawableManager {
+    private Map<String, Drawable> drawableMap;
+    private Drawable pendingImage;
+
+    public DrawableManager() {
+    	this(null);
+    }
+
+    public DrawableManager(Drawable pendingImage) {
+    	this.drawableMap = new HashMap<String, Drawable>();
+    	this.pendingImage = pendingImage;
+    }
+
+    private InputStream fetch(String urlString) throws MalformedURLException, IOException {
+    	DefaultHttpClient httpClient = new DefaultHttpClient();
+    	HttpGet request = new HttpGet(urlString);
+    	HttpResponse response = httpClient.execute(request);
+    	return response.getEntity().getContent();
+    }
+
+    public Drawable fetchDrawable(String urlString) {
+    	if (drawableMap.containsKey(urlString))
+    		return drawableMap.get(urlString);
+
+    	Log.d(this.getClass().getSimpleName(), "image url:" + urlString);
+    	try {
+    		InputStream is = fetch(urlString);
+    		Drawable drawable = Drawable.createFromStream(is, "src");
+    		if (drawable == null)
+    			return null;
+    		drawableMap.put(urlString, drawable);
+    		Log.d(this.getClass().getSimpleName(), "got a thumbnail drawable: " + drawable.getBounds() + ", "
+    				+ drawable.getIntrinsicHeight() + "," + drawable.getIntrinsicWidth() + ", "
+    				+ drawable.getMinimumHeight() + "," + drawable.getMinimumWidth());
+    		return drawable;
+    	} catch (MalformedURLException e) {
+    		Log.e(this.getClass().getSimpleName(), "fetchDrawable failed", e);
+    		return null;
+    	} catch (IOException e) {
+    		Log.e(this.getClass().getSimpleName(), "fetchDrawable failed", e);
+    		return null;
+    	}
+    }
+
+    public void fetchDrawableOnThread(final String urlString, final ImageView imageView) {
+    	if (drawableMap.containsKey(urlString)) {
+    		imageView.setImageDrawable(drawableMap.get(urlString));
+    		return;
+    	}
+
+    	if (pendingImage != null)
+    		imageView.setImageDrawable(pendingImage);
+
+    	final Handler handler = new Handler() {
+    		@Override
+    		public void handleMessage(Message message) {
+    			imageView.setImageDrawable((Drawable) message.obj);
+    		}
+    	};
+
+    	Thread thread = new Thread() {
+    		@Override
+    		public void run() {
+    			Drawable drawable = fetchDrawable(urlString);
+    			Message message = handler.obtainMessage(1, drawable);
+    			handler.sendMessage(message);
+    		}
+    	};
+    	thread.start();
+    }
+}
\ No newline at end of file
diff --git a/android/src/org/rockbox/utils/IniParser.java b/android/src/org/rockbox/utils/IniParser.java
new file mode 100644
index 0000000..6261c44
--- /dev/null
+++ b/android/src/org/rockbox/utils/IniParser.java
@@ -0,0 +1,199 @@
+package org.rockbox.utils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/* Nicked from http://www.xinotes.org/notes/note/407/
+ * Edited by Maurus Cuelenaere
+ */
+public class IniParser {
+    private Map<String,String> globalProperties;
+    private Map<String,Map<String,String>> properties;
+
+    private static enum ParseState {
+        NORMAL,
+        ESCAPE,
+        ESC_CRNL,
+        COMMENT
+    }
+
+    public IniParser() {
+        globalProperties = new HashMap<String,String>();
+        properties = new HashMap<String,Map<String,String>>();
+    }
+
+    /**
+     * Load ini as properties from input stream.
+     */
+    public void load(InputStream in) throws IOException {
+        int bufSize = 4096;
+        byte[] buffer = new byte[bufSize];
+        int n = in.read(buffer, 0, bufSize);
+
+        ParseState state = ParseState.NORMAL;
+        boolean section_open = false;
+        String current_section = null;
+        String key = null, value = null;
+        StringBuilder sb = new StringBuilder();
+        while (n >= 0) {
+            for (int i = 0; i < n; i++) {
+                char c = (char) buffer[i];
+
+                if (state == ParseState.COMMENT) { // comment, skip to end of line
+                    if ((c == '\r') ||(c == '\n')) {
+                        state = ParseState.NORMAL;
+                    }
+                    else {
+                        continue;
+                    }
+                }
+
+                if (state == ParseState.ESCAPE) {
+                    sb.append(c);
+                    if (c == '\r') {
+                        // if the EOL is \r\n, \ escapes both chars
+                        state = ParseState.ESC_CRNL; 
+                    }
+                    else {
+                        state = ParseState.NORMAL;
+                    }
+                    continue;
+                }
+
+                switch (c) {
+                    case '[': // start section
+                        sb = new StringBuilder();
+                        section_open = true;
+                        break;
+                    
+                    case ']': // end section
+                        if (section_open) {
+                            current_section = sb.toString().trim();
+                            sb = new StringBuilder();
+                            properties.put(current_section, new HashMap<String,String>());
+                            section_open = false;
+                        }
+                        else {
+                            sb.append(c);
+                        }
+                        break;
+
+                    case '\\': // escape char, take the next char as is
+                        state = ParseState.ESCAPE;
+                        break;
+
+                    case '#': 
+                    case ';': 
+                        state = ParseState.COMMENT;
+                        break;
+
+                    case '=': // assignment operator
+                    case ':':
+                        if (key == null) {
+                            key = sb.toString().trim();
+                            sb = new StringBuilder();
+                        }
+                        else {
+                            sb.append(c);
+                        }
+                        break;
+
+                    case '\r':
+                    case '\n':
+                        if ((state == ParseState.ESC_CRNL) && (c == '\n')) {
+                            sb.append(c);
+                            state = ParseState.NORMAL;
+                        }
+                        else {
+                            if (sb.length() > 0) {
+                                value = sb.toString().trim();
+                                sb = new StringBuilder();
+                        
+                                if (key != null) {
+                                    if (current_section == null) {
+                                        this.setProperty(key, value);
+                                    }
+                                    else {
+                                        this.setProperty(current_section, key, value);
+                                    }
+                                }
+                            }
+                            key = null;
+                            value = null;
+                        }
+                        break;
+
+                    case '"': // string delimiter
+                    	/* Ignore */
+                    	break;
+
+                    default: 
+                        sb.append(c);
+                }
+            }
+            n = in.read(buffer, 0, bufSize);
+        }
+    }
+
+    /**
+     * Get global property by name.
+     */
+    public String getProperty(String name) {
+        return globalProperties.get(name);
+    }
+
+    /**
+     * Set global property.
+     */
+    public void setProperty(String name, String value) {
+        globalProperties.put(name, value);
+    }
+
+    /**
+     * Return iterator of global properties.
+     */
+    public Map<String, String> properties() {
+        return globalProperties;
+    }
+
+    /**
+     * Return property iterator for specified section. Returns null if
+     * specified section does not exist.
+     */
+    public Map<String, String> properties(String section) {
+        Map<String,String> p = properties.get(section);
+        return p == null ? null : p;
+    }
+
+    /**
+     * Get property value for specified section and name. Returns null
+     * if section or property does not exist.
+     */
+    public String getProperty(String section, String name) {
+        Map<String,String> p = properties.get(section);
+        return p == null ? null : p.get(name);
+    }
+
+    /**
+     * Set property value for specified section and name. Creates section
+     * if not existing.
+     */
+    public void setProperty(String section, String name, String value) {
+        Map<String,String> p = properties.get(section);
+        if (p == null) {
+            p = new HashMap<String,String>();
+            properties.put(section, p);
+        }
+        p.put(name, value);
+    }
+
+    /**
+     * Return iterator of names of section.
+     */
+    public Set<String> sections() {
+        return properties.keySet();
+    }
+}
\ No newline at end of file
