Merge change 4584 into donut

* changes:
  Add hosttestlib library.
diff --git a/pdk/docs/guide/early_suspend.jd b/pdk/docs/guide/early_suspend.jd
deleted file mode 100755
index a27a5ca..0000000
--- a/pdk/docs/guide/early_suspend.jd
+++ /dev/null
@@ -1,29 +0,0 @@
-page.title=Early Suspend
-pdk.version=1.0
-@jd:body
-<p>The early-suspend API allows drivers to get notified when user-space writes to <code>/sys/power/request_state</code> to indicate that the user visible sleep state should change. Suspend handlers are called in order of low to high (4 - 1 below) and resume handlers are called in order of high to low (1 - 4 below).</p>
-<ol>
-  <li><code>EARLY_SUSPEND_LEVEL_BLANK_SCREEN</code>: </li>
-  <ul>
-    <li>on suspend: the screen should be turned off but the framebuffer must still be accessible. </li>
-    <li>on resume: the screen can be turned back on.</li>
-  </ul>
-  <li><code>EARLY_SUSPEND_LEVEL_STOP_DRAWING</code>:
-    <ul>
-      <li>on suspend: this level notifies user-space that it should stop accessing the framebuffer and it waits for it to complete.</li>
-      <li>on resume: it notifies user-space that it should resume screen access. Two methods are provided, console switch or a sysfs interface.</li>
-    </ul>
-  </li>
-  <li><code>EARLY_SUSPEND_LEVEL_DISABLE_FB</code>: Turn off the framebuffer
-    <ul>
-      <li>on suspend: turn off the framebuffer</li>
-      <li>on resume: turn the framebuffer back on. </li>
-    </ul>
-  </li>
-  <li><code>EARLY_SUSPEND_LEVEL_STOP_INPUT</code>:
-    <ul>
-      <li>on suspend: turn off input devices that are not capable of wakeup or where wakeup is disabled. </li>
-      <li>on resume: turn the same devices back on.</li>
-    </ul>
-  </li>
-</ol>
diff --git a/pdk/docs/guide/pdk_toc.cs b/pdk/docs/guide/pdk_toc.cs
index 82f9e86..ffd1bc2 100644
--- a/pdk/docs/guide/pdk_toc.cs
+++ b/pdk/docs/guide/pdk_toc.cs
@@ -13,7 +13,6 @@
      <ul>  
        <li><a href="<?cs var:toroot ?>guide/build_new_device.html">Configuring a New Product</a></li>
        <li><a href="<?cs var:toroot ?>guide/build_cookbook.html">Build Cookbook</a></li>
-       <li><a href="<?cs var:toroot ?>guide/bring_up.html">Bring up</a></li>
      </ul>
      </li>
      <li><a href="<?cs var:toroot ?>guide/release_keys.html">Release Keys and Signing Builds</a></li> 
@@ -29,6 +28,7 @@
 <li> <h2>System</h2>
 
 <ul>
+  <li><a href="<?cs var:toroot ?>guide/bring_up.html">Bring up</a></li>
   <li class="toggle-list">
     <div><a href="javascript:nothing()">Connectivity</a></div>
     <ul> 
@@ -53,13 +53,8 @@
       <li><a href="<?cs var:toroot ?>guide/camera.html">Camera/Video</a></li>
     </ul>
   </li>
-  <li class="toggle-list">
-    <div><a href="<?cs var:toroot ?>guide/power_management.html">Power Management</a></div>
-    <ul>
-      <li><a href="<?cs var:toroot ?>guide/wakelock.html">Wakelocks</a></li>
-      <li><a href="<?cs var:toroot ?>guide/early_suspend.html">Early Suspend</a></li>
-    </ul>
-  </li>
+  
+  <li><a href="<?cs var:toroot ?>guide/power_management.html">Power Management</a></li>
   <li><a href="<?cs var:toroot ?>guide/sensors.html">Sensors</a></li>
   <li class="toggle-list">
     <div><a href="javascript:nothing()">Telephony</a></div>
diff --git a/pdk/docs/guide/power_management.jd b/pdk/docs/guide/power_management.jd
index 6a1dbc3..91517fc 100755
--- a/pdk/docs/guide/power_management.jd
+++ b/pdk/docs/guide/power_management.jd
@@ -2,9 +2,6 @@
 pdk.version=1.0
 @jd:body
 
-
-
-
 <a name="toc"/>
 <div style="padding:10px">
 <a href="#androidPowerIntro">Introduction</a><br/>
@@ -13,7 +10,9 @@
 <a href="#androidPowerWakeLocksDefinitions">Types of Wake Locks</a><br/>
 <a href="#androidPowerWakeLockExample">Exploring a Wake Lock Example</a><br/></div>
 <a href="#androidPowerPowerManagerClass">PowerManager class</a><br/>
-<a href="#androidPowerKernelRegistration">Registering Drivers with the PM Driver</a><br/></div></font></div>
+<a href="#androidPowerKernelRegistration">Registering Drivers with the PM Driver</a><br/>
+<a href="#androidPowerEarlySuspend">Early Suspend</a><br/>
+</div>
 
 <a name="androidPowerIntro"></a><h2>Introduction</h2>
 
@@ -30,6 +29,70 @@
 
 <p>Wake locks are used by applications and services to request CPU resources.</p>
 
+<p>A locked wakelock, depending on its type, prevents the system from entering suspend or other low-power states. This document describes how to employ wakelocks. </p>
+<p>There are two settings for a wakelock:</p>
+<ul>
+  <li><code>WAKE_LOCK_SUSPEND</code>: prevents a full system suspend. </li>
+  <li><code></code><code>WAKE_LOCK_IDLE</code>: low-power states, which often cause large interrupt latencies or that disable a set of interrupts, will not be entered from idle until the wakelocks are released. </li>
+</ul>
+<p>Unless the type is specified, this document refers to wakelocks of type <code>WAKE_LOCK_SUSPEND</code>. </p>
+<p>If the suspend operation has already started when locking a wakelock, the system will abort the suspend operation as long it has not already reached the <code>suspend_late</code> stage. This means that locking a wakelock from an interrupt handler or a freezeable thread always works, but if you lock a wakelock from a <code>suspend_late</code> handler, you must also return an error from that handler to abort suspend. You can use wakelocks to allow the user-space to decide which keys should wake the full system and turn on the screen. Use <code>set_irq_wake</code> or a platform-specific API to ensure that the keypad interrupt wakes up the CPU. Once the keypad driver has resumed, the sequence of events can look like this:</p>
+<ol>
+  <li> The Keypad driver receives an interrupt, locks the keypad-scan wakelock,
+    and starts scanning the keypad matrix. </li>
+  <li>The keypad-scan code detects a key change and reports it to the input-event
+    driver. </li>
+  <li>The input-event driver sees the key change, enqueues an event, and locks
+    the input-event-queue wakelock. </li>
+  <li>The keypad-scan code detects that no keys are held and unlocks the
+    keypad-scan wakelock. </li>
+  <li>The user-space input-event thread returns from select/poll, locks the
+    process-input-events wakelock, and calls read in the input-event device. </li>
+  <li>The input-event driver dequeues the key-event and, since the queue is now
+    empty, unlocks the input-event-queue wakelock. </li>
+  <li>The user-space input-event thread returns from read. It determines that the
+    key should not wake up the full system, releases the process-input-events
+    wakelock, and calls select or poll. </li>
+</ol>
+<p>The simple sequence diagram below illustrates these steps:</p>
+    <pre>
+     					Key pressed      Key released
+      					     |		      |
+      keypad-scan       		     ++++++++++++++++++++++
+      input-event-queue 			  +++ 		  +++
+      process-input-events 		            +++ 	    +++
+      </pre>
+
+<a name="driverAPI"></a><h3>Driver API</h3>
+<p>A driver can use the wakelock API by adding a wakelock variable to its state and calling <code>wake_lock_init</code>, as illustrated in the snippet below:</p>
+<pre>
+  struct state {
+  struct wakelock wakelock;
+  }
+  init() {
+  wake_lock_init(&amp;state-&gt;wakelock, WAKE_LOCK_SUSPEND, &quot;wakelockname&quot;);
+  }
+  Before freeing the memory, wake_lock_destroy must be called:
+  uninit() {
+  wake_lock_destroy(&amp;state-&gt;wakelock);
+  }
+  </pre>
+<p> When the driver determines that it needs to run (usually in an interrupt handler), it calls <code>wake_lock</code>:</p>
+<pre>
+  wake_lock(&amp;state-&gt;wakelock);
+  </pre>
+<p>When it no longer needs to run, it calls <code>wake_unlock</code>:</p>
+<pre>
+  wake_unlock(&amp;state-&gt;wakelock);
+  </pre>
+<p> It can also call <code>wake_lock_timeout</code> to release the wakelock after a delay:</p>
+<pre>
+  wake_lock_timeout(&amp;state-&gt;wakelock, HZ);
+</pre>
+<p> This works whether or not the wakelock is already held. It is useful if the driver woke up other parts of the system that do not use wakelocks but still need to run. Avoid this when possible, since it will waste power if the timeout is long or may fail to finish needed work if the timeout is short.</p>
+<a name="userspaceAPI"></a><h3>User-space API</h3>
+<p>Write <code>lockname</code> or <code>lockname timeout</code> to <code>/sys/power/wake_lock</code> lock and, if needed, create a wakelock. The timeout here is specified in nanoseconds. Write <code>lockname</code> to <code>/sys/power/wake_unlock</code> to unlock a user wakelock.</p>
+<p> Do not use randomly generated wakelock names as there is no API to free a user-space wakelock.</p>
 
 <a name="androidPowerWakeLocksDefinitions"></a><h3>Types of Wake Locks</h3>
 
@@ -107,3 +170,33 @@
 android_register_early_resume(android_early_resume_t *handler)
 </pre>
 <p>It is critical in a drive to return immediately and not wait for anything to happen in the call back.</p>
+
+
+<a name="androidPowerEarlySuspend"></a><h2>Early Suspend</h2>
+
+<p>The early-suspend API allows drivers to get notified when user-space writes to <code>/sys/power/request_state</code> to indicate that the user visible sleep state should change. Suspend handlers are called in order of low to high (4 - 1 below) and resume handlers are called in order of high to low (1 - 4 below).</p>
+<ol>
+  <li><code>EARLY_SUSPEND_LEVEL_BLANK_SCREEN</code>: </li>
+  <ul>
+    <li>on suspend: the screen should be turned off but the framebuffer must still be accessible. </li>
+    <li>on resume: the screen can be turned back on.</li>
+  </ul>
+  <li><code>EARLY_SUSPEND_LEVEL_STOP_DRAWING</code>:
+    <ul>
+      <li>on suspend: this level notifies user-space that it should stop accessing the framebuffer and it waits for it to complete.</li>
+      <li>on resume: it notifies user-space that it should resume screen access. Two methods are provided, console switch or a sysfs interface.</li>
+    </ul>
+  </li>
+  <li><code>EARLY_SUSPEND_LEVEL_DISABLE_FB</code>: Turn off the framebuffer
+    <ul>
+      <li>on suspend: turn off the framebuffer</li>
+      <li>on resume: turn the framebuffer back on. </li>
+    </ul>
+  </li>
+  <li><code>EARLY_SUSPEND_LEVEL_STOP_INPUT</code>:
+    <ul>
+      <li>on suspend: turn off input devices that are not capable of wakeup or where wakeup is disabled. </li>
+      <li>on resume: turn the same devices back on.</li>
+    </ul>
+  </li>
+</ol>
diff --git a/pdk/docs/guide/wakelock.jd b/pdk/docs/guide/wakelock.jd
deleted file mode 100755
index ee59053..0000000
--- a/pdk/docs/guide/wakelock.jd
+++ /dev/null
@@ -1,80 +0,0 @@
-page.title=Wakelocks
-pdk.version=1.0
-@jd:body
-
-
-<div id="qv-wrapper">
-<div id="qv">
-<h2>In this document</h2>
-<a name="toc"/>
-<ul>
-<li><a href="#driverAPI">Driver API</a></li>
-<li><a href="#userspaceAPI">User Space API</a></li>
-</ul>
-</div>
-</div>
-
-<p>A locked wakelock, depending on its type, prevents the system from entering suspend or other low-power states. This document describes how to employ wakelocks. </p>
-<p>There are two settings for a wakelock:</p>
-<ul>
-  <li><code>WAKE_LOCK_SUSPEND</code>: prevents a full system suspend. </li>
-  <li><code></code><code>WAKE_LOCK_IDLE</code>: low-power states, which often cause large interrupt latencies or that disable a set of interrupts, will not be entered from idle until the wakelocks are released. </li>
-</ul>
-<p>Unless the type is specified, this document refers to wakelocks of type <code>WAKE_LOCK_SUSPEND</code>. </p>
-<p>If the suspend operation has already started when locking a wakelock, the system will abort the suspend operation as long it has not already reached the <code>suspend_late</code> stage. This means that locking a wakelock from an interrupt handler or a freezeable thread always works, but if you lock a wakelock from a <code>suspend_late</code> handler, you must also return an error from that handler to abort suspend. You can use wakelocks to allow the user-space to decide which keys should wake the full system and turn on the screen. Use <code>set_irq_wake</code> or a platform-specific API to ensure that the keypad interrupt wakes up the CPU. Once the keypad driver has resumed, the sequence of events can look like this:</p>
-<ol>
-  <li> The Keypad driver receives an interrupt, locks the keypad-scan wakelock,
-    and starts scanning the keypad matrix. </li>
-  <li>The keypad-scan code detects a key change and reports it to the input-event
-    driver. </li>
-  <li>The input-event driver sees the key change, enqueues an event, and locks
-    the input-event-queue wakelock. </li>
-  <li>The keypad-scan code detects that no keys are held and unlocks the
-    keypad-scan wakelock. </li>
-  <li>The user-space input-event thread returns from select/poll, locks the
-    process-input-events wakelock, and calls read in the input-event device. </li>
-  <li>The input-event driver dequeues the key-event and, since the queue is now
-    empty, unlocks the input-event-queue wakelock. </li>
-  <li>The user-space input-event thread returns from read. It determines that the
-    key should not wake up the full system, releases the process-input-events
-    wakelock, and calls select or poll. </li>
-</ol>
-<p>The simple sequence diagram below illustrates these steps:</p>
-    <pre>
-     					Key pressed      Key released
-      					     |		      |
-      keypad-scan       		     ++++++++++++++++++++++
-      input-event-queue 			  +++ 		  +++
-      process-input-events 		            +++ 	    +++
-      </pre>
-
-<a name="driverAPI"></a><h3>Driver API</h3>
-<p>A driver can use the wakelock API by adding a wakelock variable to its state and calling <code>wake_lock_init</code>, as illustrated in the snippet below:</p>
-<pre>
-  struct state {
-  struct wakelock wakelock;
-  }
-  init() {
-  wake_lock_init(&amp;state-&gt;wakelock, WAKE_LOCK_SUSPEND, &quot;wakelockname&quot;);
-  }
-  Before freeing the memory, wake_lock_destroy must be called:
-  uninit() {
-  wake_lock_destroy(&amp;state-&gt;wakelock);
-  }
-  </pre>
-<p> When the driver determines that it needs to run (usually in an interrupt handler), it calls <code>wake_lock</code>:</p>
-<pre>
-  wake_lock(&amp;state-&gt;wakelock);
-  </pre>
-<p>When it no longer needs to run, it calls <code>wake_unlock</code>:</p>
-<pre>
-  wake_unlock(&amp;state-&gt;wakelock);
-  </pre>
-<p> It can also call <code>wake_lock_timeout</code> to release the wakelock after a delay:</p>
-<pre>
-  wake_lock_timeout(&amp;state-&gt;wakelock, HZ);
-</pre>
-<p> This works whether or not the wakelock is already held. It is useful if the driver woke up other parts of the system that do not use wakelocks but still need to run. Avoid this when possible, since it will waste power if the timeout is long or may fail to finish needed work if the timeout is short.</p>
-<a name="userspaceAPI"></a><h3>User-space API</h3>
-<p>Write <code>lockname</code> or <code>lockname timeout</code> to <code>/sys/power/wake_lock</code> lock and, if needed, create a wakelock. The timeout here is specified in nanoseconds. Write <code>lockname</code> to <code>/sys/power/wake_unlock</code> to unlock a user wakelock.</p>
-<p> Do not use randomly generated wakelock names as there is no API to free a user-space wakelock.</p>
\ No newline at end of file
diff --git a/samples/ApiDemos/Android.mk b/samples/ApiDemos/Android.mk
index e10ef43..7921120 100644
--- a/samples/ApiDemos/Android.mk
+++ b/samples/ApiDemos/Android.mk
@@ -1,7 +1,7 @@
 LOCAL_PATH:= $(call my-dir)
 include $(CLEAR_VARS)
 
-LOCAL_MODULE_TAGS := samples
+LOCAL_MODULE_TAGS := samples tests
 
 # Only compile source java files in this apk.
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
diff --git a/testrunner/test_defs.xml b/testrunner/test_defs.xml
index 9cb780d..08fe0e8 100644
--- a/testrunner/test_defs.xml
+++ b/testrunner/test_defs.xml
@@ -185,6 +185,14 @@
     continuous="true"
     cts="true" />
 
+<test name="cts-permission2"
+    build_path="cts/tests/tests/permission2"
+    package="com.android.cts.permission2"
+    runner="android.test.InstrumentationTestRunner"
+    coverage_target="framework"
+    continuous="true"
+    cts="true" />
+
 <test name="cts-process"
     build_path="cts/tests"
     package="com.android.cts.process"
diff --git a/tools/anttasks/src/com/android/ant/ApkBuilderTask.java b/tools/anttasks/src/com/android/ant/ApkBuilderTask.java
index 3758b82..af87c78 100644
--- a/tools/anttasks/src/com/android/ant/ApkBuilderTask.java
+++ b/tools/anttasks/src/com/android/ant/ApkBuilderTask.java
@@ -16,8 +16,9 @@
 
 package com.android.ant;
 
-import com.android.apkbuilder.ApkBuilder;
-import com.android.apkbuilder.ApkBuilder.ApkFile;
+import com.android.apkbuilder.ApkBuilder.ApkCreationException;
+import com.android.apkbuilder.internal.ApkBuilderImpl;
+import com.android.apkbuilder.internal.ApkBuilderImpl.ApkFile;
 import com.android.sdklib.internal.project.ApkConfigurationHelper;
 import com.android.sdklib.internal.project.ProjectProperties;
 import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
@@ -37,7 +38,7 @@
 import java.util.Map.Entry;
 
 public class ApkBuilderTask extends Task {
-    
+
     /**
      * Class to represent nested elements. Since they all have only one attribute ('path'), the
      * same class can be used for all the nested elements (zip, file, sourcefolder, jarfolder,
@@ -45,7 +46,7 @@
      */
     public final static class Value extends ProjectComponent {
         String mPath;
-        
+
         /**
          * Sets the value of the "path" attribute.
          * @param path the value.
@@ -59,7 +60,7 @@
     private String mBaseName;
     private boolean mVerbose = false;
     private boolean mSigned = true;
-    
+
     private final ArrayList<Value> mZipList = new ArrayList<Value>();
     private final ArrayList<Value> mFileList = new ArrayList<Value>();
     private final ArrayList<Value> mSourceList = new ArrayList<Value>();
@@ -79,7 +80,7 @@
     public void setOutfolder(Path outFolder) {
         mOutFolder = outFolder.toString();
     }
-    
+
     /**
      * Sets the value of the "basename" attribute.
      * @param baseName the value.
@@ -87,7 +88,7 @@
     public void setBasename(String baseName) {
         mBaseName = baseName;
     }
-    
+
     /**
      * Sets the value of the "verbose" attribute.
      * @param verbose the value.
@@ -95,7 +96,7 @@
     public void setVerbose(boolean verbose) {
         mVerbose = verbose;
     }
-    
+
     /**
      * Sets the value of the "signed" attribute.
      * @param signed the value.
@@ -103,7 +104,7 @@
     public void setSigned(boolean signed) {
         mSigned = signed;
     }
-    
+
     /**
      * Returns an object representing a nested <var>zip</var> element.
      */
@@ -112,7 +113,7 @@
         mZipList.add(zip);
         return zip;
     }
-    
+
     /**
      * Returns an object representing a nested <var>file</var> element.
      */
@@ -139,7 +140,7 @@
         mJarList.add(file);
         return file;
     }
-    
+
     /**
      * Returns an object representing a nested <var>nativefolder</var> element.
      */
@@ -148,64 +149,64 @@
         mNativeList.add(file);
         return file;
     }
-    
+
     @Override
     public void execute() throws BuildException {
         Project taskProject = getProject();
-        
-        ApkBuilder apkBuilder = new ApkBuilder();
+
+        ApkBuilderImpl apkBuilder = new ApkBuilderImpl();
         apkBuilder.setVerbose(mVerbose);
         apkBuilder.setSignedPackage(mSigned);
-        
+
         try {
             // setup the list of everything that needs to go in the archive.
-            
+
             // go through the list of zip files to add. This will not include
             // the resource package, which is handled separaly for each apk to create.
             for (Value v : mZipList) {
                 FileInputStream input = new FileInputStream(v.mPath);
                 mZipArchives.add(input);
             }
-            
+
             // now go through the list of file to directly add the to the list.
             for (Value v : mFileList) {
-                mArchiveFiles.add(ApkBuilder.getInputFile(v.mPath));
+                mArchiveFiles.add(ApkBuilderImpl.getInputFile(v.mPath));
             }
-            
+
             // now go through the list of file to directly add the to the list.
             for (Value v : mSourceList) {
-                ApkBuilder.processSourceFolderForResource(v.mPath, mJavaResources);
+                ApkBuilderImpl.processSourceFolderForResource(v.mPath, mJavaResources);
             }
-            
+
             // now go through the list of jar folders.
             for (Value v : mJarList) {
-                ApkBuilder.processJarFolder(v.mPath, mResourcesJars);
+                ApkBuilderImpl.processJarFolder(v.mPath, mResourcesJars);
             }
-            
+
             // now the native lib folder.
             for (Value v : mNativeList) {
                 String parameter = v.mPath;
                 File f = new File(parameter);
-                
+
                 // compute the offset to get the relative path
                 int offset = parameter.length();
                 if (parameter.endsWith(File.separator) == false) {
                     offset++;
                 }
 
-                ApkBuilder.processNativeFolder(offset, f, mNativeLibraries);
+                ApkBuilderImpl.processNativeFolder(offset, f, mNativeLibraries);
             }
 
-            
+
             // first do a full resource package
             createApk(apkBuilder, null /*configName*/, null /*resourceFilter*/);
-    
+
             // now see if we need to create file with filtered resources.
             // Get the project base directory.
             File baseDir = taskProject.getBaseDir();
             ProjectProperties properties = ProjectProperties.load(baseDir.getAbsolutePath(),
                     PropertyType.DEFAULT);
-            
+
             Map<String, String> apkConfigs = ApkConfigurationHelper.getConfigs(properties);
             if (apkConfigs.size() > 0) {
                 Set<Entry<String, String>> entrySet = apkConfigs.entrySet();
@@ -217,20 +218,23 @@
             throw new BuildException(e);
         } catch (IllegalArgumentException e) {
             throw new BuildException(e);
+        } catch (ApkCreationException e) {
+            throw new BuildException(e);
         }
     }
-    
+
     /**
      * Creates an application package.
-     * @param apkBuilder 
+     * @param apkBuilder
      * @param configName the name of the filter config. Can be null in which case a full resource
      * package will be generated.
      * @param resourceFilter the resource configuration filter to pass to aapt (if configName is
      * non null)
-     * @throws FileNotFoundException 
+     * @throws FileNotFoundException
+     * @throws ApkCreationException
      */
-    private void createApk(ApkBuilder apkBuilder, String configName, String resourceFilter)
-            throws FileNotFoundException {
+    private void createApk(ApkBuilderImpl apkBuilder, String configName, String resourceFilter)
+            throws FileNotFoundException, ApkCreationException {
         // All the files to be included in the archive have already been prep'ed up, except
         // the resource package.
         // figure out its name.
@@ -240,20 +244,20 @@
         } else {
             filename = mBaseName + ".ap_";
         }
-        
+
         // now we add it to the list of zip archive (it's just a zip file).
-        
+
         // it's used as a zip archive input
         FileInputStream resoucePackageZipFile = new FileInputStream(new File(mOutFolder, filename));
         mZipArchives.add(resoucePackageZipFile);
-        
+
         // prepare the filename to generate. Same thing as the resource file.
         if (configName != null && resourceFilter != null) {
             filename = mBaseName + "-" + configName;
         } else {
             filename = mBaseName;
         }
-        
+
         if (mSigned) {
             filename = filename + "-debug.apk";
         } else {
@@ -279,13 +283,13 @@
                         filename, resourceFilter));
             }
         }
-        
+
         File f = new File(mOutFolder, filename);
-        
+
         // and generate the apk
         apkBuilder.createPackage(f.getAbsoluteFile(), mZipArchives,
                 mArchiveFiles, mJavaResources, mResourcesJars, mNativeLibraries);
-        
+
         // we are done. We need to remove the resource package from the list of zip archives
         // in case we have another apk to generate.
         mZipArchives.remove(resoucePackageZipFile);
diff --git a/tools/apkbuilder/src/com/android/apkbuilder/ApkBuilder.java b/tools/apkbuilder/src/com/android/apkbuilder/ApkBuilder.java
index 40abff1..db9f355 100644
--- a/tools/apkbuilder/src/com/android/apkbuilder/ApkBuilder.java
+++ b/tools/apkbuilder/src/com/android/apkbuilder/ApkBuilder.java
@@ -16,419 +16,69 @@
 
 package com.android.apkbuilder;
 
-import com.android.jarutils.DebugKeyProvider;
-import com.android.jarutils.JavaResourceFilter;
-import com.android.jarutils.SignedJarBuilder;
-import com.android.jarutils.DebugKeyProvider.KeytoolException;
-import com.android.prefs.AndroidLocation.AndroidLocationException;
+import com.android.apkbuilder.internal.ApkBuilderImpl;
 
-import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.FilenameFilter;
-import java.io.IOException;
-import java.security.PrivateKey;
-import java.security.cert.X509Certificate;
-import java.text.DateFormat;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.regex.Pattern;
+
 
 /**
  * Command line APK builder with signing support.
  */
 public final class ApkBuilder {
-    
-    private final static Pattern PATTERN_JAR_EXT = Pattern.compile("^.+\\.jar$",
-            Pattern.CASE_INSENSITIVE);
-    private final static Pattern PATTERN_NATIVELIB_EXT = Pattern.compile("^.+\\.so$",
-            Pattern.CASE_INSENSITIVE);
-    
-    private final static String NATIVE_LIB_ROOT = "lib/";
 
-    /**
-     * A File to be added to the APK archive.
-     * <p/>This includes the {@link File} representing the file and its path in the archive.
-     */
-    public final static class ApkFile {
-        String archivePath;
-        File file;
+    public final static class WrongOptionException extends Exception {
+        private static final long serialVersionUID = 1L;
 
-        ApkFile(File file, String path) {
-            this.file = file;
-            this.archivePath = path;
+        public WrongOptionException(String message) {
+            super(message);
         }
     }
 
-    private JavaResourceFilter mResourceFilter = new JavaResourceFilter();
-    private boolean mVerbose = false;
-    private boolean mSignedPackage = true;
-    /** the optional type of the debug keystore. If <code>null</code>, the default */
-    private String mStoreType = null;
+    public final static class ApkCreationException extends Exception {
+        private static final long serialVersionUID = 1L;
+
+        public ApkCreationException(String message) {
+            super(message);
+        }
+
+        public ApkCreationException(Throwable throwable) {
+            super(throwable);
+        }
+
+    }
 
     /**
-     * @param args
+     * Main method. This is meant to be called from the command line through an exec.
+     * <p/>WARNING: this will call {@link System#exit(int)} if anything goes wrong.
+     * @param args command line arguments.
      */
     public static void main(String[] args) {
-        new ApkBuilder().run(args);
-    }
-    
-    public void setVerbose(boolean verbose) {
-        mVerbose = verbose;
-    }
-    
-    public void setSignedPackage(boolean signedPackage) {
-        mSignedPackage = signedPackage;
-    }
-
-    private void run(String[] args) {
-        if (args.length < 1) {
-            printUsageAndQuit();
-        }
-
         try {
-            // read the first args that should be a file path
-            File outFile = getOutFile(args[0]);
-    
-            ArrayList<FileInputStream> zipArchives = new ArrayList<FileInputStream>();
-            ArrayList<File> archiveFiles = new ArrayList<File>();
-            ArrayList<ApkFile> javaResources = new ArrayList<ApkFile>();
-            ArrayList<FileInputStream> resourcesJars = new ArrayList<FileInputStream>();
-            ArrayList<ApkFile> nativeLibraries = new ArrayList<ApkFile>();
-    
-            int index = 1;
-            do {
-                String argument = args[index++];
-    
-                if ("-v".equals(argument)) {
-                    mVerbose = true;
-                } else if ("-u".equals(argument)) {
-                    mSignedPackage = false;
-                } else if ("-z".equals(argument)) {
-                    // quick check on the next argument.
-                    if (index == args.length) printUsageAndQuit();
-                    
-                    try {
-                        FileInputStream input = new FileInputStream(args[index++]);
-                        zipArchives.add(input);
-                    } catch (FileNotFoundException e) {
-                        printAndExit(e.getMessage());
-                    }
-                } else if ("-f". equals(argument)) {
-                    // quick check on the next argument.
-                    if (index == args.length) printUsageAndQuit();
-    
-                    archiveFiles.add(getInputFile(args[index++]));
-                } else if ("-rf". equals(argument)) {
-                    // quick check on the next argument.
-                    if (index == args.length) printUsageAndQuit();
-    
-                    processSourceFolderForResource(args[index++], javaResources);
-                } else if ("-rj". equals(argument)) {
-                    // quick check on the next argument.
-                    if (index == args.length) printUsageAndQuit();
-                    
-                    processJarFolder(args[index++], resourcesJars);
-                } else if ("-nf".equals(argument)) {
-                    // quick check on the next argument.
-                    if (index == args.length) printUsageAndQuit();
-                    
-                    String parameter = args[index++];
-                    File f = new File(parameter);
-    
-                    // compute the offset to get the relative path
-                    int offset = parameter.length();
-                    if (parameter.endsWith(File.separator) == false) {
-                        offset++;
-                    }
-    
-                    processNativeFolder(offset, f, nativeLibraries);
-                } else if ("-storetype".equals(argument)) {
-                    // quick check on the next argument.
-                    if (index == args.length) printUsageAndQuit();
-                    
-                    mStoreType  = args[index++];
-                } else {
-                    printAndExit("Unknown argument: " + argument);
-                }
-            } while (index < args.length);
-            
-            createPackage(outFile, zipArchives, archiveFiles, javaResources, resourcesJars,
-                    nativeLibraries);
-        } catch (IllegalArgumentException e) {
-            printAndExit(e.getMessage());
+            new ApkBuilderImpl().run(args);
+        } catch (WrongOptionException e) {
+            printUsageAndQuit();
         } catch (FileNotFoundException e) {
             printAndExit(e.getMessage());
-        }
-    }
-
-
-    private File getOutFile(String filepath) {
-        File f = new File(filepath);
-        
-        if (f.isDirectory()) {
-            printAndExit(filepath + " is a directory!");
-        }
-        
-        if (f.exists()) { // will be a file in this case.
-            if (f.canWrite() == false) {
-                printAndExit("Cannot write " + filepath);
-            }
-        } else {
-            try {
-                if (f.createNewFile() == false) {
-                    printAndExit("Failed to create " + filepath);
-                }
-            } catch (IOException e) {
-                printAndExit("Failed to create '" + filepath + "' : " + e.getMessage());
-            }
-        }
-        
-        return f;
-    }
-
-    public static File getInputFile(String filepath) throws IllegalArgumentException {
-        File f = new File(filepath);
-        
-        if (f.isDirectory()) {
-            throw new IllegalArgumentException(filepath + " is a directory!");
-        }
-        
-        if (f.exists()) {
-            if (f.canRead() == false) {
-                throw new IllegalArgumentException("Cannot read " + filepath);
-            }
-        } else {
-            throw new IllegalArgumentException(filepath + " does not exists!");
-        }
-
-        return f;
-    }
-
-    /**
-     * Processes a source folder and adds its java resources to a given list of {@link ApkFile}.
-     * @param folderPath the path to the source folder.
-     * @param javaResources the list of {@link ApkFile} to fill.
-     */
-    public static void processSourceFolderForResource(String folderPath,
-            ArrayList<ApkFile> javaResources) {
-        
-        File folder = new File(folderPath);
-        
-        if (folder.isDirectory()) {
-            // file is a directory, process its content.
-            File[] files = folder.listFiles();
-            for (File file : files) {
-                processFileForResource(file, null, javaResources);
-            }
-        } else {
-            // not a directory? output error and quit.
-            if (folder.exists()) {
-                throw new IllegalArgumentException(folderPath + " is not a folder!");
-            } else {
-                throw new IllegalArgumentException(folderPath + " does not exist!");
-            }
-        }
-    }
-    
-    public static void processJarFolder(String parameter, ArrayList<FileInputStream> resourcesJars)
-            throws FileNotFoundException {
-        File f = new File(parameter);
-        if (f.isDirectory()) {
-            String[] files = f.list(new FilenameFilter() {
-                public boolean accept(File dir, String name) {
-                    return PATTERN_JAR_EXT.matcher(name).matches();
-                }
-            });
-
-            for (String file : files) {
-                String path = f.getAbsolutePath() + File.separator + file;
-                FileInputStream input = new FileInputStream(path);
-                resourcesJars.add(input);
-            }
-        } else {
-            FileInputStream input = new FileInputStream(parameter);
-            resourcesJars.add(input);
-        }
-    }
-
-    
-    /**
-     * Processes a {@link File} that could be a {@link ApkFile}, or a folder containing
-     * java resources.
-     * @param file the {@link File} to process.
-     * @param path the relative path of this file to the source folder. Can be <code>null</code> to
-     * identify a root file. 
-     * @param javaResources the list of {@link ApkFile} object to fill.
-     */
-    private static void processFileForResource(File file, String path,
-            ArrayList<ApkFile> javaResources) {
-        if (file.isDirectory()) {
-            // a directory? we check it
-            if (JavaResourceFilter.checkFolderForPackaging(file.getName())) {
-                // if it's valid, we append its name to the current path.
-                if (path == null) {
-                    path = file.getName();
-                } else {
-                    path = path + "/" + file.getName();
-                }
-
-                // and process its content.
-                File[] files = file.listFiles();
-                for (File contentFile : files) {
-                    processFileForResource(contentFile, path, javaResources);
-                }
-            }
-        } else {
-            // a file? we check it
-            if (JavaResourceFilter.checkFileForPackaging(file.getName())) {
-                // we append its name to the current path
-                if (path == null) {
-                    path = file.getName();
-                } else {
-                    path = path + "/" + file.getName();
-                }
-
-                // and add it to the list.
-                javaResources.add(new ApkFile(file, path));
-            }
-        }
-    }
-    
-    /**
-     * Process a {@link File} for native library inclusion.
-     * @param offset the length of the root folder (used to compute relative path)
-     * @param f the {@link File} to process
-     * @param nativeLibraries the array to add native libraries.
-     */
-    public static void processNativeFolder(int offset, File f, ArrayList<ApkFile> nativeLibraries) {
-        if (f.isDirectory()) {
-            File[] children = f.listFiles();
-            
-            if (children != null) {
-                for (File child : children) {
-                    processNativeFolder(offset, child, nativeLibraries);
-                }
-            }
-        } else if (f.isFile()) {
-            if (PATTERN_NATIVELIB_EXT.matcher(f.getName()).matches()) {
-                String path = NATIVE_LIB_ROOT + 
-                        f.getAbsolutePath().substring(offset).replace('\\', '/');
-                
-                nativeLibraries.add(new ApkFile(f, path));
-            }
+        } catch (ApkCreationException e) {
+            printAndExit(e.getMessage());
         }
     }
 
     /**
-     * Creates the application package
-     * @param outFile 
-     * @param zipArchives
-     * @param resourcesJars 
-     * @param files 
-     * @param javaResources 
-     * keystore type of the Java VM is used.
+     * API entry point similar to the {@link #main(String[])} method.
+     * <p/>Unlike {@link #main(String[])}, this will not call {@link System#exit(int)} and instead
+     * will throw exceptions.
+     * @param args command line arguments.
+     * @throws WrongOptionException if the command line arguments are incorrect.
+     * @throws FileNotFoundException if a required file was not found.
+     * @throws ApkCreationException if an error happened during the creation of the APK.
      */
-    public void createPackage(File outFile, ArrayList<FileInputStream> zipArchives,
-            ArrayList<File> files, ArrayList<ApkFile> javaResources,
-            ArrayList<FileInputStream> resourcesJars, ArrayList<ApkFile> nativeLibraries) {
-        
-        // get the debug key
-        try {
-            SignedJarBuilder builder;
-
-            if (mSignedPackage) {
-                System.err.println(String.format("Using keystore: %s",
-                        DebugKeyProvider.getDefaultKeyStoreOsPath()));
-                
-                
-                DebugKeyProvider keyProvider = new DebugKeyProvider(
-                        null /* osKeyPath: use default */,
-                        mStoreType, null /* IKeyGenOutput */);
-                PrivateKey key = keyProvider.getDebugKey();
-                X509Certificate certificate = (X509Certificate)keyProvider.getCertificate();
-                
-                if (key == null) {
-                    throw new IllegalArgumentException("Unable to get debug signature key");
-                }
-                
-                // compare the certificate expiration date
-                if (certificate != null && certificate.getNotAfter().compareTo(new Date()) < 0) {
-                    // TODO, regenerate a new one.
-                    throw new IllegalArgumentException("Debug Certificate expired on " +
-                            DateFormat.getInstance().format(certificate.getNotAfter()));
-                }
-
-                builder = new SignedJarBuilder(
-                        new FileOutputStream(outFile.getAbsolutePath(), false /* append */), key,
-                        certificate);
-            } else {
-                builder = new SignedJarBuilder(
-                        new FileOutputStream(outFile.getAbsolutePath(), false /* append */),
-                        null /* key */, null /* certificate */);
-            }
-
-            // add the archives
-            for (FileInputStream input : zipArchives) {
-                builder.writeZip(input, null /* filter */);
-            }
-
-            // add the single files
-            for (File input : files) {
-                // always put the file at the root of the archive in this case
-                builder.writeFile(input, input.getName());
-                if (mVerbose) {
-                    System.err.println(String.format("%1$s => %2$s", input.getAbsolutePath(),
-                            input.getName()));
-                }
-            }
-            
-            // add the java resource from the source folders.
-            for (ApkFile resource : javaResources) {
-                builder.writeFile(resource.file, resource.archivePath);
-                if (mVerbose) {
-                    System.err.println(String.format("%1$s => %2$s",
-                            resource.file.getAbsolutePath(), resource.archivePath));
-                }
-            }
-
-            // add the java resource from jar files.
-            for (FileInputStream input : resourcesJars) {
-                builder.writeZip(input, mResourceFilter);
-            }
-            
-            // add the native files
-            for (ApkFile file : nativeLibraries) {
-                builder.writeFile(file.file, file.archivePath);
-                if (mVerbose) {
-                    System.err.println(String.format("%1$s => %2$s", file.file.getAbsolutePath(),
-                            file.archivePath));
-                }
-            }
-            
-            // close and sign the application package.
-            builder.close();
-        } catch (KeytoolException e) {
-            if (e.getJavaHome() == null) {
-                throw new IllegalArgumentException(e.getMessage() + 
-                        "\nJAVA_HOME seems undefined, setting it will help locating keytool automatically\n" +
-                        "You can also manually execute the following command\n:" +
-                        e.getCommandLine());
-            } else {
-                throw new IllegalArgumentException(e.getMessage() + 
-                        "\nJAVA_HOME is set to: " + e.getJavaHome() +
-                        "\nUpdate it if necessary, or manually execute the following command:\n" +
-                        e.getCommandLine());
-            }
-        } catch (AndroidLocationException e) {
-            throw new IllegalArgumentException(e);
-        } catch (Exception e) {
-            throw new IllegalArgumentException(e);
-        }
+    public static void createApk(String[] args) throws FileNotFoundException, WrongOptionException,
+            ApkCreationException {
+        new ApkBuilderImpl().run(args);
     }
 
-    private void printUsageAndQuit() {
+    private static void printUsageAndQuit() {
         // 80 cols marker:  01234567890123456789012345678901234567890123456789012345678901234567890123456789
         System.err.println("A command line tool to package an Android application from various sources.");
         System.err.println("Usage: apkbuilder <out archive> [-v][-u][-storetype STORE_TYPE] [-z inputzip]");
@@ -455,11 +105,11 @@
         System.err.println("");
         System.err.println("    -nf     Followed by the root folder containing native libraries to");
         System.err.println("            include in the application package.");
-        
+
         System.exit(1);
     }
-    
-    private void printAndExit(String... messages) {
+
+    private static void printAndExit(String... messages) {
         for (String message : messages) {
             System.err.println(message);
         }
diff --git a/tools/apkbuilder/src/com/android/apkbuilder/internal/ApkBuilderImpl.java b/tools/apkbuilder/src/com/android/apkbuilder/internal/ApkBuilderImpl.java
new file mode 100644
index 0000000..780af75
--- /dev/null
+++ b/tools/apkbuilder/src/com/android/apkbuilder/internal/ApkBuilderImpl.java
@@ -0,0 +1,437 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed 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.
+ */
+
+package com.android.apkbuilder.internal;
+
+import com.android.apkbuilder.ApkBuilder.WrongOptionException;
+import com.android.apkbuilder.ApkBuilder.ApkCreationException;
+import com.android.jarutils.DebugKeyProvider;
+import com.android.jarutils.JavaResourceFilter;
+import com.android.jarutils.SignedJarBuilder;
+import com.android.jarutils.DebugKeyProvider.KeytoolException;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.regex.Pattern;
+
+/**
+ * Command line APK builder with signing support.
+ */
+public final class ApkBuilderImpl {
+
+    private final static Pattern PATTERN_JAR_EXT = Pattern.compile("^.+\\.jar$",
+            Pattern.CASE_INSENSITIVE);
+    private final static Pattern PATTERN_NATIVELIB_EXT = Pattern.compile("^.+\\.so$",
+            Pattern.CASE_INSENSITIVE);
+
+    private final static String NATIVE_LIB_ROOT = "lib/";
+
+    /**
+     * A File to be added to the APK archive.
+     * <p/>This includes the {@link File} representing the file and its path in the archive.
+     */
+    public final static class ApkFile {
+        String archivePath;
+        File file;
+
+        ApkFile(File file, String path) {
+            this.file = file;
+            this.archivePath = path;
+        }
+    }
+
+    private JavaResourceFilter mResourceFilter = new JavaResourceFilter();
+    private boolean mVerbose = false;
+    private boolean mSignedPackage = true;
+    /** the optional type of the debug keystore. If <code>null</code>, the default */
+    private String mStoreType = null;
+
+    public void setVerbose(boolean verbose) {
+        mVerbose = verbose;
+    }
+
+    public void setSignedPackage(boolean signedPackage) {
+        mSignedPackage = signedPackage;
+    }
+
+    public void run(String[] args) throws WrongOptionException, FileNotFoundException,
+            ApkCreationException {
+        if (args.length < 1) {
+            throw new WrongOptionException("No options specified");
+        }
+
+        // read the first args that should be a file path
+        File outFile = getOutFile(args[0]);
+
+        ArrayList<FileInputStream> zipArchives = new ArrayList<FileInputStream>();
+        ArrayList<File> archiveFiles = new ArrayList<File>();
+        ArrayList<ApkFile> javaResources = new ArrayList<ApkFile>();
+        ArrayList<FileInputStream> resourcesJars = new ArrayList<FileInputStream>();
+        ArrayList<ApkFile> nativeLibraries = new ArrayList<ApkFile>();
+
+        int index = 1;
+        do {
+            String argument = args[index++];
+
+            if ("-v".equals(argument)) {
+                mVerbose = true;
+            } else if ("-u".equals(argument)) {
+                mSignedPackage = false;
+            } else if ("-z".equals(argument)) {
+                // quick check on the next argument.
+                if (index == args.length)  {
+                    throw new WrongOptionException("Missing value for -z");
+                }
+
+                try {
+                    FileInputStream input = new FileInputStream(args[index++]);
+                    zipArchives.add(input);
+                } catch (FileNotFoundException e) {
+                    throw new ApkCreationException("-z file is not found");
+                }
+            } else if ("-f". equals(argument)) {
+                // quick check on the next argument.
+                if (index == args.length) {
+                    throw new WrongOptionException("Missing value for -f");
+                }
+
+                archiveFiles.add(getInputFile(args[index++]));
+            } else if ("-rf". equals(argument)) {
+                // quick check on the next argument.
+                if (index == args.length) {
+                    throw new WrongOptionException("Missing value for -rf");
+                }
+
+                processSourceFolderForResource(args[index++], javaResources);
+            } else if ("-rj". equals(argument)) {
+                // quick check on the next argument.
+                if (index == args.length) {
+                    throw new WrongOptionException("Missing value for -rj");
+                }
+
+                processJarFolder(args[index++], resourcesJars);
+            } else if ("-nf".equals(argument)) {
+                // quick check on the next argument.
+                if (index == args.length) {
+                    throw new WrongOptionException("Missing value for -nf");
+                }
+
+                String parameter = args[index++];
+                File f = new File(parameter);
+
+                // compute the offset to get the relative path
+                int offset = parameter.length();
+                if (parameter.endsWith(File.separator) == false) {
+                    offset++;
+                }
+
+                processNativeFolder(offset, f, nativeLibraries);
+            } else if ("-storetype".equals(argument)) {
+                // quick check on the next argument.
+                if (index == args.length) {
+                    throw new WrongOptionException("Missing value for -storetype");
+                }
+
+                mStoreType  = args[index++];
+            } else {
+                throw new WrongOptionException("Unknown argument: " + argument);
+            }
+        } while (index < args.length);
+
+        createPackage(outFile, zipArchives, archiveFiles, javaResources, resourcesJars,
+                nativeLibraries);
+    }
+
+
+    private File getOutFile(String filepath) throws ApkCreationException {
+        File f = new File(filepath);
+
+        if (f.isDirectory()) {
+            throw new ApkCreationException(filepath + " is a directory!");
+        }
+
+        if (f.exists()) { // will be a file in this case.
+            if (f.canWrite() == false) {
+                throw new ApkCreationException("Cannot write " + filepath);
+            }
+        } else {
+            try {
+                if (f.createNewFile() == false) {
+                    throw new ApkCreationException("Failed to create " + filepath);
+                }
+            } catch (IOException e) {
+                throw new ApkCreationException(
+                        "Failed to create '" + filepath + "' : " + e.getMessage());
+            }
+        }
+
+        return f;
+    }
+
+    public static File getInputFile(String filepath) throws ApkCreationException {
+        File f = new File(filepath);
+
+        if (f.isDirectory()) {
+            throw new ApkCreationException(filepath + " is a directory!");
+        }
+
+        if (f.exists()) {
+            if (f.canRead() == false) {
+                throw new ApkCreationException("Cannot read " + filepath);
+            }
+        } else {
+            throw new ApkCreationException(filepath + " does not exists!");
+        }
+
+        return f;
+    }
+
+    /**
+     * Processes a source folder and adds its java resources to a given list of {@link ApkFile}.
+     * @param folderPath the path to the source folder.
+     * @param javaResources the list of {@link ApkFile} to fill.
+     * @throws ApkCreationException
+     */
+    public static void processSourceFolderForResource(String folderPath,
+            ArrayList<ApkFile> javaResources) throws ApkCreationException {
+
+        File folder = new File(folderPath);
+
+        if (folder.isDirectory()) {
+            // file is a directory, process its content.
+            File[] files = folder.listFiles();
+            for (File file : files) {
+                processFileForResource(file, null, javaResources);
+            }
+        } else {
+            // not a directory? output error and quit.
+            if (folder.exists()) {
+                throw new ApkCreationException(folderPath + " is not a folder!");
+            } else {
+                throw new ApkCreationException(folderPath + " does not exist!");
+            }
+        }
+    }
+
+    public static void processJarFolder(String parameter, Collection<FileInputStream> resourcesJars)
+            throws FileNotFoundException {
+        File f = new File(parameter);
+        if (f.isDirectory()) {
+            String[] files = f.list(new FilenameFilter() {
+                public boolean accept(File dir, String name) {
+                    return PATTERN_JAR_EXT.matcher(name).matches();
+                }
+            });
+
+            for (String file : files) {
+                String path = f.getAbsolutePath() + File.separator + file;
+                FileInputStream input = new FileInputStream(path);
+                resourcesJars.add(input);
+            }
+        } else {
+            FileInputStream input = new FileInputStream(parameter);
+            resourcesJars.add(input);
+        }
+    }
+
+
+    /**
+     * Processes a {@link File} that could be a {@link ApkFile}, or a folder containing
+     * java resources.
+     * @param file the {@link File} to process.
+     * @param path the relative path of this file to the source folder. Can be <code>null</code> to
+     * identify a root file.
+     * @param javaResources the Collection of {@link ApkFile} object to fill.
+     */
+    private static void processFileForResource(File file, String path,
+            Collection<ApkFile> javaResources) {
+        if (file.isDirectory()) {
+            // a directory? we check it
+            if (JavaResourceFilter.checkFolderForPackaging(file.getName())) {
+                // if it's valid, we append its name to the current path.
+                if (path == null) {
+                    path = file.getName();
+                } else {
+                    path = path + "/" + file.getName();
+                }
+
+                // and process its content.
+                File[] files = file.listFiles();
+                for (File contentFile : files) {
+                    processFileForResource(contentFile, path, javaResources);
+                }
+            }
+        } else {
+            // a file? we check it
+            if (JavaResourceFilter.checkFileForPackaging(file.getName())) {
+                // we append its name to the current path
+                if (path == null) {
+                    path = file.getName();
+                } else {
+                    path = path + "/" + file.getName();
+                }
+
+                // and add it to the list.
+                javaResources.add(new ApkFile(file, path));
+            }
+        }
+    }
+
+    /**
+     * Process a {@link File} for native library inclusion.
+     * @param offset the length of the root folder (used to compute relative path)
+     * @param f the {@link File} to process
+     * @param nativeLibraries the collection to add native libraries to.
+     */
+    public static void processNativeFolder(int offset, File f,
+            Collection<ApkFile> nativeLibraries) {
+        if (f.isDirectory()) {
+            File[] children = f.listFiles();
+
+            if (children != null) {
+                for (File child : children) {
+                    processNativeFolder(offset, child, nativeLibraries);
+                }
+            }
+        } else if (f.isFile()) {
+            if (PATTERN_NATIVELIB_EXT.matcher(f.getName()).matches()) {
+                String path = NATIVE_LIB_ROOT +
+                        f.getAbsolutePath().substring(offset).replace('\\', '/');
+
+                nativeLibraries.add(new ApkFile(f, path));
+            }
+        }
+    }
+
+    /**
+     * Creates the application package
+     * @param outFile the package file to create
+     * @param zipArchives the list of zip archive
+     * @param files the list of files to include in the archive
+     * @param javaResources the list of java resources from the source folders.
+     * @param resourcesJars the list of jar files from which to take java resources
+     * @throws ApkCreationException
+     */
+    public void createPackage(File outFile, Iterable<? extends FileInputStream> zipArchives,
+            Iterable<? extends File> files, Iterable<? extends ApkFile> javaResources,
+            Iterable<? extends FileInputStream> resourcesJars,
+            Iterable<? extends ApkFile> nativeLibraries) throws ApkCreationException {
+
+        // get the debug key
+        try {
+            SignedJarBuilder builder;
+
+            if (mSignedPackage) {
+                System.err.println(String.format("Using keystore: %s",
+                        DebugKeyProvider.getDefaultKeyStoreOsPath()));
+
+
+                DebugKeyProvider keyProvider = new DebugKeyProvider(
+                        null /* osKeyPath: use default */,
+                        mStoreType, null /* IKeyGenOutput */);
+                PrivateKey key = keyProvider.getDebugKey();
+                X509Certificate certificate = (X509Certificate)keyProvider.getCertificate();
+
+                if (key == null) {
+                    throw new ApkCreationException("Unable to get debug signature key");
+                }
+
+                // compare the certificate expiration date
+                if (certificate != null && certificate.getNotAfter().compareTo(new Date()) < 0) {
+                    // TODO, regenerate a new one.
+                    throw new ApkCreationException("Debug Certificate expired on " +
+                            DateFormat.getInstance().format(certificate.getNotAfter()));
+                }
+
+                builder = new SignedJarBuilder(
+                        new FileOutputStream(outFile.getAbsolutePath(), false /* append */), key,
+                        certificate);
+            } else {
+                builder = new SignedJarBuilder(
+                        new FileOutputStream(outFile.getAbsolutePath(), false /* append */),
+                        null /* key */, null /* certificate */);
+            }
+
+            // add the archives
+            for (FileInputStream input : zipArchives) {
+                builder.writeZip(input, null /* filter */);
+            }
+
+            // add the single files
+            for (File input : files) {
+                // always put the file at the root of the archive in this case
+                builder.writeFile(input, input.getName());
+                if (mVerbose) {
+                    System.err.println(String.format("%1$s => %2$s", input.getAbsolutePath(),
+                            input.getName()));
+                }
+            }
+
+            // add the java resource from the source folders.
+            for (ApkFile resource : javaResources) {
+                builder.writeFile(resource.file, resource.archivePath);
+                if (mVerbose) {
+                    System.err.println(String.format("%1$s => %2$s",
+                            resource.file.getAbsolutePath(), resource.archivePath));
+                }
+            }
+
+            // add the java resource from jar files.
+            for (FileInputStream input : resourcesJars) {
+                builder.writeZip(input, mResourceFilter);
+            }
+
+            // add the native files
+            for (ApkFile file : nativeLibraries) {
+                builder.writeFile(file.file, file.archivePath);
+                if (mVerbose) {
+                    System.err.println(String.format("%1$s => %2$s", file.file.getAbsolutePath(),
+                            file.archivePath));
+                }
+            }
+
+            // close and sign the application package.
+            builder.close();
+        } catch (KeytoolException e) {
+            if (e.getJavaHome() == null) {
+                throw new ApkCreationException(e.getMessage() +
+                        "\nJAVA_HOME seems undefined, setting it will help locating keytool automatically\n" +
+                        "You can also manually execute the following command\n:" +
+                        e.getCommandLine());
+            } else {
+                throw new ApkCreationException(e.getMessage() +
+                        "\nJAVA_HOME is set to: " + e.getJavaHome() +
+                        "\nUpdate it if necessary, or manually execute the following command:\n" +
+                        e.getCommandLine());
+            }
+        } catch (AndroidLocationException e) {
+            throw new ApkCreationException(e);
+        } catch (Exception e) {
+            throw new ApkCreationException(e);
+        }
+    }
+}
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java
index aebf60a..f7c71b7 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java
@@ -57,7 +57,7 @@
     public static final String MANIFEST_SDK_URL = "/reference/android/R.styleable.html#";  //$NON-NLS-1$
 
     public static final String IMAGE_KEY = "image"; //$NON-NLS-1$
-    
+
     private static final String CODE  = "$code";  //$NON-NLS-1$
     private static final String LINK  = "$link";  //$NON-NLS-1$
     private static final String ELEM  = "$elem";  //$NON-NLS-1$
@@ -72,7 +72,7 @@
         /**
          * Creates a new {@link TextAttributeDescriptor} instance for the given XML name,
          * UI name and tooltip.
-         * 
+         *
          * @param xmlName The XML attribute name.
          * @param uiName The UI attribute name.
          * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
@@ -86,7 +86,7 @@
 
     /**
      * Add all {@link AttributeInfo} to the the array of {@link AttributeDescriptor}.
-     * 
+     *
      * @param attributes The list of {@link AttributeDescriptor} to append to
      * @param elementXmlName Optional XML local name of the element to which attributes are
      *              being added. When not null, this is used to filter overrides.
@@ -120,7 +120,7 @@
 
     /**
      * Add an {@link AttributeInfo} to the the array of {@link AttributeDescriptor}.
-     * 
+     *
      * @param attributes The list of {@link AttributeDescriptor} to append to
      * @param elementXmlName Optional XML local name of the element to which attributes are
      *              being added. When not null, this is used to filter overrides.
@@ -145,13 +145,13 @@
         if (required) {
             uiName += "*"; //$NON-NLS-1$
         }
-        
+
         String tooltip = null;
         String rawTooltip = info.getJavaDoc();
         if (rawTooltip == null) {
             rawTooltip = "";
         }
-        
+
         String deprecated = info.getDeprecatedDoc();
         if (deprecated != null) {
             if (rawTooltip.length() > 0) {
@@ -172,7 +172,7 @@
         if (flen > 0) {
             // Fill the formats in a set for faster access
             HashSet<Format> formats_set = new HashSet<Format>();
-            
+
             StringBuilder sb = new StringBuilder();
             if (rawTooltip != null && rawTooltip.length() > 0) {
                 sb.append(rawTooltip);
@@ -220,7 +220,7 @@
                         overrideAttrLocalName = elements[elements.length - 1];
                         elements = elements[0].split(",");       //$NON-NLS-1$
                     }
-                    
+
                     if (overrideAttrLocalName == null ||
                             !overrideAttrLocalName.equals(xmlLocalName)) {
                         continue;
@@ -236,7 +236,7 @@
                             }
                         }
                     }
-                    
+
                     if (!ok_element) {
                         continue;
                     }
@@ -247,7 +247,7 @@
                             // The override is instance of the class to create, which must
                             // have a constructor compatible with TextAttributeDescriptor.
                             @SuppressWarnings("unchecked") //$NON-NLS-1$
-                            Class<? extends TextAttributeDescriptor> clazz = 
+                            Class<? extends TextAttributeDescriptor> clazz =
                                 (Class<? extends TextAttributeDescriptor>) override;
                             Constructor<? extends TextAttributeDescriptor> cons;
                                 cons = clazz.getConstructor(new Class<?>[] {
@@ -310,7 +310,7 @@
      * Indicates the the given {@link AttributeInfo} already exists in the ArrayList of
      * {@link AttributeDescriptor}. This test for the presence of a descriptor with the same
      * XML name.
-     * 
+     *
      * @param attributes The list of {@link AttributeDescriptor} to compare to.
      * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
      *              See {@link SdkConstants#NS_RESOURCES} for a common value.
@@ -370,9 +370,9 @@
                 buf.append(c);
             }
         }
-        
+
         name = buf.toString();
-        
+
         // Replace these acronyms by upper-case versions
         // - (?<=^| ) means "if preceded by a space or beginning of string"
         // - (?=$| )  means "if followed by a space or end of string"
@@ -381,11 +381,11 @@
 
         return name;
     }
-    
+
     /**
      * Capitalizes the string, i.e. transforms the initial [a-z] into [A-Z].
      * Returns the string unmodified if the first character is not [a-z].
-     * 
+     *
      * @param str The string to capitalize.
      * @return The capitalized string
      */
@@ -393,7 +393,7 @@
         if (str == null || str.length() < 1 || str.charAt(0) < 'a' || str.charAt(0) > 'z') {
             return str;
         }
-        
+
         StringBuilder sb = new StringBuilder();
         sb.append((char)(str.charAt(0) + 'A' - 'a'));
         sb.append(str.substring(1));
@@ -405,7 +405,7 @@
      */
     public static String formatTooltip(String javadoc) {
         ArrayList<String> spans = scanJavadoc(javadoc);
-        
+
         StringBuilder sb = new StringBuilder();
         boolean needBreak = false;
 
@@ -430,7 +430,7 @@
                 if (text != null) {
                     text = text.trim();
                 }
-                
+
                 // If there's no text, use the anchor if there's one
                 if (text == null || text.length() == 0) {
                     text = anchor;
@@ -441,12 +441,12 @@
                         // If we still have no text, use the base as text
                         text = base;
                     }
-                } 
+                }
 
                 if (text != null) {
                     sb.append(text);
                 }
-                
+
             } else if (ELEM.equals(s)) {
                 s = spans.get(++i);
                 if (s != null) {
@@ -462,17 +462,17 @@
                 needBreak = false;
             }
         }
-        
+
         return sb.toString();
     }
-    
+
     /**
      * Formats the javadoc tooltip to be usable in a FormText.
      * <p/>
      * If the descriptor can provide an icon, the caller should provide
      * elementsDescriptor.getIcon() as "image" to FormText, e.g.:
      * <code>formText.setImage(IMAGE_KEY, elementsDescriptor.getIcon());</code>
-     * 
+     *
      * @param javadoc The javadoc to format. Cannot be null.
      * @param elementDescriptor The element descriptor parent of the javadoc. Cannot be null.
      * @param androidDocBaseUrl The base URL for the documentation. Cannot be null. Should be
@@ -488,9 +488,9 @@
         if (sdkUrl != null && sdkUrl.startsWith(MANIFEST_SDK_URL)) {
             fullSdkUrl = androidDocBaseUrl + sdkUrl;
         }
-        
+
         StringBuilder sb = new StringBuilder();
-        
+
         Image icon = elementDescriptor.getIcon();
         if (icon != null) {
             sb.append("<form><li style=\"image\" value=\"" +        //$NON-NLS-1$
@@ -516,7 +516,7 @@
                 String base   = spans.get(++i);
                 String anchor = spans.get(++i);
                 String text   = spans.get(++i);
-                
+
                 if (base != null) {
                     base = base.trim();
                 }
@@ -526,7 +526,7 @@
                 if (text != null) {
                     text = text.trim();
                 }
-                
+
                 // If there's no text, use the anchor if there's one
                 if (text == null || text.length() == 0) {
                     text = anchor;
@@ -560,7 +560,7 @@
                         // If we still have no text, use the base as text
                         text = base;
                     }
-                } 
+                }
 
                 if (url != null && text != null) {
                     sb.append("<a href=\"");                        //$NON-NLS-1$
@@ -600,15 +600,15 @@
 
     private static ArrayList<String> scanJavadoc(String javadoc) {
         ArrayList<String> spans = new ArrayList<String>();
-        
+
         // Standardize all whitespace in the javadoc to single spaces.
         if (javadoc != null) {
             javadoc = javadoc.replaceAll("[ \t\f\r\n]+", " "); //$NON-NLS-1$ //$NON-NLS-2$
         }
-        
+
         // Detects {@link <base>#<name> <text>} where all 3 are optional
         Pattern p_link = Pattern.compile("\\{@link\\s+([^#\\}\\s]*)(?:#([^\\s\\}]*))?(?:\\s*([^\\}]*))?\\}(.*)"); //$NON-NLS-1$
-        // Detects <code>blah</code> 
+        // Detects <code>blah</code>
         Pattern p_code = Pattern.compile("<code>(.+?)</code>(.*)");                 //$NON-NLS-1$
         // Detects @blah@, used in hard-coded tooltip descriptors
         Pattern p_elem = Pattern.compile("@([\\w -]+)@(.*)");                       //$NON-NLS-1$
@@ -621,7 +621,7 @@
 
         int currentLength = 0;
         String text = null;
-        
+
         while(javadoc != null && javadoc.length() > 0) {
             Matcher m;
             String s = null;
@@ -665,7 +665,7 @@
             }
             if (s != null && s.length() > 0) {
                 s = cleanupJavadocHtml(s);
-                
+
                 if (currentLength >= JAVADOC_BREAK_LENGTH) {
                     spans.add(BREAK);
                     currentLength = 0;
@@ -680,12 +680,12 @@
                     currentLength = 0;
                     s = s.substring(pos + 1);
                 }
-                
+
                 spans.add(s);
                 currentLength += s.length();
             }
         }
-        
+
         return spans;
     }
 
@@ -722,15 +722,13 @@
                 fill ? LayoutConstants.VALUE_FILL_PARENT : LayoutConstants.VALUE_WRAP_CONTENT,
                 false /* override */);
 
-        String widget_id = getFreeWidgetId(ui_node.getUiRoot(),
-                new Object[] { ui_node.getDescriptor().getXmlLocalName(), null, null, null });
+        String widget_id = getFreeWidgetId(ui_node);
         if (widget_id != null) {
-            ui_node.setAttributeValue(LayoutConstants.ATTR_ID, "@+id/" + widget_id, //$NON-NLS-1$
-                    false /* override */);
+            ui_node.setAttributeValue(LayoutConstants.ATTR_ID, widget_id, false /* override */);
         }
 
         ui_node.setAttributeValue(LayoutConstants.ATTR_TEXT, widget_id, false /*override*/);
-        
+
         if (updateLayout) {
             UiElementNode ui_parent = ui_node.getUiParent();
             if (ui_parent != null &&
@@ -752,19 +750,26 @@
     /**
      * Given a UI root node, returns the first available id that matches the
      * pattern "prefix%02d".
-     *  
+     * <p/>TabWidget is a special case and the method will always return "@android:id/tabs".
+     *
      * @param uiNode The UI node that gives the prefix to match.
-     * @return A suitable generated id
+     * @return A suitable generated id in the attribute form needed by the XML id tag
+     * (e.g. "@+id/something")
      */
     public static String getFreeWidgetId(UiElementNode uiNode) {
-        return getFreeWidgetId(uiNode.getUiRoot(),
-                new Object[] { uiNode.getDescriptor().getXmlLocalName(), null, null, null });
+        String name = uiNode.getDescriptor().getXmlLocalName();
+        if ("TabWidget".equals(name)) {                        //$NON-NLS-1$
+            return "@android:id/tabs";                         //$NON-NLS-1$
+        }
+
+        return "@+id/" + getFreeWidgetId(uiNode.getUiRoot(),   //$NON-NLS-1$
+                new Object[] { name, null, null, null });
     }
 
     /**
      * Given a UI root node, returns the first available id that matches the
      * pattern "prefix%02d".
-     * 
+     *
      * For recursion purposes, a "context" is given. Since Java doesn't have in-out parameters
      * in methods and we're not going to do a dedicated type, we just use an object array which
      * must contain one initial item and several are built on the fly just for internal storage:
@@ -775,7 +780,7 @@
      * <li> map(Set<String>): A set of the ids collected so far when walking through the widget
      *                        hierarchy. Must start with null.
      * </ul>
-     *  
+     *
      * @param uiRoot The Ui root node where to start searching recursively. For the initial call
      *               you want to pass the document root.
      * @param params An in-out context of parameters used during recursion, as explained above.
@@ -837,9 +842,9 @@
         for (UiElementNode uiChild : uiRoot.getUiChildren()) {
             getFreeWidgetId(uiChild, params);
         }
-        
+
         // Note: return params[2] (not "generated") since it could have changed during recursion.
         return (String) params[2];
     }
-    
+
 }
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/avdmanager/AvdManagerListPage.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/avdmanager/AvdManagerListPage.java
index 9708a0f..5867f7e 100755
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/avdmanager/AvdManagerListPage.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/avdmanager/AvdManagerListPage.java
@@ -434,35 +434,6 @@
     }
 
     /**
-     * Triggered when the user selects the "delete" button (the extra action in the selector)
-     * Deletes the currently selected AVD, if any.
-     *
-     * This is obsolete. Kept around to reuse the code later in the AvdSelector itself.
-     */
-    @Deprecated
-    private void onDelete() {
-        AvdInfo avdInfo = mAvdSelector.getSelected();
-        AvdManager avdm = getAvdManager();
-        if (avdInfo == null || avdm == null) {
-            return;
-        }
-
-        // Confirm you want to delete this AVD
-        if (!AdtPlugin.displayPrompt("Delete Android Virtual Device",
-                String.format("Please confirm that you want to delete the Android Virtual Device named '%s'. This operation cannot be reverted.",
-                        avdInfo.getName()))) {
-            return;
-        }
-
-        SdkLog log = new SdkLog(String.format("Result of deleting AVD '%s':", avdInfo.getName()));
-
-        boolean success = avdm.deleteAvd(avdInfo, log);
-
-        log.display(success);
-        reloadAvdList();
-    }
-
-    /**
      * Triggered when the user selects the "create" button.
      */
     private void onCreate() {
diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java
index 2c71e0f..b260c89 100644
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java
@@ -41,6 +41,11 @@
      */
     public final static int CURRENT_PLATFORM = currentPlatform();
 
+    /**
+     * Charset for the ini file handled by the SDK.
+     */
+    public final static String INI_CHARSET = "UTF-8";
+
     /** An SDK Project's AndroidManifest.xml file */
     public static final String FN_ANDROID_MANIFEST_XML= "AndroidManifest.xml";
     /** An SDK Project's build.xml file */
diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkManager.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkManager.java
index b09018b..85160a3 100644
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkManager.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkManager.java
@@ -180,7 +180,9 @@
                 writer.write(String.format("0x%04x\n", i));
             }
         } finally {
-            writer.close();
+            if (writer != null) {
+                writer.close();
+            }
         }
     }
 
@@ -606,7 +608,7 @@
 
 
     /**
-     * Parses a property file and returns
+     * Parses a property file (using UTF-8 encoding) and returns
      * @param buildProp the property file to parse
      * @param log the ISdkLog object receiving warning/error from the parsing.
      * @return the map of (key,value) pairs, or null if the parsing failed.
@@ -616,7 +618,7 @@
         BufferedReader reader = null;
         try {
             fis = new FileInputStream(buildProp);
-            reader = new BufferedReader(new InputStreamReader(fis));
+            reader = new BufferedReader(new InputStreamReader(fis, SdkConstants.INI_CHARSET));
 
             String line = null;
             Map<String, String> map = new HashMap<String, String>();
diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/avd/AvdManager.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/avd/AvdManager.java
index 3225236..5179a0d 100644
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/avd/AvdManager.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/avd/AvdManager.java
@@ -32,6 +32,7 @@
 import java.io.FilenameFilter;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -44,7 +45,7 @@
  * Android Virtual Device Manager to manage AVDs.
  */
 public final class AvdManager {
-    
+
     /**
      * Exception thrown when something is wrong with a target path.
      */
@@ -55,16 +56,16 @@
             super(message);
         }
     }
-    
+
     public static final String AVD_FOLDER_EXTENSION = ".avd";
 
     public final static String AVD_INFO_PATH = "path";
     public final static String AVD_INFO_TARGET = "target";
-    
+
     /**
      * AVD/config.ini key name representing the SDK-relative path of the skin folder, if any,
      * or a 320x480 like constant for a numeric skin size.
-     * 
+     *
      * @see #NUMERIC_SKIN_SIZE
      */
     public final static String AVD_INI_SKIN_PATH = "skin.path";
@@ -79,14 +80,14 @@
      * AVD/config.ini key name representing the path to the sdcard file.
      * If missing, the default name "sdcard.img" will be used for the sdcard, if there's such
      * a file.
-     * 
+     *
      * @see #SDCARD_IMG
      */
     public final static String AVD_INI_SDCARD_PATH = "sdcard.path";
     /**
      * AVD/config.ini key name representing the size of the SD card.
      * This property is for UI purposes only. It is not used by the emulator.
-     * 
+     *
      * @see #SDCARD_SIZE_PATTERN
      */
     public final static String AVD_INI_SDCARD_SIZE = "sdcard.size";
@@ -101,7 +102,7 @@
     /**
      * AVD/config.ini key name representing the second path where the emulator looks
      * for system images. Typically this is the path to the platform system image.
-     * 
+     *
      * @see #AVD_INI_IMAGES_1
      */
     public final static String AVD_INI_IMAGES_2 = "image.sysdir.2";
@@ -135,7 +136,7 @@
 
     /** An immutable structure describing an Android Virtual Device. */
     public static final class AvdInfo {
-        
+
         /**
          * Status for an {@link AvdInfo}. Indicates whether or not this AVD is valid.
          */
@@ -155,7 +156,7 @@
             /** System Image folder in config.ini doesn't exist */
             ERROR_IMAGE_DIR;
         }
-        
+
         private final String mName;
         private final String mPath;
         private final String mTargetHash;
@@ -164,11 +165,11 @@
         private final AvdStatus mStatus;
 
         /**
-         * Creates a new valid AVD info. Values are immutable. 
+         * Creates a new valid AVD info. Values are immutable.
          * <p/>
          * Such an AVD is available and can be used.
          * The error string is set to null.
-         * 
+         *
          * @param name The name of the AVD (for display or reference)
          * @param path The path to the config.ini file
          * @param targetHash the target hash
@@ -181,11 +182,11 @@
         }
 
         /**
-         * Creates a new <em>invalid</em> AVD info. Values are immutable. 
+         * Creates a new <em>invalid</em> AVD info. Values are immutable.
          * <p/>
          * Such an AVD is not complete and cannot be used.
          * The error string must be non-null.
-         * 
+         *
          * @param name The name of the AVD (for display or reference)
          * @param path The path to the config.ini file
          * @param targetHash the target hash
@@ -212,7 +213,7 @@
         public String getPath() {
             return mPath;
         }
-        
+
         /**
          * Returns the target hash string.
          */
@@ -230,8 +231,8 @@
             return mStatus;
         }
 
-        /** 
-         * Helper method that returns the .ini {@link File} for a given AVD name. 
+        /**
+         * Helper method that returns the .ini {@link File} for a given AVD name.
          * @throws AndroidLocationException if there's a problem getting android root directory.
          */
         public static File getIniFile(String name) throws AndroidLocationException {
@@ -240,23 +241,23 @@
             return new File(avdRoot, name + INI_EXTENSION);
         }
 
-        /** 
-         * Returns the .ini {@link File} for this AVD. 
+        /**
+         * Returns the .ini {@link File} for this AVD.
          * @throws AndroidLocationException if there's a problem getting android root directory.
          */
         public File getIniFile() throws AndroidLocationException {
             return getIniFile(mName);
         }
-        
-        /** 
-         * Helper method that returns the Config {@link File} for a given AVD name. 
+
+        /**
+         * Helper method that returns the Config {@link File} for a given AVD name.
          */
         public static File getConfigFile(String path) {
             return new File(path, CONFIG_INI);
         }
-        
-        /** 
-         * Returns the Config {@link File} for this AVD. 
+
+        /**
+         * Returns the Config {@link File} for this AVD.
          */
         public File getConfigFile() {
             return getConfigFile(mPath);
@@ -268,7 +269,7 @@
         public Map<String, String> getProperties() {
             return mProperties;
         }
-        
+
         /**
          * Returns the error message for the AVD or <code>null</code> if {@link #getStatus()}
          * returns {@link AvdStatus#OK}
@@ -299,7 +300,7 @@
             } catch (AndroidLocationException e) {
                 return "Unable to get HOME folder.";
             }
-            
+
             return null;
         }
     }
@@ -309,14 +310,14 @@
     private AvdInfo[] mBrokenAvdList;
     private final SdkManager mSdk;
 
-    /** 
+    /**
      * TODO remove this field. Each caller should give its own logger to the methods
      * here instead of relying on a global logger. Otherwise that means that each
      * caller needs to re-instantiate this just to change the logger, which is plain
      * ugly.
-     * 
+     *
      * We'll revisit this in the final AVD Manager UI for donut.
-     */ 
+     */
     private ISdkLog mSdkLog;
 
     public AvdManager(SdkManager sdk, ISdkLog sdkLog) throws AndroidLocationException {
@@ -327,9 +328,9 @@
 
     /**
      * Changes the current {@link ISdkLog}.
-     * 
+     *
      * This method should not exist. See comments for {@link #mSdkLog}.
-     * 
+     *
      * @return The {@link ISdkLog} that was currently being used.
      * @see #mSdkLog
      */
@@ -362,7 +363,7 @@
                         list.add(avd);
                     }
                 }
-                
+
                 mValidAvdList = list.toArray(new AvdInfo[list.size()]);
             }
             return mValidAvdList;
@@ -413,7 +414,7 @@
 
         return null;
     }
-    
+
     /**
      * Reloads the AVD list.
      * @throws AndroidLocationException if there was an error finding the location of the
@@ -434,7 +435,7 @@
 
     /**
      * Creates a new AVD. It is expected that there is no existing AVD with this name already.
-     * 
+     *
      * @param avdFolder the data folder for the AVD. It will be created as needed.
      * @param name the name of the AVD
      * @param target the target of the AVD
@@ -444,12 +445,12 @@
      * @param hardwareConfig the hardware setup for the AVD. Can be null to use defaults.
      * @param removePrevious If true remove any previous files.
      * @return The new {@link AvdInfo} in case of success (which has just been added to the
-     *         internal list) or null in case of failure. 
+     *         internal list) or null in case of failure.
      */
     public AvdInfo createAvd(File avdFolder, String name, IAndroidTarget target,
             String skinName, String sdcard, Map<String,String> hardwareConfig,
             boolean removePrevious) {
-        
+
         File iniFile = null;
         boolean needCleanup = false;
         try {
@@ -478,33 +479,33 @@
             // writes the userdata.img in it.
             String imagePath = target.getPath(IAndroidTarget.IMAGES);
             File userdataSrc = new File(imagePath, USERDATA_IMG);
-            
+
             if (userdataSrc.exists() == false && target.isPlatform() == false) {
                 imagePath = target.getParent().getPath(IAndroidTarget.IMAGES);
                 userdataSrc = new File(imagePath, USERDATA_IMG);
             }
-            
+
             if (userdataSrc.exists() == false) {
                 mSdkLog.error(null, "Unable to find a '%1$s' file to copy into the AVD folder.",
                         USERDATA_IMG);
                 needCleanup = true;
                 return null;
             }
-            
+
             FileInputStream fis = new FileInputStream(userdataSrc);
-            
+
             File userdataDest = new File(avdFolder, USERDATA_IMG);
             FileOutputStream fos = new FileOutputStream(userdataDest);
-            
+
             byte[] buffer = new byte[4096];
             int count;
             while ((count = fis.read(buffer)) != -1) {
                 fos.write(buffer, 0, count);
             }
-            
+
             fos.close();
             fis.close();
-            
+
             // Config file.
             HashMap<String, String> values = new HashMap<String, String>();
 
@@ -512,7 +513,7 @@
                 needCleanup = true;
                 return null;
             }
-            
+
             // Now the skin.
             if (skinName == null || skinName.length() == 0) {
                 skinName = target.getDefaultSkin();
@@ -545,31 +546,31 @@
                 } else {
                     // Sdcard is possibly a size. In that case we create a file called 'sdcard.img'
                     // in the AVD folder, and do not put any value in config.ini.
-                    
+
                     // First, check that it matches the pattern for sdcard size
                     Matcher m = SDCARD_SIZE_PATTERN.matcher(sdcard);
                     if (m.matches()) {
                         // create the sdcard.
                         sdcardFile = new File(avdFolder, SDCARD_IMG);
                         String path = sdcardFile.getAbsolutePath();
-                        
+
                         // execute mksdcard with the proper parameters.
                         File toolsFolder = new File(mSdk.getLocation(), SdkConstants.FD_TOOLS);
                         File mkSdCard = new File(toolsFolder, SdkConstants.mkSdCardCmdName());
-                        
+
                         if (mkSdCard.isFile() == false) {
                             mSdkLog.error(null, "'%1$s' is missing from the SDK tools folder.",
                                     mkSdCard.getName());
                             needCleanup = true;
                             return null;
                         }
-                        
+
                         if (createSdCard(mkSdCard.getAbsolutePath(), sdcard, path) == false) {
                             needCleanup = true;
                             return null; // mksdcard output has already been displayed, no need to
                                          // output anything else.
                         }
-                        
+
                         // add a property containing the size of the sdcard for display purpose
                         // only when the dev does 'android list avd'
                         values.put(AVD_INI_SDCARD_SIZE, sdcard);
@@ -592,7 +593,7 @@
 
             File configIniFile = new File(avdFolder, CONFIG_INI);
             writeIniFile(configIniFile, values);
-            
+
             if (mSdkLog != null) {
                 if (target.isPlatform()) {
                     mSdkLog.printf("Created AVD '%1$s' based on %2$s\n", name, target.getName());
@@ -601,7 +602,7 @@
                                target.getVendor());
                 }
             }
-            
+
             // create the AvdInfo object, and add it to the list
             AvdInfo newAvdInfo = new AvdInfo(name,
                     avdFolder.getAbsolutePath(),
@@ -609,7 +610,7 @@
                     target, values);
 
             AvdInfo oldAvdInfo = getAvd(name, false /*validAvdOnly*/);
-            
+
             synchronized (mAllAvdList) {
                 if (oldAvdInfo != null && removePrevious) {
                     mAllAvdList.remove(oldAvdInfo);
@@ -619,7 +620,7 @@
             }
 
             if (removePrevious &&
-                    newAvdInfo != null && 
+                    newAvdInfo != null &&
                     oldAvdInfo != null &&
                     !oldAvdInfo.getPath().equals(newAvdInfo.getPath())) {
                 mSdkLog.warning("Removing previous AVD directory at %s", oldAvdInfo.getPath());
@@ -643,12 +644,12 @@
                 if (iniFile != null && iniFile.exists()) {
                     iniFile.delete();
                 }
-                
+
                 recursiveDelete(avdFolder);
                 avdFolder.delete();
             }
         }
-        
+
         return null;
     }
 
@@ -668,7 +669,7 @@
             assert false;
             throw new InvalidTargetPathException("Target location is not inside the SDK.");
         }
-        
+
         File folder = new File(imageFullPath);
         if (folder.isDirectory()) {
             String[] list = folder.list(new FilenameFilter() {
@@ -682,36 +683,36 @@
                 if (imageFullPath.charAt(0) == File.separatorChar) {
                     imageFullPath = imageFullPath.substring(1);
                 }
-        
+
                 return imageFullPath;
             }
         }
-        
+
         return null;
     }
-    
+
     /**
      * Returns the path to the skin, as a relative path to the SDK.
      */
     private String getSkinRelativePath(String skinName, IAndroidTarget target) {
         // first look to see if the skin is in the target
-        
+
         String path = target.getPath(IAndroidTarget.SKINS);
         File skin = new File(path, skinName);
-        
+
         if (skin.exists() == false && target.isPlatform() == false) {
             target = target.getParent();
 
             path = target.getPath(IAndroidTarget.SKINS);
             skin = new File(path, skinName);
         }
-        
+
         // skin really does not exist!
         if (skin.exists() == false) {
             mSdkLog.error(null, "Skin '%1$s' does not exist.", skinName);
             return null;
         }
-        
+
         // get the skin path
         path = skin.getAbsolutePath();
 
@@ -733,7 +734,7 @@
 
     /**
      * Creates the ini file for an AVD.
-     * 
+     *
      * @param name of the AVD.
      * @param avdFolder path for the data folder of the AVD.
      * @param target of the AVD.
@@ -750,10 +751,10 @@
 
         return iniFile;
     }
-    
+
     /**
      * Creates the ini file for an AVD.
-     * 
+     *
      * @param info of the AVD.
      * @throws AndroidLocationException if there's a problem getting android root directory.
      * @throws IOException if {@link File#getAbsolutePath()} fails.
@@ -772,17 +773,17 @@
      * could not be loaded due to some error. That means this method still tries to remove
      * the AVD ini file or its folder if it can be found. An error will be output if any of
      * these operations fail.
-     * 
+     *
      * @param avdInfo the information on the AVD to delete
      * @return True if the AVD was deleted with no error.
      */
     public boolean deleteAvd(AvdInfo avdInfo, ISdkLog log) {
         try {
             boolean error = false;
-            
+
             File f = avdInfo.getIniFile();
             if (f != null && f.exists()) {
-                log.warning("Deleting file %1$s", f.getCanonicalPath());
+                log.printf("Deleting file %1$s", f.getCanonicalPath());
                 if (!f.delete()) {
                     log.error(null, "Failed to delete %1$s", f.getCanonicalPath());
                     error = true;
@@ -793,7 +794,7 @@
             if (path != null) {
                 f = new File(path);
                 if (f.exists()) {
-                    log.warning("Deleting folder %1$s", f.getCanonicalPath());
+                    log.printf("Deleting folder %1$s", f.getCanonicalPath());
                     recursiveDelete(f);
                     if (!f.delete()) {
                         log.error(null, "Failed to delete %1$s", f.getCanonicalPath());
@@ -805,10 +806,10 @@
             removeAvd(avdInfo);
 
             if (error) {
-                log.printf("AVD '%1$s' deleted with errors. See warnings above.\n",
+                log.printf("\nAVD '%1$s' deleted with errors. See errors above.",
                         avdInfo.getName());
             } else {
-                log.printf("AVD '%1$s' deleted.\n", avdInfo.getName());
+                log.printf("\nAVD '%1$s' deleted.", avdInfo.getName());
                 return true;
             }
 
@@ -819,22 +820,22 @@
         }
         return false;
     }
-    
+
     /**
      * Moves and/or rename an existing AVD and its files.
      * This also change it in the manager's list.
      * <p/>
      * The caller should make sure the name or path given are valid, do not exist and are
      * actually different than current values.
-     * 
+     *
      * @param avdInfo the information on the AVD to move.
      * @param newName the new name of the AVD if non null.
      * @param paramFolderPath the new data folder if non null.
      * @return True if the move succeeded or there was nothing to do.
-     *         If false, this method will have had already output error in the log. 
+     *         If false, this method will have had already output error in the log.
      */
     public boolean moveAvd(AvdInfo avdInfo, String newName, String paramFolderPath, ISdkLog log) {
-        
+
         try {
             if (paramFolderPath != null) {
                 File f = new File(avdInfo.getPath());
@@ -844,7 +845,7 @@
                             avdInfo.getPath(), paramFolderPath);
                     return false;
                 }
-    
+
                 // update AVD info
                 AvdInfo info = new AvdInfo(avdInfo.getName(), paramFolderPath,
                         avdInfo.getTargetHash(), avdInfo.getTarget(), avdInfo.getProperties());
@@ -857,10 +858,10 @@
             if (newName != null) {
                 File oldIniFile = avdInfo.getIniFile();
                 File newIniFile = AvdInfo.getIniFile(newName);
-                
+
                 log.warning("Moving '%1$s' to '%2$s'.", oldIniFile.getPath(), newIniFile.getPath());
                 if (!oldIniFile.renameTo(newIniFile)) {
-                    log.error(null, "Failed to move '%1$s' to '%2$s'.", 
+                    log.error(null, "Failed to move '%1$s' to '%2$s'.",
                             oldIniFile.getPath(), newIniFile.getPath());
                     return false;
                 }
@@ -885,7 +886,7 @@
 
     /**
      * Helper method to recursively delete a folder's content (but not the folder itself).
-     * 
+     *
      * @throws SecurityException like {@link File#delete()} does if file/folder is not writable.
      */
     public void recursiveDelete(File folder) {
@@ -902,7 +903,7 @@
      * <p/>
      * This lists the $HOME/.android/avd/<name>.ini files.
      * Such files are properties file than then indicate where the AVD folder is located.
-     * 
+     *
      * @return A new {@link File} array or null. The array might be empty.
      * @throws AndroidLocationException if there's a problem getting android root directory.
      */
@@ -920,7 +921,7 @@
             folder.mkdirs();
             return null;
         }
-        
+
         File[] avds = folder.listFiles(new FilenameFilter() {
             public boolean accept(File parent, String name) {
                 if (INI_NAME_PATTERN.matcher(name).matches()) {
@@ -932,14 +933,14 @@
                 return false;
             }
         });
-        
+
         return avds;
     }
 
     /**
      * Computes the internal list of available AVDs
      * @param allList the list to contain all the AVDs
-     * 
+     *
      * @throws AndroidLocationException if there's a problem getting android root directory.
      */
     private void buildAvdList(ArrayList<AvdInfo> allList) throws AndroidLocationException {
@@ -956,7 +957,7 @@
 
     /**
      * Parses an AVD .ini file to create an {@link AvdInfo}.
-     * 
+     *
      * @param path The path to the AVD .ini file
      * @return A new {@link AvdInfo} with an {@link AvdStatus} indicating whether this AVD is
      *         valid or not.
@@ -970,7 +971,7 @@
         IAndroidTarget target = null;
         File configIniFile = null;
         Map<String, String> properties = null;
-        
+
         if (targetHash != null) {
             target = mSdk.getTargetFromHashString(targetHash);
         }
@@ -979,7 +980,7 @@
         if (avdPath != null) {
             configIniFile = new File(avdPath, CONFIG_INI);
         }
-        
+
         if (configIniFile != null) {
             properties = SdkManager.parsePropertyFile(configIniFile, mSdkLog);
         }
@@ -990,7 +991,7 @@
         if (matcher.matches()) {
             name = matcher.group(1);
         }
-        
+
         // check the image.sysdir are valid
         boolean validImageSysdir = true;
         if (properties != null) {
@@ -1012,7 +1013,7 @@
         }
 
         AvdStatus status;
-        
+
         if (avdPath == null) {
             status = AvdStatus.ERROR_PATH;
         } else if (configIniFile == null) {
@@ -1028,7 +1029,7 @@
         } else {
             status = AvdStatus.OK;
         }
-        
+
         AvdInfo info = new AvdInfo(
                 name,
                 avdPath,
@@ -1036,30 +1037,31 @@
                 target,
                 properties,
                 status);
-        
+
         return info;
     }
 
     /**
-     * Writes a .ini file from a set of properties.
-     * 
+     * Writes a .ini file from a set of properties, using UTF-8 encoding.
+     *
      * @param iniFile The file to generate.
      * @param values THe properties to place in the ini file.
      * @throws IOException if {@link FileWriter} fails to open, write or close the file.
      */
     private static void writeIniFile(File iniFile, Map<String, String> values)
             throws IOException {
-        FileWriter writer = new FileWriter(iniFile);
-        
+        OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(iniFile),
+                SdkConstants.INI_CHARSET);
+
         for (Entry<String, String> entry : values.entrySet()) {
             writer.write(String.format("%1$s=%2$s\n", entry.getKey(), entry.getValue()));
         }
         writer.close();
     }
-    
+
     /**
      * Invokes the tool to create a new SD card image file.
-     * 
+     *
      * @param toolLocation The path to the mksdcard tool.
      * @param size The size of the new SD Card, compatible with {@link #SDCARD_SIZE_PATTERN}.
      * @param location The path of the new sdcard image file to generate.
@@ -1072,7 +1074,7 @@
             command[1] = size;
             command[2] = location;
             Process process = Runtime.getRuntime().exec(command);
-    
+
             ArrayList<String> errorOutput = new ArrayList<String>();
             ArrayList<String> stdOutput = new ArrayList<String>();
             int status = grabProcessOutput(process, errorOutput, stdOutput,
@@ -1091,7 +1093,7 @@
         } catch (IOException e) {
             // pass, print error below
         }
-        
+
         mSdkLog.error(null, "Failed to create the SD card.");
         return false;
     }
@@ -1102,7 +1104,7 @@
      * @param process The process to get the ouput from
      * @param errorOutput The array to store the stderr output. cannot be null.
      * @param stdOutput The array to store the stdout output. cannot be null.
-     * @param waitforReaders if true, this will wait for the reader threads. 
+     * @param waitforReaders if true, this will wait for the reader threads.
      * @return the process return code.
      * @throws InterruptedException
      */
@@ -1179,7 +1181,7 @@
 
     /**
      * Removes an {@link AvdInfo} from the internal list.
-     * 
+     *
      * @param avdInfo The {@link AvdInfo} to remove.
      * @return true if this {@link AvdInfo} was present and has been removed.
      */
@@ -1190,15 +1192,15 @@
                 return true;
             }
         }
-        
+
         return false;
     }
 
     /**
      * Updates an AVD with new path to the system image folders.
      * @param name the name of the AVD to update.
-     * @throws IOException 
-     * @throws AndroidLocationException 
+     * @throws IOException
+     * @throws AndroidLocationException
      */
     public void updateAvd(String name) throws IOException, AndroidLocationException {
         // find the AVD to update. It should be be in the broken list.
@@ -1211,7 +1213,7 @@
                 }
             }
         }
-        
+
         if (avd == null) {
             // not in the broken list, just return.
             mSdkLog.error(null, "There is no Android Virtual Device named '%s'.", name);
@@ -1226,21 +1228,21 @@
         if (oldProperties != null) {
             properties.putAll(oldProperties);
         }
-        
+
         AvdStatus status;
-        
+
         // create the path to the new system images.
         if (setImagePathProperties(avd.getTarget(), properties)) {
             if (properties.containsKey(AVD_INI_IMAGES_1)) {
                 mSdkLog.printf("Updated '%1$s' with value '%2$s'\n", AVD_INI_IMAGES_1,
                         properties.get(AVD_INI_IMAGES_1));
             }
-    
+
             if (properties.containsKey(AVD_INI_IMAGES_2)) {
                 mSdkLog.printf("Updated '%1$s' with value '%2$s'\n", AVD_INI_IMAGES_2,
                         properties.get(AVD_INI_IMAGES_2));
             }
-            
+
             status = AvdStatus.OK;
         } else {
             mSdkLog.error(null, "Unable to find non empty system images folders for %1$s", name);
@@ -1263,7 +1265,7 @@
                 avd.getTarget(),
                 properties,
                 status);
-        
+
         replaceAvd(avd, newAvd);
     }
 
@@ -1276,18 +1278,18 @@
     private boolean setImagePathProperties(IAndroidTarget target, Map<String, String> properties) {
         properties.remove(AVD_INI_IMAGES_1);
         properties.remove(AVD_INI_IMAGES_2);
-        
+
         try {
             String property = AVD_INI_IMAGES_1;
-            
+
             // First the image folders of the target itself
             String imagePath = getImageRelativePath(target);
             if (imagePath != null) {
                 properties.put(property, imagePath);
                 property = AVD_INI_IMAGES_2;
             }
-    
-    
+
+
             // If the target is an add-on we need to add the Platform image as a backup.
             IAndroidTarget parent = target.getParent();
             if (parent != null) {
@@ -1296,16 +1298,16 @@
                     properties.put(property, imagePath);
                 }
             }
-            
+
             // we need at least one path!
             return properties.containsKey(AVD_INI_IMAGES_1);
         } catch (InvalidTargetPathException e) {
             mSdkLog.error(e, e.getMessage());
         }
-        
+
         return false;
     }
-    
+
     /**
      * Replaces an old {@link AvdInfo} with a new one in the lists storing them.
      * @param oldAvd the {@link AvdInfo} to remove.
diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/project/ProjectProperties.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/project/ProjectProperties.java
index c64bf7f..33f8837 100644
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/project/ProjectProperties.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/project/ProjectProperties.java
@@ -17,11 +17,14 @@
 package com.android.sdklib.internal.project;
 
 import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.SdkConstants;
 import com.android.sdklib.SdkManager;
 
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.FileWriter;
 import java.io.IOException;
+import java.io.OutputStreamWriter;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -35,12 +38,12 @@
     public final static String PROPERTY_TARGET = "target";
     public final static String PROPERTY_APK_CONFIGS = "apk-configurations";
     public final static String PROPERTY_SDK = "sdk-location";
-    
+
     public static enum PropertyType {
         BUILD("build.properties", BUILD_HEADER),
         DEFAULT("default.properties", DEFAULT_HEADER),
         LOCAL("local.properties", LOCAL_HEADER);
-        
+
         private final String mFilename;
         private final String mHeader;
 
@@ -48,14 +51,14 @@
             mFilename = filename;
             mHeader = header;
         }
-        
+
         public String getFilename() {
             return mFilename;
         }
     }
-    
+
     private final static String LOCAL_HEADER =
-//           1-------10--------20--------30--------40--------50--------60--------70--------80        
+//           1-------10--------20--------30--------40--------50--------60--------70--------80
             "# This file is automatically generated by Android Tools.\n" +
             "# Do not modify this file -- YOUR CHANGES WILL BE ERASED!\n" +
             "# \n" +
@@ -64,7 +67,7 @@
             "\n";
 
     private final static String DEFAULT_HEADER =
-//          1-------10--------20--------30--------40--------50--------60--------70--------80        
+//          1-------10--------20--------30--------40--------50--------60--------70--------80
            "# This file is automatically generated by Android Tools.\n" +
            "# Do not modify this file -- YOUR CHANGES WILL BE ERASED!\n" +
            "# \n" +
@@ -76,7 +79,7 @@
            "\n";
 
     private final static String BUILD_HEADER =
-//          1-------10--------20--------30--------40--------50--------60--------70--------80        
+//          1-------10--------20--------30--------40--------50--------60--------70--------80
            "# This file is used to override default values used by the Ant build system.\n" +
            "# \n" +
            "# This file must be checked in Version Control Systems, as it is\n" +
@@ -95,7 +98,7 @@
 
     private final static Map<String, String> COMMENT_MAP = new HashMap<String, String>();
     static {
-//               1-------10--------20--------30--------40--------50--------60--------70--------80        
+//               1-------10--------20--------30--------40--------50--------60--------70--------80
         COMMENT_MAP.put(PROPERTY_TARGET,
                 "# Project target.\n");
         COMMENT_MAP.put(PROPERTY_APK_CONFIGS,
@@ -114,7 +117,7 @@
                 "# For customization when using a Version Control System, please read the\n" +
                 "# header note.\n");
     }
-    
+
     private final String mProjectFolderOsPath;
     private final Map<String, String> mProperties;
     private final PropertyType mType;
@@ -122,9 +125,9 @@
     /**
      * Loads a project properties file and return a {@link ProjectProperties} object
      * containing the properties
-     * 
+     *
      * @param projectFolderOsPath the project folder.
-     * @param type One the possible {@link PropertyType}s. 
+     * @param type One the possible {@link PropertyType}s.
      */
     public static ProjectProperties load(String projectFolderOsPath, PropertyType type) {
         File projectFolder = new File(projectFolderOsPath);
@@ -139,7 +142,7 @@
         }
         return null;
     }
- 
+
     /**
      * Merges all properties from the given file into the current properties.
      * <p/>
@@ -153,8 +156,8 @@
      * <li>The result is that this contains all the properties from default plus those
      *     overridden by the build.properties file.
      * </ul>
-     * 
-     * @param type One the possible {@link PropertyType}s. 
+     *
+     * @param type One the possible {@link PropertyType}s.
      * @return this object, for chaining.
      */
     public ProjectProperties merge(PropertyType type) {
@@ -187,7 +190,7 @@
         // create and return a ProjectProperties with an empty map.
         return new ProjectProperties(projectFolderOsPath, new HashMap<String, String>(), type);
     }
-    
+
     /**
      * Sets a new properties. If a property with the same name already exists, it is replaced.
      * @param name the name of the property.
@@ -196,7 +199,7 @@
     public void setProperty(String name, String value) {
         mProperties.put(name, value);
     }
-    
+
     /**
      * Sets the target property to the given {@link IAndroidTarget} object.
      * @param target the Android target.
@@ -205,7 +208,7 @@
         assert mType == PropertyType.DEFAULT;
         mProperties.put(PROPERTY_TARGET, target.hashString());
     }
-    
+
     /**
      * Returns the value of a property.
      * @param name the name of the property.
@@ -214,7 +217,7 @@
     public String getProperty(String name) {
         return mProperties.get(name);
     }
-    
+
     /**
      * Removes a property and returns its previous value (or null if the property did not exist).
      * @param name the name of the property to remove.
@@ -224,17 +227,18 @@
     }
 
     /**
-     * Saves the property file.
+     * Saves the property file, using UTF-8 encoding.
      * @throws IOException
      */
     public void save() throws IOException {
         File toSave = new File(mProjectFolderOsPath, mType.mFilename);
-        
-        FileWriter writer = new FileWriter(toSave);
-        
+
+        OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(toSave),
+                SdkConstants.INI_CHARSET);
+
         // write the header
         writer.write(mType.mHeader);
-        
+
         // write the properties.
         for (Entry<String, String> entry : mProperties.entrySet()) {
             String comment = COMMENT_MAP.get(entry.getKey());
@@ -245,11 +249,11 @@
             value = value.replaceAll("\\\\", "\\\\\\\\");
             writer.write(String.format("%s=%s\n", entry.getKey(), value));
         }
-        
+
         // close the file to flush
         writer.close();
     }
-    
+
     /**
      * Private constructor.
      * <p/>
diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonPackage.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonPackage.java
index 6d39098..e273a54 100755
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonPackage.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonPackage.java
@@ -28,6 +28,7 @@
 

 import java.io.File;

 import java.util.ArrayList;

+import java.util.Map;

 

 /**

  * Represents an add-on XML node in an SDK repository.

@@ -64,13 +65,13 @@
      * <p/>

      * This constructor should throw an exception if the package cannot be created.

      */

-    AddonPackage(RepoSource source, Node packageNode) {

-        super(source, packageNode);

-        mVendor   = getXmlString(packageNode, SdkRepository.NODE_VENDOR);

-        mName     = getXmlString(packageNode, SdkRepository.NODE_NAME);

-        mApiLevel = getXmlInt   (packageNode, SdkRepository.NODE_API_LEVEL, 0);

+    AddonPackage(RepoSource source, Node packageNode, Map<String,String> licenses) {

+        super(source, packageNode, licenses);

+        mVendor   = XmlParserUtils.getXmlString(packageNode, SdkRepository.NODE_VENDOR);

+        mName     = XmlParserUtils.getXmlString(packageNode, SdkRepository.NODE_NAME);

+        mApiLevel = XmlParserUtils.getXmlInt   (packageNode, SdkRepository.NODE_API_LEVEL, 0);

 

-        mLibs = parseLibs(getFirstChild(packageNode, SdkRepository.NODE_LIBS));

+        mLibs = parseLibs(XmlParserUtils.getFirstChild(packageNode, SdkRepository.NODE_LIBS));

     }

 

     /**

@@ -131,8 +132,8 @@
      * Parses a <lib> element from a <libs> container.

      */

     private Lib parseLib(Node libNode) {

-        return new Lib(getXmlString(libNode, SdkRepository.NODE_NAME),

-                       getXmlString(libNode, SdkRepository.NODE_DESCRIPTION));

+        return new Lib(XmlParserUtils.getXmlString(libNode, SdkRepository.NODE_NAME),

+                       XmlParserUtils.getXmlString(libNode, SdkRepository.NODE_DESCRIPTION));

     }

 

     /** Returns the vendor, a string, for add-on packages. */

@@ -182,25 +183,46 @@
      * has this add-ons installed, we'll use that one.

      *

      * @param osSdkRoot The OS path of the SDK root folder.

+     * @param suggestedDir A suggestion for the installation folder name, based on the root

+     *                     folder used in the zip archive.

+     * @param sdkManager An existing SDK manager to list current platforms and addons.

      * @return A new {@link File} corresponding to the directory to use to install this package.

      */

     @Override

-    public File getInstallFolder(String osSdkRoot) {

+    public File getInstallFolder(String osSdkRoot, String suggestedDir, SdkManager sdkManager) {

         File addons = new File(osSdkRoot, SdkConstants.FD_ADDONS);

 

-        String name = String.format("%s-%d", getName(), getApiLevel()); // $NON-NLS-1$

+        // First find if this add-on is already installed. If so, reuse the same directory.

+        for (IAndroidTarget target : sdkManager.getTargets()) {

+            if (!target.isPlatform() &&

+                    target.getApiVersionNumber() == getApiLevel() &&

+                    target.getName().equals(getName()) &&

+                    target.getVendor().equals(getVendor())) {

+                return new File(target.getLocation());

+            }

+        }

 

-        // FIXME this will fail if the name is not ASCII compatible. This could easily

-        // happen: a Chinese or Japanese name etc for example,

-        // to name a few.

-        name = name.toLowerCase();

-        name = name.replaceAll("[^a-zA-Z0-9_-]+", "_");                 // $NON-NLS-1$

-        name = name.replaceAll("_+", "_");                              // $NON-NLS-1$

+        // Otherwise, see about reusing the suggestedDir. It must not be already used or

+        // add some index to it, or we try to make up one.

+        String name = suggestedDir;

 

-        File folder = new File(addons, name);

+        if (suggestedDir == null || suggestedDir.length() == 0) {

+            name = String.format("addon-%s-%s-%d", getName(), getVendor(), getApiLevel()); //$NON-NLS-1$

+            name = name.toLowerCase();

+            name = name.replaceAll("[^a-z0-9_-]+", "_");      //$NON-NLS-1$ //$NON-NLS-2$

+            name = name.replaceAll("_+", "_");                //$NON-NLS-1$ //$NON-NLS-2$

+        }

 

-        // TODO find similar existing addon in addons folder

-        return folder;

+        for (int i = 0; i < 100; i++) {

+            String name2 = i == 0 ? name : String.format("%s-%d", name, i); //$NON-NLS-1$

+            File folder = new File(addons, name2);

+            if (!folder.exists()) {

+                return folder;

+            }

+        }

+

+        // We shouldn't really get here. I mean, seriously, we tried hard enough.

+        return null;

     }

 

     /**

diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Archive.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Archive.java
index 08a536b..191ef19 100755
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Archive.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Archive.java
@@ -16,6 +16,8 @@
 

 package com.android.sdklib.internal.repository;

 

+import com.android.sdklib.SdkManager;

+

 import java.io.File;

 import java.io.FileInputStream;

 import java.io.FileOutputStream;

@@ -341,11 +343,23 @@
      *

      * @return True if the archive was installed, false otherwise.

      */

-    public boolean install(String osSdkRoot, boolean forceHttp, ITaskMonitor monitor) {

+    public boolean install(String osSdkRoot,

+            boolean forceHttp,

+            SdkManager sdkManager,

+            ITaskMonitor monitor) {

+

+        Package pkg = getParentPackage();

 

         File archiveFile = null;

         try {

-            String name = getParentPackage().getShortDescription();

+            String name = pkg.getShortDescription();

+

+            if (pkg instanceof ExtraPackage && !((ExtraPackage) pkg).isPathValid()) {

+                monitor.setResult("Skipping %1$s: %2$s is not a valid install path.",

+                        name,

+                        ((ExtraPackage) pkg).getPath());

+                return false;

+            }

 

             if (isLocal()) {

                 // This should never happen.

@@ -364,7 +378,7 @@
 

             archiveFile = downloadFile(monitor, forceHttp);

             if (archiveFile != null) {

-                if (unarchive(osSdkRoot, archiveFile, monitor)) {

+                if (unarchive(osSdkRoot, archiveFile, sdkManager, monitor)) {

                     monitor.setResult("Installed: %1$s", name);

                     return true;

                 }

@@ -568,48 +582,63 @@
     /**

      * Install the given archive in the given folder.

      */

-    private boolean unarchive(String osSdkRoot, File archiveFile, ITaskMonitor monitor) {

-        String name = getParentPackage().getShortDescription();

-        String desc = String.format("Installing %1$s", name);

-        monitor.setDescription(desc);

+    private boolean unarchive(String osSdkRoot,

+            File archiveFile,

+            SdkManager sdkManager,

+            ITaskMonitor monitor) {

+        String pkgName = getParentPackage().getShortDescription();

+        String pkgDesc = String.format("Installing %1$s", pkgName);

+        monitor.setDescription(pkgDesc);

 

-        File destFolder = getParentPackage().getInstallFolder(osSdkRoot);

+        // We always unzip in a temp folder which name depends on the package type

+        // (e.g. addon, tools, etc.) and then move the folder to the destination folder.

+        // If the destination folder exists, it will be renamed and deleted at the very

+        // end if everything succeeded.

 

-        File unzipDestFolder = destFolder;

+        String pkgKind = getParentPackage().getClass().getSimpleName();

+

+        File destFolder = null;

+        File unzipDestFolder = null;

         File renamedDestFolder = null;

 

         try {

-            // If this folder already exists, unzip in a temporary folder and then move/unlink.

-            if (destFolder.exists()) {

-                // Find a new temp folder that doesn't exist yet

-                unzipDestFolder = findTempFolder(destFolder, "new");  //$NON-NLS-1$

+            // Find a new temp folder that doesn't exist yet

+            unzipDestFolder = findTempFolder(osSdkRoot, pkgKind, "new");  //$NON-NLS-1$

 

-                if (unzipDestFolder == null) {

-                    // this should not seriously happen.

-                    monitor.setResult("Failed to find a suitable temp directory similar to %1$s.",

-                            destFolder.getPath());

-                    return false;

-                }

+            if (unzipDestFolder == null) {

+                // this should not seriously happen.

+                monitor.setResult("Failed to find a temp directory in %1$s.", osSdkRoot);

+                return false;

             }

 

             if (!unzipDestFolder.mkdirs()) {

-                monitor.setResult("Failed to create directory %1$s",

-                        unzipDestFolder.getPath());

+                monitor.setResult("Failed to create directory %1$s", unzipDestFolder.getPath());

                 return false;

             }

 

-            if (!unzipFolder(archiveFile, getSize(), unzipDestFolder, desc, monitor)) {

+            String[] zipRootFolder = new String[] { null };

+            if (!unzipFolder(archiveFile, getSize(),

+                    unzipDestFolder, pkgDesc,

+                    zipRootFolder, monitor)) {

                 return false;

             }

 

-            if (unzipDestFolder != destFolder) {

-                // Swap the old folder by the new one.

-                // Both original folders will be deleted in the finally clause below.

-                renamedDestFolder = findTempFolder(destFolder, "old");  //$NON-NLS-1$

+            // Compute destination directory

+            destFolder = getParentPackage().getInstallFolder(

+                    osSdkRoot, zipRootFolder[0], sdkManager);

+

+            if (destFolder == null) {

+                // this should not seriously happen.

+                monitor.setResult("Failed to compute installation directory for %1$s.", pkgName);

+                return false;

+            }

+

+            // Swap the old folder by the new one.

+            if (destFolder.isDirectory()) {

+                renamedDestFolder = findTempFolder(osSdkRoot, pkgKind, "old");  //$NON-NLS-1$

                 if (renamedDestFolder == null) {

                     // this should not seriously happen.

-                    monitor.setResult("Failed to find a suitable temp directory similar to %1$s.",

-                            destFolder.getPath());

+                    monitor.setResult("Failed to find a temp directory in %1$s.", osSdkRoot);

                     return false;

                 }

 

@@ -619,28 +648,36 @@
                     return false;

 

                 }

-                if (!unzipDestFolder.renameTo(destFolder)) {

-                    monitor.setResult("Failed to rename directory %1$s to %2$s",

-                            unzipDestFolder.getPath(), destFolder.getPath());

-                    return false;

-                }

             }

 

+            if (!unzipDestFolder.renameTo(destFolder)) {

+                monitor.setResult("Failed to rename directory %1$s to %2$s",

+                        unzipDestFolder.getPath(), destFolder.getPath());

+                return false;

+            }

+

+            unzipDestFolder = null;

             return true;

 

         } finally {

             // Cleanup if the unzip folder is still set.

             deleteFileOrFolder(renamedDestFolder);

-            if (unzipDestFolder != destFolder) {

-                deleteFileOrFolder(unzipDestFolder);

-            }

+            deleteFileOrFolder(unzipDestFolder);

         }

     }

 

+    /**

+     * Unzips a zip file into the given destination directory.

+     *

+     * The archive file MUST have a unique "root" folder. This root folder is skipped when

+     * unarchiving. However we return that root folder name to the caller, as it can be used

+     * as a template to know what destination directory to use in the Add-on case.

+     */

     private boolean unzipFolder(File archiveFile,

             long compressedSize,

             File unzipDestFolder,

             String description,

+            String[] outZipRootFolder,

             ITaskMonitor monitor) {

 

         description += " (%1$d%%)";

@@ -678,6 +715,9 @@
                 if (pos < 0 || pos == name.length() - 1) {

                     continue;

                 } else {

+                    if (outZipRootFolder[0] == null && pos > 0) {

+                        outZipRootFolder[0] = name.substring(0, pos);

+                    }

                     name = name.substring(pos + 1);

                 }

 

@@ -762,21 +802,27 @@
     }

 

     /**

-     * Finds a temp folder which name is similar to the one of the ideal folder

-     * and with a ".tmpN" appended.

+     * Finds a temp folder in the form of osBasePath/temp/prefix.suffixNNN.

      * <p/>

      * This operation is not atomic so there's no guarantee the folder can't get

      * created in between. This is however unlikely and the caller can assume the

      * returned folder does not exist yet.

      * <p/>

-     * Returns null if no such folder can be found (e.g. if all candidates exist),

-     * which is rather unlikely.

+     * Returns null if no such folder can be found (e.g. if all candidates exist,

+     * which is rather unlikely) or if the base temp folder cannot be created.

      */

-    private File findTempFolder(File idealFolder, String suffix) {

-        String basePath = idealFolder.getPath();

+    private File findTempFolder(String osBasePath, String prefix, String suffix) {

+        File baseTempFolder = new File(osBasePath, "temp");

+

+        if (!baseTempFolder.isDirectory()) {

+            if (!baseTempFolder.mkdirs()) {

+                return null;

+            }

+        }

 

         for (int i = 1; i < 100; i++) {

-            File folder = new File(String.format("%1$s.%2$s%3$02d", basePath, suffix, i));  //$NON-NLS-1$

+            File folder = new File(baseTempFolder,

+                    String.format("%1$s.%2$s%3$02d", prefix, suffix, i));  //$NON-NLS-1$

             if (!folder.exists()) {

                 return folder;

             }

diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/DocPackage.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/DocPackage.java
index 66e3255..e7fa893 100755
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/DocPackage.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/DocPackage.java
@@ -17,6 +17,7 @@
 package com.android.sdklib.internal.repository;

 

 import com.android.sdklib.SdkConstants;

+import com.android.sdklib.SdkManager;

 import com.android.sdklib.internal.repository.Archive.Arch;

 import com.android.sdklib.internal.repository.Archive.Os;

 import com.android.sdklib.repository.SdkRepository;

@@ -24,6 +25,7 @@
 import org.w3c.dom.Node;

 

 import java.io.File;

+import java.util.Map;

 

 /**

  * Represents a doc XML node in an SDK repository.

@@ -37,9 +39,9 @@
      * <p/>

      * This constructor should throw an exception if the package cannot be created.

      */

-    DocPackage(RepoSource source, Node packageNode) {

-        super(source, packageNode);

-        mApiLevel = getXmlInt(packageNode, SdkRepository.NODE_API_LEVEL, 0);

+    DocPackage(RepoSource source, Node packageNode, Map<String,String> licenses) {

+        super(source, packageNode, licenses);

+        mApiLevel = XmlParserUtils.getXmlInt(packageNode, SdkRepository.NODE_API_LEVEL, 0);

     }

 

     /**

@@ -98,10 +100,13 @@
      * A "doc" package should always be located in SDK/docs.

      *

      * @param osSdkRoot The OS path of the SDK root folder.

+     * @param suggestedDir A suggestion for the installation folder name, based on the root

+     *                     folder used in the zip archive.

+     * @param sdkManager An existing SDK manager to list current platforms and addons.

      * @return A new {@link File} corresponding to the directory to use to install this package.

      */

     @Override

-    public File getInstallFolder(String osSdkRoot) {

+    public File getInstallFolder(String osSdkRoot, String suggestedDir, SdkManager sdkManager) {

         return new File(osSdkRoot, SdkConstants.FD_DOCS);

     }

 

diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ExtraPackage.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ExtraPackage.java
new file mode 100755
index 0000000..9cd602c
--- /dev/null
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ExtraPackage.java
@@ -0,0 +1,148 @@
+/*

+ * Copyright (C) 2009 The Android Open Source Project

+ *

+ * Licensed 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.

+ */

+

+package com.android.sdklib.internal.repository;

+

+import com.android.sdklib.SdkConstants;

+import com.android.sdklib.SdkManager;

+import com.android.sdklib.internal.repository.Archive.Arch;

+import com.android.sdklib.internal.repository.Archive.Os;

+import com.android.sdklib.repository.SdkRepository;

+

+import org.w3c.dom.Node;

+

+import java.io.File;

+import java.util.Map;

+

+/**

+ * Represents a extra XML node in an SDK repository.

+ */

+public class ExtraPackage extends Package {

+

+    private final String mPath;

+

+    /**

+     * Creates a new tool package from the attributes and elements of the given XML node.

+     * <p/>

+     * This constructor should throw an exception if the package cannot be created.

+     */

+    ExtraPackage(RepoSource source, Node packageNode, Map<String,String> licenses) {

+        super(source, packageNode, licenses);

+        mPath = XmlParserUtils.getXmlString(packageNode, SdkRepository.NODE_PATH);

+    }

+

+    /**

+     * Manually create a new package with one archive and the given attributes.

+     * This is used to create packages from local directories in which case there must be

+     * one archive which URL is the actual target location.

+     */

+    ExtraPackage(RepoSource source,

+            String path,

+            int revision,

+            String license,

+            String description,

+            String descUrl,

+            Os archiveOs,

+            Arch archiveArch,

+            String archiveOsPath) {

+        super(source,

+                revision,

+                license,

+                description,

+                descUrl,

+                archiveOs,

+                archiveArch,

+                archiveOsPath);

+        mPath = path;

+    }

+

+    /**

+     * Static helper to check if a given path is acceptable for an "extra" package.

+     */

+    public boolean isPathValid() {

+        if (SdkConstants.FD_ADDONS.equals(mPath) ||

+                SdkConstants.FD_PLATFORMS.equals(mPath) ||

+                SdkConstants.FD_TOOLS.equals(mPath) ||

+                SdkConstants.FD_DOCS.equals(mPath)) {

+            return false;

+        }

+        return mPath != null && mPath.indexOf('/') == -1 && mPath.indexOf('\\') == -1;

+    }

+

+    /**

+     * The install folder name. It must be a single-segment path.

+     * The paths "add-ons", "platforms", "tools" and "docs" are reserved and cannot be used.

+     * This limitation cannot be written in the XML Schema and must be enforced here by using

+     * the method {@link #isPathValid()} *before* installing the package.

+     */

+    public String getPath() {

+        return mPath;

+    }

+

+    /** Returns a short description for an {@link IDescription}. */

+    @Override

+    public String getShortDescription() {

+        return String.format("Extra %1$s package, revision %2$d", getPath(), getRevision());

+    }

+

+    /** Returns a long description for an {@link IDescription}. */

+    @Override

+    public String getLongDescription() {

+        return String.format("Extra %1$s package, revision %2$d.\n%3$s",

+                getPath(),

+                getRevision(),

+                super.getLongDescription());

+    }

+

+    /**

+     * Computes a potential installation folder if an archive of this package were

+     * to be installed right away in the given SDK root.

+     * <p/>

+     * A "tool" package should always be located in SDK/tools.

+     *

+     * @param osSdkRoot The OS path of the SDK root folder.

+     * @param suggestedDir A suggestion for the installation folder name, based on the root

+     *                     folder used in the zip archive.

+     * @param sdkManager An existing SDK manager to list current platforms and addons.

+     * @return A new {@link File} corresponding to the directory to use to install this package.

+     */

+    @Override

+    public File getInstallFolder(String osSdkRoot, String suggestedDir, SdkManager sdkManager) {

+        return new File(osSdkRoot, getPath());

+    }

+

+    /**

+     * Computes whether the given extra package is a suitable update for the current package.

+     * The base method checks the class type.

+     * The tools package also tests that the revision number is greater and the path is the

+     * same.

+     * <p/>

+     * An update is just that: a new package that supersedes the current one. If the new

+     * package has the same revision as the current one, it's not an update.

+     *

+     * @param replacementPackage The potential replacement package.

+     * @return True if the replacement package is a suitable update for this one.

+     */

+    @Override

+    public boolean canBeUpdatedBy(Package replacementPackage) {

+        if (!super.canBeUpdatedBy(replacementPackage)) {

+            return false;

+        }

+

+        ExtraPackage newPkg = (ExtraPackage) replacementPackage;

+        return newPkg.getRevision() > this.getRevision() && newPkg.getPath().equals(this.getPath());

+    }

+}

diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/LocalSdkParser.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/LocalSdkParser.java
index 4ff8fb8..91e264c 100755
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/LocalSdkParser.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/LocalSdkParser.java
@@ -17,7 +17,6 @@
 package com.android.sdklib.internal.repository;

 

 import com.android.sdklib.IAndroidTarget;

-import com.android.sdklib.ISdkLog;

 import com.android.sdklib.SdkConstants;

 import com.android.sdklib.SdkManager;

 import com.android.sdklib.internal.repository.Archive.Arch;

@@ -36,6 +35,7 @@
 import java.io.InputStream;

 import java.io.StringReader;

 import java.util.ArrayList;

+import java.util.HashMap;

 import java.util.HashSet;

 import java.util.Set;

 import java.util.regex.Matcher;

@@ -63,7 +63,7 @@
     }

 

     /**

-     * Returns the packages found by the last call to {@link #parseSdk(String)}.

+     * Returns the packages found by the last call to {@link #parseSdk(String, SdkManager)}.

      */

     public Package[] getPackages() {

         return mPackages;

@@ -71,7 +71,7 @@
 

     /**

      * Clear the internal packages list. After this call, {@link #getPackages()} will return

-     * null till {@link #parseSdk(String)} is called.

+     * null till {@link #parseSdk(String, SdkManager)} is called.

      */

     public void clearPackages() {

         mPackages = null;

@@ -84,9 +84,10 @@
      * at any time later.

      *

      * @param osSdkRoot The path to the SDK folder.

+     * @param sdkManager An existing SDK manager to list current platforms and addons.

      * @return The packages found. Can be retrieved later using {@link #getPackages()}.

      */

-    public Package[] parseSdk(String osSdkRoot) {

+    public Package[] parseSdk(String osSdkRoot, SdkManager sdkManager) {

         ArrayList<Package> packages = new ArrayList<Package>();

 

         Package pkg = scanDoc(new File(osSdkRoot, SdkConstants.FD_DOCS));

@@ -100,20 +101,7 @@
         }

 

         // for platforms and add-ons, rely on the SdkManager parser

-        SdkManager sdkman = SdkManager.createManager(osSdkRoot, new ISdkLog() {

-            // A dummy sdk logger that doesn't log anything.

-            public void error(Throwable t, String errorFormat, Object... args) {

-                // pass

-            }

-            public void printf(String msgFormat, Object... args) {

-                // pass

-            }

-            public void warning(String warningFormat, Object... args) {

-                // pass

-            }

-        });

-

-        for(IAndroidTarget target : sdkman.getTargets()) {

+        for(IAndroidTarget target : sdkManager.getTargets()) {

             pkg = null;

 

             if (target.isPlatform()) {

@@ -263,6 +251,22 @@
             Node root = getFirstChild(doc, SdkRepository.NODE_SDK_REPOSITORY);

             if (root != null) {

 

+                // Parse license definitions

+                HashMap<String, String> licenses = new HashMap<String, String>();

+                for (Node child = root.getFirstChild();

+                     child != null;

+                     child = child.getNextSibling()) {

+                    if (child.getNodeType() == Node.ELEMENT_NODE &&

+                            SdkRepository.NS_SDK_REPOSITORY.equals(child.getNamespaceURI()) &&

+                            child.getLocalName().equals(SdkRepository.NODE_LICENSE)) {

+                        Node id = child.getAttributes().getNamedItem(SdkRepository.ATTR_ID);

+                        if (id != null) {

+                            licenses.put(id.getNodeValue(), child.getTextContent());

+                        }

+                    }

+                }

+

+                // Parse packages

                 for (Node child = root.getFirstChild();

                      child != null;

                      child = child.getNextSibling()) {

@@ -273,16 +277,16 @@
 

                         try {

                             if (SdkRepository.NODE_ADD_ON.equals(name)) {

-                                return new AddonPackage(null /*source*/, child);

+                                return new AddonPackage(null /*source*/, child, licenses);

 

                             } else if (SdkRepository.NODE_PLATFORM.equals(name)) {

-                                return new PlatformPackage(null /*source*/, child);

+                                return new PlatformPackage(null /*source*/, child, licenses);

 

                             } else if (SdkRepository.NODE_DOC.equals(name)) {

-                                return new DocPackage(null /*source*/, child);

+                                return new DocPackage(null /*source*/, child, licenses);

 

                             } else if (SdkRepository.NODE_TOOL.equals(name)) {

-                                return new ToolPackage(null /*source*/, child);

+                                return new ToolPackage(null /*source*/, child, licenses);

                             }

                         } catch (Exception e) {

                             // Ignore invalid packages

diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Package.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Package.java
index cdac3c1..0e11ef5 100755
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Package.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Package.java
@@ -16,6 +16,7 @@
 

 package com.android.sdklib.internal.repository;

 

+import com.android.sdklib.SdkManager;

 import com.android.sdklib.internal.repository.Archive.Arch;

 import com.android.sdklib.internal.repository.Archive.Os;

 import com.android.sdklib.repository.SdkRepository;

@@ -24,6 +25,7 @@
 

 import java.io.File;

 import java.util.ArrayList;

+import java.util.Map;

 

 /**

  * A {@link Package} is the base class for "something" that can be downloaded from

@@ -51,13 +53,15 @@
      * <p/>

      * This constructor should throw an exception if the package cannot be created.

      */

-    Package(RepoSource source, Node packageNode) {

+    Package(RepoSource source, Node packageNode, Map<String,String> licenses) {

         mSource = source;

-        mRevision    = getXmlInt   (packageNode, SdkRepository.NODE_REVISION, 0);

-        mDescription = getXmlString(packageNode, SdkRepository.NODE_DESCRIPTION);

-        mDescUrl     = getXmlString(packageNode, SdkRepository.NODE_DESC_URL);

-        mLicense     = getXmlString(packageNode, SdkRepository.NODE_LICENSE);

-        mArchives = parseArchives(getFirstChild(packageNode, SdkRepository.NODE_ARCHIVES));

+        mRevision    = XmlParserUtils.getXmlInt   (packageNode, SdkRepository.NODE_REVISION, 0);

+        mDescription = XmlParserUtils.getXmlString(packageNode, SdkRepository.NODE_DESCRIPTION);

+        mDescUrl     = XmlParserUtils.getXmlString(packageNode, SdkRepository.NODE_DESC_URL);

+

+        mLicense  = parseLicense(packageNode, licenses);

+        mArchives = parseArchives(XmlParserUtils.getFirstChild(

+                                  packageNode, SdkRepository.NODE_ARCHIVES));

     }

 

     /**

@@ -86,6 +90,24 @@
     }

 

     /**

+     * Parses the uses-licence node of this package, if any, and returns the license

+     * definition if there's one. Returns null if there's no uses-license element or no

+     * license of this name defined.

+     */

+    private String parseLicense(Node packageNode, Map<String, String> licenses) {

+        Node usesLicense = XmlParserUtils.getFirstChild(

+                                            packageNode, SdkRepository.NODE_USES_LICENSE);

+        if (usesLicense != null) {

+            Node ref = usesLicense.getAttributes().getNamedItem(SdkRepository.ATTR_REF);

+            if (ref != null) {

+                String licenseRef = ref.getNodeValue();

+                return licenses.get(licenseRef);

+            }

+        }

+        return null;

+    }

+

+    /**

      * Parses an XML node to process the <archives> element.

      */

     private Archive[] parseArchives(Node archivesNode) {

@@ -113,13 +135,13 @@
     private Archive parseArchive(Node archiveNode) {

         Archive a = new Archive(

                     this,

-                    (Os)   getEnumAttribute(archiveNode, SdkRepository.ATTR_OS,

+                    (Os)   XmlParserUtils.getEnumAttribute(archiveNode, SdkRepository.ATTR_OS,

                             Os.values(), null),

-                    (Arch) getEnumAttribute(archiveNode, SdkRepository.ATTR_ARCH,

+                    (Arch) XmlParserUtils.getEnumAttribute(archiveNode, SdkRepository.ATTR_ARCH,

                             Arch.values(), Arch.ANY),

-                    getXmlString(archiveNode, SdkRepository.NODE_URL),

-                    getXmlLong(archiveNode, SdkRepository.NODE_SIZE, 0),

-                    getXmlString(archiveNode, SdkRepository.NODE_CHECKSUM)

+                    XmlParserUtils.getXmlString(archiveNode, SdkRepository.NODE_URL),

+                    XmlParserUtils.getXmlLong  (archiveNode, SdkRepository.NODE_SIZE, 0),

+                    XmlParserUtils.getXmlString(archiveNode, SdkRepository.NODE_CHECKSUM)

                 );

 

         return a;

@@ -198,9 +220,13 @@
      * existing or new folder depending on the current content of the SDK.

      *

      * @param osSdkRoot The OS path of the SDK root folder.

+     * @param suggestedDir A suggestion for the installation folder name, based on the root

+     *                     folder used in the zip archive.

+     * @param sdkManager An existing SDK manager to list current platforms and addons.

      * @return A new {@link File} corresponding to the directory to use to install this package.

      */

-    public abstract File getInstallFolder(String osSdkRoot);

+    public abstract File getInstallFolder(

+            String osSdkRoot, String suggestedDir, SdkManager sdkManager);

 

     /**

      * Computes whether the given package is a suitable update for the current package.

@@ -219,85 +245,4 @@
             replacementPackage.getRevision() > this.getRevision();

     }

 

-    //---

-

-    /**

-     * Returns the first child element with the given XML local name.

-     * If xmlLocalName is null, returns the very first child element.

-     */

-    protected static Node getFirstChild(Node node, String xmlLocalName) {

-

-        for(Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {

-            if (child.getNodeType() == Node.ELEMENT_NODE &&

-                    SdkRepository.NS_SDK_REPOSITORY.equals(child.getNamespaceURI())) {

-                if (xmlLocalName == null || xmlLocalName.equals(child.getLocalName())) {

-                    return child;

-                }

-            }

-        }

-

-        return null;

-    }

-

-    /**

-     * Retrieves the value of that XML element as a string.

-     * Returns an empty string when the element is missing.

-     */

-    protected static String getXmlString(Node node, String xmlLocalName) {

-        Node child = getFirstChild(node, xmlLocalName);

-

-        return child == null ? "" : child.getTextContent();  //$NON-NLS-1$

-    }

-

-    /**

-     * Retrieves the value of that XML element as an integer.

-     * Returns the default value when the element is missing or is not an integer.

-     */

-    protected static int getXmlInt(Node node, String xmlLocalName, int defaultValue) {

-        String s = getXmlString(node, xmlLocalName);

-        try {

-            return Integer.parseInt(s);

-        } catch (NumberFormatException e) {

-            return defaultValue;

-        }

-    }

-

-    /**

-     * Retrieves the value of that XML element as a long.

-     * Returns the default value when the element is missing or is not an integer.

-     */

-    protected static long getXmlLong(Node node, String xmlLocalName, long defaultValue) {

-        String s = getXmlString(node, xmlLocalName);

-        try {

-            return Long.parseLong(s);

-        } catch (NumberFormatException e) {

-            return defaultValue;

-        }

-    }

-

-    /**

-     * Retrieve an attribute which value must match one of the given enums using a

-     * case-insensitive name match.

-     *

-     * Returns defaultValue if the attribute does not exist or its value does not match

-     * the given enum values.

-     */

-    private Object getEnumAttribute(

-            Node archiveNode,

-            String attrName,

-            Object[] values,

-            Object defaultValue) {

-

-        Node attr = archiveNode.getAttributes().getNamedItem(attrName);

-        if (attr != null) {

-            String found = attr.getNodeValue();

-            for (Object value : values) {

-                if (value.toString().equalsIgnoreCase(found)) {

-                    return value;

-                }

-            }

-        }

-

-        return defaultValue;

-    }

 }

diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/PlatformPackage.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/PlatformPackage.java
index b4b8cf3..85dbbd0 100755
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/PlatformPackage.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/PlatformPackage.java
@@ -26,6 +26,7 @@
 import org.w3c.dom.Node;

 

 import java.io.File;

+import java.util.Map;

 

 /**

  * Represents a platform XML node in an SDK repository.

@@ -40,10 +41,10 @@
      * <p/>

      * This constructor should throw an exception if the package cannot be created.

      */

-    PlatformPackage(RepoSource source, Node packageNode) {

-        super(source, packageNode);

-        mVersion  = getXmlString(packageNode, SdkRepository.NODE_VERSION);

-        mApiLevel = getXmlInt   (packageNode, SdkRepository.NODE_API_LEVEL, 0);

+    PlatformPackage(RepoSource source, Node packageNode, Map<String,String> licenses) {

+        super(source, packageNode, licenses);

+        mVersion  = XmlParserUtils.getXmlString(packageNode, SdkRepository.NODE_VERSION);

+        mApiLevel = XmlParserUtils.getXmlInt   (packageNode, SdkRepository.NODE_API_LEVEL, 0);

     }

 

     /**

@@ -102,13 +103,26 @@
      * has this platform version installed, we'll use that one.

      *

      * @param osSdkRoot The OS path of the SDK root folder.

+     * @param suggestedDir A suggestion for the installation folder name, based on the root

+     *                     folder used in the zip archive.

+     * @param sdkManager An existing SDK manager to list current platforms and addons.

      * @return A new {@link File} corresponding to the directory to use to install this package.

      */

     @Override

-    public File getInstallFolder(String osSdkRoot) {

+    public File getInstallFolder(String osSdkRoot, String suggestedDir, SdkManager sdkManager) {

+

+        // First find if this add-on is already installed. If so, reuse the same directory.

+        for (IAndroidTarget target : sdkManager.getTargets()) {

+            if (target.isPlatform() &&

+                    target.getApiVersionNumber() == getApiLevel() &&

+                    target.getApiVersionName().equals(getVersion())) {

+                return new File(target.getLocation());

+            }

+        }

+

         File platforms = new File(osSdkRoot, SdkConstants.FD_PLATFORMS);

         File folder = new File(platforms, String.format("android-%s", getVersion())); //$NON-NLS-1$

-        // TODO find similar existing platform in platforms folder

+

         return folder;

     }

 

diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/RepoSource.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/RepoSource.java
index b1993ee..498cfab 100755
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/RepoSource.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/RepoSource.java
@@ -28,6 +28,7 @@
 import java.io.InputStream;

 import java.net.URL;

 import java.util.ArrayList;

+import java.util.HashMap;

 

 import javax.xml.XMLConstants;

 import javax.xml.parsers.DocumentBuilder;

@@ -46,7 +47,7 @@
 public class RepoSource implements IDescription {

 

     private final String mUrl;

-    private final boolean mAddonOnly;

+    private final boolean mUserSource;

 

     private Package[] mPackages;

     private String mDescription;

@@ -54,12 +55,18 @@
     /**

      * Constructs a new source for the given repository URL.

      */

-    public RepoSource(String url, boolean addonOnly) {

+    public RepoSource(String url, boolean userSource) {

         mUrl = url;

-        mAddonOnly = addonOnly;

+        mUserSource = userSource;

         setDefaultDescription();

     }

 

+    /** Returns true if this is a user source. We only load addon and extra packages

+     * from a user source and ignore the rest. */

+    public boolean isUserSource() {

+        return mUserSource;

+    }

+

     /** Returns the URL of the repository.xml file for this source. */

     public String getUrl() {

         return mUrl;

@@ -137,7 +144,7 @@
     }

 

     private void setDefaultDescription() {

-        if (mAddonOnly) {

+        if (mUserSource) {

             mDescription = String.format("Add-on Source: %1$s", mUrl);

         } else {

             mDescription = String.format("SDK Source: %1$s", mUrl);

@@ -248,6 +255,22 @@
 

                 ArrayList<Package> packages = new ArrayList<Package>();

 

+                // Parse license definitions

+                HashMap<String, String> licenses = new HashMap<String, String>();

+                for (Node child = root.getFirstChild();

+                     child != null;

+                     child = child.getNextSibling()) {

+                    if (child.getNodeType() == Node.ELEMENT_NODE &&

+                            SdkRepository.NS_SDK_REPOSITORY.equals(child.getNamespaceURI()) &&

+                            child.getLocalName().equals(SdkRepository.NODE_LICENSE)) {

+                        Node id = child.getAttributes().getNamedItem(SdkRepository.ATTR_ID);

+                        if (id != null) {

+                            licenses.put(id.getNodeValue(), child.getTextContent());

+                        }

+                    }

+                }

+

+                // Parse packages

                 for (Node child = root.getFirstChild();

                      child != null;

                      child = child.getNextSibling()) {

@@ -257,16 +280,23 @@
                         Package p = null;

 

                         try {

+                            // We can load addon and extra packages from all sources, either

+                            // internal or user sources.

                             if (SdkRepository.NODE_ADD_ON.equals(name)) {

-                                p = new AddonPackage(this, child);

+                                p = new AddonPackage(this, child, licenses);

 

-                            } else if (!mAddonOnly) {

+                            } else if (SdkRepository.NODE_EXTRA.equals(name)) {

+                                p = new ExtraPackage(this, child, licenses);

+

+                            } else if (!mUserSource) {

+                                // We only load platform, doc and tool packages from internal

+                                // sources, never from user sources.

                                 if (SdkRepository.NODE_PLATFORM.equals(name)) {

-                                    p = new PlatformPackage(this, child);

+                                    p = new PlatformPackage(this, child, licenses);

                                 } else if (SdkRepository.NODE_DOC.equals(name)) {

-                                    p = new DocPackage(this, child);

+                                    p = new DocPackage(this, child, licenses);

                                 } else if (SdkRepository.NODE_TOOL.equals(name)) {

-                                    p = new ToolPackage(this, child);

+                                    p = new ToolPackage(this, child, licenses);

                                 }

                             }

 

diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/RepoSources.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/RepoSources.java
index 42462d9..a15014d 100755
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/RepoSources.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/RepoSources.java
@@ -16,13 +16,29 @@
 

 package com.android.sdklib.internal.repository;

 

+import com.android.prefs.AndroidLocation;

+import com.android.prefs.AndroidLocation.AndroidLocationException;

+import com.android.sdklib.ISdkLog;

+

+import java.io.File;

+import java.io.FileInputStream;

+import java.io.FileOutputStream;

+import java.io.IOException;

 import java.util.ArrayList;

+import java.util.Iterator;

+import java.util.Properties;

 

 /**

  * A list of sdk-repository sources.

  */

 public class RepoSources {

 

+    private static final String KEY_COUNT = "count";

+

+    private static final String KEY_SRC = "src";

+

+    private static final String SRC_FILENAME = "repositories.cfg"; //$NON-NLS-1$

+

     private ArrayList<RepoSource> mSources = new ArrayList<RepoSource>();

 

     public RepoSources() {

@@ -36,9 +52,113 @@
     }

 

     /**

+     * Removes a source from the Sources list.

+     */

+    public void remove(RepoSource source) {

+        mSources.remove(source);

+    }

+

+    /**

      * Returns the sources list array. This is never null.

      */

-    public ArrayList<RepoSource> getSources() {

-        return mSources;

+    public RepoSource[] getSources() {

+        return mSources.toArray(new RepoSource[mSources.size()]);

+    }

+

+    /**

+     * Loads all user sources. This <em>replaces</em> all existing user sources

+     * by the ones from the property file.

+     */

+    public void loadUserSources(ISdkLog log) {

+

+        // Remove all existing user sources

+        for (Iterator<RepoSource> it = mSources.iterator(); it.hasNext(); ) {

+            RepoSource s = it.next();

+            if (s.isUserSource()) {

+                it.remove();

+            }

+        }

+

+        // Load new user sources from property file

+        FileInputStream fis = null;

+        try {

+            String folder = AndroidLocation.getFolder();

+            File f = new File(folder, SRC_FILENAME);

+            if (f.exists()) {

+                fis = new FileInputStream(f);

+

+                Properties props = new Properties();

+                props.load(fis);

+

+                int count = Integer.parseInt(props.getProperty(KEY_COUNT, "0"));

+

+                for (int i = 0; i < count; i++) {

+                    String url = props.getProperty(String.format("%s%02d", KEY_SRC, count));  //$NON-NLS-1$

+                    if (url != null) {

+                        mSources.add(new RepoSource(url, true /*userSource*/));

+                    }

+                }

+            }

+

+        } catch (NumberFormatException e) {

+            log.error(e, null);

+

+        } catch (AndroidLocationException e) {

+            log.error(e, null);

+

+        } catch (IOException e) {

+            log.error(e, null);

+

+        } finally {

+            if (fis != null) {

+                try {

+                    fis.close();

+                } catch (IOException e) {

+                }

+            }

+        }

+

+    }

+

+    /**

+     * Saves all the user sources.

+     * @param log

+     */

+    public void saveUserSources(ISdkLog log) {

+        FileOutputStream fos = null;

+        try {

+            String folder = AndroidLocation.getFolder();

+            File f = new File(folder, SRC_FILENAME);

+

+            fos = new FileOutputStream(f);

+

+            Properties props = new Properties();

+

+            int count = 0;

+            for (RepoSource s : mSources) {

+                if (s.isUserSource()) {

+                    count++;

+                    props.setProperty(String.format("%s%02d", KEY_SRC, count), s.getUrl());  //$NON-NLS-1$

+                }

+            }

+            props.setProperty(KEY_COUNT, Integer.toString(count));

+

+            props.store( fos, "## User Sources for Android tool");  //$NON-NLS-1$

+

+        } catch (AndroidLocationException e) {

+            log.error(e, null);

+

+        } catch (IOException e) {

+            log.error(e, null);

+

+        } finally {

+            if (fos != null) {

+                try {

+                    fos.close();

+                } catch (IOException e) {

+                }

+            }

+        }

+

     }

 }

diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ToolPackage.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ToolPackage.java
index b355b93..1076049 100755
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ToolPackage.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ToolPackage.java
@@ -17,12 +17,14 @@
 package com.android.sdklib.internal.repository;

 

 import com.android.sdklib.SdkConstants;

+import com.android.sdklib.SdkManager;

 import com.android.sdklib.internal.repository.Archive.Arch;

 import com.android.sdklib.internal.repository.Archive.Os;

 

 import org.w3c.dom.Node;

 

 import java.io.File;

+import java.util.Map;

 

 /**

  * Represents a tool XML node in an SDK repository.

@@ -34,8 +36,8 @@
      * <p/>

      * This constructor should throw an exception if the package cannot be created.

      */

-    ToolPackage(RepoSource source, Node packageNode) {

-        super(source, packageNode);

+    ToolPackage(RepoSource source, Node packageNode, Map<String,String> licenses) {

+        super(source, packageNode, licenses);

     }

 

     /**

@@ -82,10 +84,13 @@
      * A "tool" package should always be located in SDK/tools.

      *

      * @param osSdkRoot The OS path of the SDK root folder.

+     * @param suggestedDir A suggestion for the installation folder name, based on the root

+     *                     folder used in the zip archive.

+     * @param sdkManager An existing SDK manager to list current platforms and addons.

      * @return A new {@link File} corresponding to the directory to use to install this package.

      */

     @Override

-    public File getInstallFolder(String osSdkRoot) {

+    public File getInstallFolder(String osSdkRoot, String suggestedDir, SdkManager sdkManager) {

         return new File(osSdkRoot, SdkConstants.FD_TOOLS);

     }

 

diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/XmlParserUtils.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/XmlParserUtils.java
new file mode 100755
index 0000000..7a8bc7d
--- /dev/null
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/XmlParserUtils.java
@@ -0,0 +1,108 @@
+/*

+ * Copyright (C) 2009 The Android Open Source Project

+ *

+ * Licensed 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.

+ */

+

+package com.android.sdklib.internal.repository;

+

+import com.android.sdklib.repository.SdkRepository;

+

+import org.w3c.dom.Node;

+

+/**

+ * Misc utilities to help extracting elements and attributes out of an XML document.

+ */

+class XmlParserUtils {

+

+    /**

+     * Returns the first child element with the given XML local name.

+     * If xmlLocalName is null, returns the very first child element.

+     */

+    public static Node getFirstChild(Node node, String xmlLocalName) {

+

+        for(Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {

+            if (child.getNodeType() == Node.ELEMENT_NODE &&

+                    SdkRepository.NS_SDK_REPOSITORY.equals(child.getNamespaceURI())) {

+                if (xmlLocalName == null || xmlLocalName.equals(child.getLocalName())) {

+                    return child;

+                }

+            }

+        }

+

+        return null;

+    }

+

+    /**

+     * Retrieves the value of that XML element as a string.

+     * Returns an empty string when the element is missing.

+     */

+    public static String getXmlString(Node node, String xmlLocalName) {

+        Node child = getFirstChild(node, xmlLocalName);

+

+        return child == null ? "" : child.getTextContent();  //$NON-NLS-1$

+    }

+

+    /**

+     * Retrieves the value of that XML element as an integer.

+     * Returns the default value when the element is missing or is not an integer.

+     */

+    public static int getXmlInt(Node node, String xmlLocalName, int defaultValue) {

+        String s = getXmlString(node, xmlLocalName);

+        try {

+            return Integer.parseInt(s);

+        } catch (NumberFormatException e) {

+            return defaultValue;

+        }

+    }

+

+    /**

+     * Retrieves the value of that XML element as a long.

+     * Returns the default value when the element is missing or is not an integer.

+     */

+    public static long getXmlLong(Node node, String xmlLocalName, long defaultValue) {

+        String s = getXmlString(node, xmlLocalName);

+        try {

+            return Long.parseLong(s);

+        } catch (NumberFormatException e) {

+            return defaultValue;

+        }

+    }

+

+    /**

+     * Retrieve an attribute which value must match one of the given enums using a

+     * case-insensitive name match.

+     *

+     * Returns defaultValue if the attribute does not exist or its value does not match

+     * the given enum values.

+     */

+    public static Object getEnumAttribute(

+            Node archiveNode,

+            String attrName,

+            Object[] values,

+            Object defaultValue) {

+

+        Node attr = archiveNode.getAttributes().getNamedItem(attrName);

+        if (attr != null) {

+            String found = attr.getNodeValue();

+            for (Object value : values) {

+                if (value.toString().equalsIgnoreCase(found)) {

+                    return value;

+                }

+            }

+        }

+

+        return defaultValue;

+    }

+

+}

diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/repository/SdkRepository.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/repository/SdkRepository.java
index cd6d45b..a317640 100755
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/repository/SdkRepository.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/repository/SdkRepository.java
@@ -43,11 +43,15 @@
     public static final String NODE_TOOL     = "tool";                          //$NON-NLS-1$

     /** A doc package. */

     public static final String NODE_DOC      = "doc";                           //$NON-NLS-1$

+    /** An extra package. */

+    public static final String NODE_EXTRA    = "extra";                         //$NON-NLS-1$

 

+    /** The license definition. */

+    public static final String NODE_LICENSE = "license";                        //$NON-NLS-1$

+    /** The optional uses-license for all packages (platform, add-on, tool, doc) or for a lib. */

+    public static final String NODE_USES_LICENSE = "uses-license";              //$NON-NLS-1$

     /** The revision, an int > 0, for all packages (platform, add-on, tool, doc). */

     public static final String NODE_REVISION    = "revision";                   //$NON-NLS-1$

-    /** The optional license for all packages (platform, add-on, tool, doc) or for a lib. */

-    public static final String NODE_LICENSE = "license";                        //$NON-NLS-1$

     /** The optional description for all packages (platform, add-on, tool, doc) or for a lib. */

     public static final String NODE_DESCRIPTION = "description";                //$NON-NLS-1$

     /** The optional description URL for all packages (platform, add-on, tool, doc). */

@@ -67,6 +71,9 @@
     /** A lib element in a libs container. */

     public static final String NODE_LIB       = "lib";                          //$NON-NLS-1$

 

+    /** The path, a string, for extra packages. */

+    public static final String NODE_PATH = "path";                              //$NON-NLS-1$

+

     /** The archives container, for all packages. */

     public static final String NODE_ARCHIVES = "archives";                      //$NON-NLS-1$

     /** An archive element, for the archives container. */

@@ -86,6 +93,12 @@
     /** An optional archive Architecture attribute. */

     public static final String ATTR_ARCH = "arch";                              //$NON-NLS-1$

 

+    /** A license definition ID. */

+    public static final String ATTR_ID = "id";                                  //$NON-NLS-1$

+    /** A license reference. */

+    public static final String ATTR_REF = "ref";                                //$NON-NLS-1$

+

+

     public static InputStream getXsdStream() {

         return SdkRepository.class.getResourceAsStream("sdk-repository.xsd");   //$NON-NLS-1$

     }

diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/repository/sdk-repository.xsd b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/repository/sdk-repository.xsd
index 1862ae8..c55e7d5 100755
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/repository/sdk-repository.xsd
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/repository/sdk-repository.xsd
@@ -40,7 +40,7 @@
         <xsd:complexType>
             <xsd:choice minOccurs="0" maxOccurs="unbounded">
 
-                <!-- The definition of an SDK platform package -->
+                <!-- The definition of an SDK platform package. -->
 
                 <xsd:element name="platform">
                     <xsd:annotation>
@@ -55,21 +55,22 @@
 
                             <!-- The revision, an int > 0, incremented each time a new
                                  package is generated. -->
-                            <xsd:element name="revision"    type="xsd:positiveInteger" />
+                            <xsd:element name="revision"     type="xsd:positiveInteger" />
                             <!-- The optional license of this package. If present, users will have
                                  to agree to it before downloading. -->
-                            <xsd:element name="license" type="xsd:string" minOccurs="0" />
+                            <xsd:element name="uses-license" type="sdk:licenseType" minOccurs="0" />
                             <!-- The optional description of this package. -->
-                            <xsd:element name="description" type="xsd:string" minOccurs="0" />
+                            <xsd:element name="description"  type="xsd:string"      minOccurs="0" />
                             <!-- The optional description URL of this package -->
-                            <xsd:element name="desc-url"    type="xsd:token"  minOccurs="0" />
+                            <xsd:element name="desc-url"     type="xsd:token"       minOccurs="0" />
                             <!-- A list of file archives for this package. -->
-                            <xsd:element name="archives"    type="sdk:archivesType" />
+                            <xsd:element name="archives"     type="sdk:archivesType" />
                         </xsd:all>
                     </xsd:complexType>
                 </xsd:element>
 
-                <!-- The definition of an SDK Add-on package -->
+
+                <!-- The definition of an SDK Add-on package. -->
 
                 <xsd:element name="add-on">
                     <xsd:annotation>
@@ -86,16 +87,16 @@
 
                             <!-- The revision, an int > 0, incremented each time a new
                                  package is generated. -->
-                            <xsd:element name="revision"    type="xsd:positiveInteger" />
+                            <xsd:element name="revision"     type="xsd:positiveInteger" />
                             <!-- The optional license of this package. If present, users will have
                                  to agree to it before downloading. -->
-                            <xsd:element name="license" type="xsd:string" minOccurs="0" />
+                            <xsd:element name="uses-license" type="sdk:licenseType" minOccurs="0" />
                             <!-- The optional description of this package. -->
-                            <xsd:element name="description" type="xsd:string" minOccurs="0" />
+                            <xsd:element name="description"  type="xsd:string"      minOccurs="0" />
                             <!-- The optional description URL of this package -->
-                            <xsd:element name="desc-url"    type="xsd:token"  minOccurs="0" />
+                            <xsd:element name="desc-url"     type="xsd:token"       minOccurs="0" />
                             <!-- A list of file archives for this package. -->
-                            <xsd:element name="archives"    type="sdk:archivesType" />
+                            <xsd:element name="archives"     type="sdk:archivesType" />
 
                             <!-- An add-on can declare 0 or more libraries. -->
 
@@ -119,7 +120,8 @@
                     </xsd:complexType>
                 </xsd:element>
 
-                <!-- The definition of an SDK tool package -->
+
+                <!-- The definition of an SDK tool package. -->
 
                 <xsd:element name="tool">
                     <xsd:annotation>
@@ -129,21 +131,22 @@
                         <xsd:all>
                             <!-- The revision, an int > 0, incremented each time a new
                                  package is generated. -->
-                            <xsd:element name="revision"    type="xsd:positiveInteger" />
+                            <xsd:element name="revision"     type="xsd:positiveInteger" />
                             <!-- The optional license of this package. If present, users will have
                                  to agree to it before downloading. -->
-                            <xsd:element name="license" type="xsd:string" minOccurs="0" />
+                            <xsd:element name="uses-license" type="sdk:licenseType" minOccurs="0" />
                             <!-- The optional description of this package. -->
-                            <xsd:element name="description" type="xsd:string" minOccurs="0" />
+                            <xsd:element name="description"  type="xsd:string"      minOccurs="0" />
                             <!-- The optional description URL of this package -->
-                            <xsd:element name="desc-url"    type="xsd:token"  minOccurs="0" />
+                            <xsd:element name="desc-url"     type="xsd:token"       minOccurs="0" />
                             <!-- A list of file archives for this package. -->
-                            <xsd:element name="archives"    type="sdk:archivesType" />
+                            <xsd:element name="archives"     type="sdk:archivesType" />
                         </xsd:all>
                     </xsd:complexType>
                 </xsd:element>
 
-                <!-- The definition of an SDK doc package -->
+
+                <!-- The definition of an SDK doc package. -->
 
                 <xsd:element name="doc">
                     <xsd:annotation>
@@ -156,23 +159,108 @@
 
                             <!-- The revision, an int > 0, incremented each time a new
                                  package is generated. -->
-                            <xsd:element name="revision"    type="xsd:positiveInteger" />
+                            <xsd:element name="revision"     type="xsd:positiveInteger" />
                             <!-- The optional license of this package. If present, users will have
                                  to agree to it before downloading. -->
-                            <xsd:element name="license" type="xsd:string" minOccurs="0" />
+                            <xsd:element name="uses-license" type="sdk:licenseType" minOccurs="0" />
                             <!-- The optional description of this package. -->
-                            <xsd:element name="description" type="xsd:string" minOccurs="0" />
+                            <xsd:element name="description"  type="xsd:string"      minOccurs="0" />
                             <!-- The optional description URL of this package -->
-                            <xsd:element name="desc-url"    type="xsd:token"  minOccurs="0" />
+                            <xsd:element name="desc-url"     type="xsd:token"       minOccurs="0" />
                             <!-- A list of file archives for this package. -->
-                            <xsd:element name="archives"    type="sdk:archivesType" />
+                            <xsd:element name="archives"     type="sdk:archivesType" />
                         </xsd:all>
                     </xsd:complexType>
                 </xsd:element>
+
+
+                <!-- The definition of an SDK extra package. This kind of package is for
+                     "free" content and specifies in which fixed root directory it must be
+                     installed.
+                -->
+
+                <xsd:element name="extra">
+                    <xsd:annotation>
+                        <xsd:documentation>
+                            An SDK extra package. This kind of package is for "free"
+                            content and specifies in which fixed root directory it must be
+                            installed.
+                            The paths "add-ons", "platforms", "tools" and "docs" are
+                            reserved and cannot be used.
+                        </xsd:documentation>
+                    </xsd:annotation>
+                    <xsd:complexType>
+                        <xsd:all>
+                            <!-- The install folder name. It must be a single-segment path.
+                                 The paths "add-ons", "platforms", "tools" and "docs" are
+                                 reserved and cannot be used.
+                            -->
+                            <xsd:element name="path">
+                                <xsd:simpleType>
+                                    <xsd:restriction base="xsd:token">
+                                        <xsd:pattern value="[^/\\]+"/>
+                                    </xsd:restriction>
+                                </xsd:simpleType>
+                            </xsd:element>
+
+                            <!-- The revision, an int > 0, incremented each time a new
+                                 package is generated. -->
+                            <xsd:element name="revision"     type="xsd:positiveInteger" />
+                            <!-- The optional license of this package. If present, users will have
+                                 to agree to it before downloading. -->
+                            <xsd:element name="uses-license" type="sdk:licenseType" minOccurs="0" />
+                            <!-- The optional description of this package. -->
+                            <xsd:element name="description"  type="xsd:string"      minOccurs="0" />
+                            <!-- The optional description URL of this package -->
+                            <xsd:element name="desc-url"     type="xsd:token"       minOccurs="0" />
+                            <!-- A list of file archives for this package. -->
+                            <xsd:element name="archives"     type="sdk:archivesType" />
+                        </xsd:all>
+                    </xsd:complexType>
+                </xsd:element>
+
+
+
+                <!-- The definition of a license to be referenced by the uses-license element. -->
+
+                <xsd:element name="license">
+                    <xsd:annotation>
+                        <xsd:documentation>
+                            A license definition. Such a license must be used later as a reference
+                            using a uses-license element in one of the package elements.
+                        </xsd:documentation>
+                    </xsd:annotation>
+                    <xsd:complexType>
+                        <xsd:simpleContent>
+                            <xsd:extension base="xsd:string">
+                                <xsd:attribute name="id"   type="xsd:ID" />
+                                <xsd:attribute name="type" type="xsd:token" fixed="text" />
+                            </xsd:extension>
+                        </xsd:simpleContent>
+                    </xsd:complexType>
+                </xsd:element>
             </xsd:choice>
         </xsd:complexType>
     </xsd:element>
 
+
+    <!-- Type describing the license used by a package.
+         The license MUST be defined using a license node and referenced
+         using the ref attribute of the license element inside a package.
+     -->
+
+    <xsd:complexType name="licenseType">
+        <xsd:annotation>
+            <xsd:documentation>
+                Describes the license used by a package. The license MUST be defined
+                using a license node and referenced using the ref attribute of the
+                license element inside a package.
+            </xsd:documentation>
+        </xsd:annotation>
+        <xsd:attribute name="ref" type="xsd:IDREF" />
+    </xsd:complexType>
+
+
     <!-- A collection of files that can be downloaded for a given architecture.
          The <archives> node is mandatory in the repository elements and the
          collection must have at least one <archive> declared.
@@ -232,6 +320,7 @@
         </xsd:sequence>
     </xsd:complexType>
 
+
     <!-- The definition of a file checksum -->
 
     <xsd:simpleType name="sha1Number">
@@ -254,4 +343,5 @@
         </xsd:simpleContent>
     </xsd:complexType>
 
+
 </xsd:schema>
diff --git a/tools/sdkmanager/libs/sdklib/tests/com/android/sdklib/repository/TestSdkRepository.java b/tools/sdkmanager/libs/sdklib/tests/com/android/sdklib/repository/TestSdkRepository.java
index 879f3e6..207a5a2 100755
--- a/tools/sdkmanager/libs/sdklib/tests/com/android/sdklib/repository/TestSdkRepository.java
+++ b/tools/sdkmanager/libs/sdklib/tests/com/android/sdklib/repository/TestSdkRepository.java
@@ -16,6 +16,8 @@
 

 package com.android.sdklib.repository;

 

+import com.android.sdklib.SdkConstants;

+

 import org.xml.sax.ErrorHandler;

 import org.xml.sax.SAXException;

 import org.xml.sax.SAXParseException;

@@ -245,4 +247,57 @@
         // If we get here, the validator has not failed as we expected it to.

         fail();

     }

+

+    /** A document an unknown license id. */

+    public void testLicenseIdNotFound() throws Exception {

+        // we define a license named "lic1" and then reference "lic2" instead

+        String document = "<?xml version=\"1.0\"?>" +

+            "<r:sdk-repository xmlns:r=\"http://schemas.android.com/sdk/android/repository/1\" >" +

+            "<r:license id=\"lic1\"> some license </r:license> " +

+            "<r:tool> <r:uses-license ref=\"lic2\" /> <r:revision>1</r:revision> " +

+            "<r:archives> <r:archive os=\"any\"> <r:size>1</r:size> <r:checksum>2822ae37115ebf13412bbef91339ee0d9454525e</r:checksum> " +

+            "<r:url>url</r:url> </r:archive> </r:archives> </r:tool>" +

+            "</r:sdk-repository>";

+

+        Source source = new StreamSource(new StringReader(document));

+

+        // don't capture the validator errors, we want it to fail and catch the exception

+        Validator validator = getValidator(null);

+        try {

+            validator.validate(source);

+        } catch (SAXParseException e) {

+            // We expect a parse error referring to this grammar rule

+            assertRegex("cvc-id.1: There is no ID/IDREF binding for IDREF 'lic2'.*",

+                    e.getMessage());

+            return;

+        }

+        // If we get here, the validator has not failed as we expected it to.

+        fail();

+    }

+

+    /** A document a slash in an extra path. */

+    public void testExtraPathWithSlash() throws Exception {

+        // we define a license named "lic1" and then reference "lic2" instead

+        String document = "<?xml version=\"1.0\"?>" +

+            "<r:sdk-repository xmlns:r=\"http://schemas.android.com/sdk/android/repository/1\" >" +

+            "<r:extra> <r:revision>1</r:revision> <r:path>path/cannot\\contain\\segments</r:path> " +

+            "<r:archives> <r:archive os=\"any\"> <r:size>1</r:size> <r:checksum>2822ae37115ebf13412bbef91339ee0d9454525e</r:checksum> " +

+            "<r:url>url</r:url> </r:archive> </r:archives> </r:extra>" +

+            "</r:sdk-repository>";

+

+        Source source = new StreamSource(new StringReader(document));

+

+        // don't capture the validator errors, we want it to fail and catch the exception

+        Validator validator = getValidator(null);

+        try {

+            validator.validate(source);

+        } catch (SAXParseException e) {

+            // We expect a parse error referring to this grammar rule

+            assertRegex("cvc-pattern-valid: Value 'path/cannot\\\\contain\\\\segments' is not facet-valid with respect to pattern.*",

+                    e.getMessage());

+            return;

+        }

+        // If we get here, the validator has not failed as we expected it to.

+        fail();

+    }

 }

diff --git a/tools/sdkmanager/libs/sdklib/tests/com/android/sdklib/repository/repository_sample.xml b/tools/sdkmanager/libs/sdklib/tests/com/android/sdklib/repository/repository_sample.xml
index a2d0cda..d4eacf9 100755
--- a/tools/sdkmanager/libs/sdklib/tests/com/android/sdklib/repository/repository_sample.xml
+++ b/tools/sdkmanager/libs/sdklib/tests/com/android/sdklib/repository/repository_sample.xml
@@ -18,6 +18,17 @@
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

     xmlns:sdk="http://schemas.android.com/sdk/android/repository/1">

 

+    <!-- Define a couple of licenses. These will be referenced by uses-license later. -->

+

+    <sdk:license type="text" id="license1">

+        This is the license

+        for this platform.

+    </sdk:license>

+

+    <sdk:license id="license2">

+        Licenses are only of type 'text' right now, so this is implied.

+    </sdk:license>

+

     <!-- Inner elements must be either platform, add-on, doc or tool.

          There can be 0 or more of each, in any order. -->

 

@@ -25,9 +36,8 @@
         <sdk:version>1.0</sdk:version>

         <sdk:api-level>1</sdk:api-level>

         <sdk:revision>3</sdk:revision>

+        <sdk:uses-license ref="license1" />

         <sdk:description>Some optional description</sdk:description>

-        <sdk:license>This is the license

-        for this platform.</sdk:license>

         <sdk:desc-url>http://www.example.com/platform1.html</sdk:desc-url>

         <!-- The archives node is mandatory and it cannot be empty. -->

         <sdk:archives>

@@ -42,8 +52,8 @@
     <sdk:doc>

         <sdk:api-level>1</sdk:api-level>

         <sdk:revision>1</sdk:revision>

-        <sdk:description>Some optional description</sdk:description>

         <!-- the license element is not mandatory. -->

+        <sdk:description>Some optional description</sdk:description>

         <sdk:desc-url>http://www.example.com/docs.html</sdk:desc-url>

         <sdk:archives>

             <sdk:archive os="any">

@@ -59,8 +69,7 @@
         <sdk:api-level>1</sdk:api-level>

         <sdk:vendor>John Doe</sdk:vendor>

         <sdk:revision>1</sdk:revision>

-        <!-- license can be empty. -->

-        <sdk:license></sdk:license>

+        <sdk:uses-license ref="license2" />

         <sdk:description>Some optional description</sdk:description>

         <sdk:desc-url>http://www.example.com/myfirstaddon</sdk:desc-url>

         <sdk:archives>

@@ -87,7 +96,7 @@
         <sdk:version>1.1</sdk:version>

         <sdk:api-level>2</sdk:api-level>

         <sdk:revision>12</sdk:revision>

-        <sdk:license>This is the license for this package.</sdk:license>

+        <sdk:uses-license ref="license1" />

         <!-- sdk:description and sdk:desc-url are optional -->

         <sdk:archives>

             <sdk:archive os="windows">

@@ -145,14 +154,14 @@
                 <sdk:name>com.android.mymaps</sdk:name>

             </sdk:lib>

         </sdk:libs>

-        <sdk:license>This is the license for this package.</sdk:license>

+        <sdk:uses-license ref="license2" />

     </sdk:add-on>

 

     <sdk:tool>

         <sdk:revision>1</sdk:revision>

         <sdk:description>Some optional description</sdk:description>

         <sdk:desc-url>http://www.example.com/tools.html</sdk:desc-url>

-        <sdk:license>This is the license for this package.</sdk:license>

+        <sdk:uses-license ref="license1" />

         <sdk:archives>

             <sdk:archive os="any">

                 <sdk:size>65536</sdk:size>

@@ -165,7 +174,7 @@
     <sdk:doc>

         <sdk:api-level>2</sdk:api-level>

         <sdk:revision>42</sdk:revision>

-        <sdk:license>This is the license for this package.</sdk:license>

+        <sdk:uses-license ref="license2" />

         <sdk:archives>

             <sdk:archive os="windows">

                 <sdk:size>65536</sdk:size>

@@ -187,7 +196,7 @@
 

     <sdk:tool>

         <sdk:revision>42</sdk:revision>

-        <sdk:license>This is the license for this package.</sdk:license>

+        <sdk:uses-license ref="license1" />

         <sdk:archives>

             <sdk:archive os="windows">

                 <sdk:size>65536</sdk:size>

@@ -208,7 +217,7 @@
     </sdk:tool>

 

     <sdk:add-on>

-        <sdk:license>This is the license for this package.</sdk:license>

+        <sdk:uses-license ref="license2" />

         <sdk:name>This add-on has no libraries</sdk:name>

         <sdk:api-level>4</sdk:api-level>

         <sdk:vendor>Joe Bar</sdk:vendor>

@@ -224,4 +233,19 @@
         <sdk:libs />

     </sdk:add-on>

 

+    <sdk:extra>

+        <sdk:path>usb_driver</sdk:path>

+        <sdk:uses-license ref="license2" />

+        <sdk:revision>43</sdk:revision>

+        <sdk:archives>

+            <sdk:archive os="any" arch="any">

+                <sdk:size>65536</sdk:size>

+                <sdk:checksum type="sha1">2822ae37115ebf13412bbef91339ee0d9454525e</sdk:checksum>

+                <sdk:url>distrib/extraduff.zip</sdk:url>

+            </sdk:archive>

+        </sdk:archives>

+        <sdk:description>An Extra package for the USB driver, it will install in $SDK/usb_driver</sdk:description>

+        <sdk:desc-url>http://www.example.com/extra.html</sdk:desc-url>

+    </sdk:extra>

+

 </sdk:sdk-repository>

diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/LocalPackagesPage.java b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/LocalPackagesPage.java
index f87f30a..115bf32 100755
--- a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/LocalPackagesPage.java
+++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/LocalPackagesPage.java
@@ -200,6 +200,7 @@
     private void postCreate() {

         mUpdaterData.addListeners(this);

         adjustColumnsWidth();

+        updateButtonsState();

     }

 

     /**

@@ -222,12 +223,26 @@
     }

 

     /**

+     * Enable or disable buttons depending on list content and selection

+     */

+    private void updateButtonsState() {

+        ISelection sel = mTableViewerPackages.getSelection();

+        boolean hasSelection = sel != null && !sel.isEmpty();

+

+        mUpdateButton.setEnabled(mTablePackages.getItemCount() > 0);

+        mDeleteButton.setEnabled(hasSelection);

+        mRefreshButton.setEnabled(true);

+    }

+

+    /**

      * Called when an item in the package table viewer is selected.

      * If the items is an {@link IDescription} (as it should), this will display its long

      * description in the description area. Otherwise when the item is not of the expected

      * type or there is no selection, it empties the description area.

      */

     private void onTreeSelected() {

+        updateButtonsState();

+

         ISelection sel = mTableViewerPackages.getSelection();

         if (sel instanceof IStructuredSelection) {

             Object elem = ((IStructuredSelection) sel).getFirstElement();

@@ -288,6 +303,7 @@
 

     private void onRefreshSelected() {

         mUpdaterData.reloadSdk();

+        updateButtonsState();

     }

 

     public void onSdkChange() {

diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/LocalSdkAdapter.java b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/LocalSdkAdapter.java
index 05e0b06..a92129f 100755
--- a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/LocalSdkAdapter.java
+++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/LocalSdkAdapter.java
@@ -101,7 +101,8 @@
 

                 if (packages == null) {

                     // load on demand the first time

-                    packages = parser.parseSdk(mUpdaterData.getOsSdkRoot());

+                    packages = parser.parseSdk(mUpdaterData.getOsSdkRoot(),

+                            mUpdaterData.getSdkManager());

                 }

 

                 if (packages != null) {

diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/RemotePackagesPage.java b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/RemotePackagesPage.java
index e187de6..2b96b31 100755
--- a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/RemotePackagesPage.java
+++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/RemotePackagesPage.java
@@ -23,14 +23,16 @@
 import com.android.sdklib.internal.repository.RepoSource;

 import com.android.sdkuilib.internal.repository.UpdaterData.ISdkListener;

 

+import org.eclipse.jface.dialogs.IInputValidator;

+import org.eclipse.jface.dialogs.InputDialog;

+import org.eclipse.jface.dialogs.MessageDialog;

 import org.eclipse.jface.viewers.CheckStateChangedEvent;

 import org.eclipse.jface.viewers.CheckboxTreeViewer;

-import org.eclipse.jface.viewers.DoubleClickEvent;

 import org.eclipse.jface.viewers.ICheckStateListener;

-import org.eclipse.jface.viewers.IDoubleClickListener;

 import org.eclipse.jface.viewers.ISelection;

 import org.eclipse.jface.viewers.ITreeContentProvider;

 import org.eclipse.jface.viewers.ITreeSelection;

+import org.eclipse.jface.window.Window;

 import org.eclipse.swt.SWT;

 import org.eclipse.swt.events.ControlAdapter;

 import org.eclipse.swt.events.ControlEvent;

@@ -48,18 +50,6 @@
 

 import java.util.ArrayList;

 

-/*

- * TODO list

- * - check source => toggle packages: all, none

- * - check package => set source check to tri-state

- * - check callback => install enable if has selection

- * - select tree item: delete site enable if add-on source

- * - select tree item: refresh enable if source

- * - load add-on sites from pref

- * - delete site callback, update pref

- * - refresh callback

- * - install selected callback

- */

 

 public class RemotePackagesPage extends Composite implements ISdkListener {

 

@@ -70,7 +60,7 @@
     private TreeColumn mColumnSource;

     private Group mDescriptionContainer;

     private Button mAddSiteButton;

-    private Button mRemoveSiteButton;

+    private Button mDeleteSiteButton;

     private Label mPlaceholder3;

     private Button mRefreshButton;

     private Button mInstallSelectedButton;

@@ -95,11 +85,6 @@
         parent.setLayout(new GridLayout(5, false));

 

         mTreeViewerSources = new CheckboxTreeViewer(parent, SWT.BORDER);

-        mTreeViewerSources.addDoubleClickListener(new IDoubleClickListener() {

-            public void doubleClick(DoubleClickEvent event) {

-                onTreeDoubleClick(event); //$hide$

-            }

-        });

         mTreeViewerSources.addCheckStateListener(new ICheckStateListener() {

             public void checkStateChanged(CheckStateChangedEvent event) {

                 onTreeCheckStateChanged(event); //$hide$

@@ -137,14 +122,14 @@
         });

         mAddSiteButton.setText("Add Site...");

 

-        mRemoveSiteButton = new Button(parent, SWT.NONE);

-        mRemoveSiteButton.addSelectionListener(new SelectionAdapter() {

+        mDeleteSiteButton = new Button(parent, SWT.NONE);

+        mDeleteSiteButton.addSelectionListener(new SelectionAdapter() {

             @Override

             public void widgetSelected(SelectionEvent e) {

                 onRemoveSiteSelected(); //$hide$

             }

         });

-        mRemoveSiteButton.setText("Delete Site...");

+        mDeleteSiteButton.setText("Delete Site...");

 

         mPlaceholder3 = new Label(parent, SWT.NONE);

         mPlaceholder3.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false, 1, 1));

@@ -189,6 +174,7 @@
     private void postCreate() {

         mUpdaterData.addListeners(this);

         adjustColumnsWidth();

+        updateButtonsState();

     }

 

     /**

@@ -218,6 +204,8 @@
      * type or there is no selection, it empties the description area.

      */

     private void onTreeSelected() {

+        updateButtonsState();

+

         ISelection sel = mTreeViewerSources.getSelection();

         if (sel instanceof ITreeSelection) {

             Object elem = ((ITreeSelection) sel).getFirstElement();

@@ -238,6 +226,8 @@
      * When checking a package, only its compatible archives are checked.

      */

     private void onTreeCheckStateChanged(CheckStateChangedEvent event) {

+        updateButtonsState();

+

         boolean b = event.getChecked();

         Object elem = event.getElement(); // Will be Archive or Package or RepoSource

 

@@ -268,22 +258,12 @@
         mTreeViewerSources.setExpandedState(pkg, true);

         for (Object archive : provider.getChildren(pkg)) {

             if (archive instanceof Archive) {

-                if (((Archive) archive).isCompatible()) {

-                    mTreeViewerSources.setChecked(archive, true);

-                } else {

-                    mTreeViewerSources.setChecked(archive, false);

-                    // TODO change the item image to mark it incompatible

-                }

+                mTreeViewerSources.setChecked(archive, ((Archive) archive).isCompatible());

             }

         }

     }

 

-    private void onTreeDoubleClick(DoubleClickEvent event) {

-        // TODO use or remove

-    }

-

     private void onInstallSelectedArchives() {

-

         ArrayList<Archive> archives = new ArrayList<Archive>();

         for (Object element : mTreeViewerSources.getCheckedElements()) {

             if (element instanceof Archive) {

@@ -297,17 +277,75 @@
     }

 

     private void onAddSiteSelected() {

-        // TODO prompt for new addon site URL, store, refresh

+

+        final RepoSource[] knowSources = mUpdaterData.getSources().getSources();

+        String title = "Add Site URL";

+        String msg = "Please enter the URL of the repository.xml for the new site:";

+

+        InputDialog dlg = new InputDialog(getShell(), title, msg, null, new IInputValidator() {

+            public String isValid(String newText) {

+

+                if (newText == null || newText.length() == 0) {

+                    return "Please enter an URL.";

+                }

+

+                // A URL should have one of the following prefixes

+                if (!newText.startsWith("file://") &&

+                        !newText.startsWith("ftp://") &&

+                        !newText.startsWith("http://") &&

+                        !newText.startsWith("https://")) {

+                    return "The URL must start by one of file://, ftp://, http:// or https://";

+                }

+

+                // Reject URLs that are already in the source list

+                for (RepoSource s : knowSources) {

+                    if (newText.equals(s.getUrl())) {

+                        return "This site is already listed.";

+                    }

+                }

+

+                return null;

+            }

+        });

+

+        if (dlg.open() == Window.OK) {

+            String url = dlg.getValue();

+            mUpdaterData.getSources().add(new RepoSource(url, true /*userSource*/));

+        }

     }

 

     private void onRemoveSiteSelected() {

-        // TODO prompt for removing addon site URL, store, refresh

+        boolean changed = false;

+

+        ISelection sel = mTreeViewerSources.getSelection();

+        if (mUpdaterData != null && sel instanceof ITreeSelection) {

+            for (Object c : ((ITreeSelection) sel).toList()) {

+                if (c instanceof RepoSource && ((RepoSource) c).isUserSource()) {

+                    RepoSource source = (RepoSource) c;

+

+                    String title = "Delete Site?";

+

+                    String msg = String.format("Are you sure you want to delete the site '%1$s'?",

+                            source.getUrl());

+

+                    if (MessageDialog.openQuestion(getShell(), title, msg)) {

+                        mUpdaterData.getSources().remove(source);

+                        changed = true;

+                    }

+                }

+            }

+        }

+

+        if (changed) {

+            onRefreshSelected();

+        }

     }

 

     private void onRefreshSelected() {

         if (mUpdaterData != null) {

             mUpdaterData.refreshSources(false /*forceFetching*/);

         }

+        updateButtonsState();

     }

 

     public void onSdkChange() {

@@ -318,6 +356,39 @@
         onTreeSelected();

     }

 

+    private void updateButtonsState() {

+        // We install archives, so there should be at least one checked archive.

+        // Having sites or packages checked does not count.

+        boolean hasCheckedArchive = false;

+        Object[] checked = mTreeViewerSources.getCheckedElements();

+        if (checked != null) {

+            for (Object c : checked) {

+                if (c instanceof Archive) {

+                    hasCheckedArchive = true;

+                    break;

+                }

+            }

+        }

+

+        // Is there a selected site Source?

+        boolean hasSelectedUserSource = false;

+        ISelection sel = mTreeViewerSources.getSelection();

+        if (sel instanceof ITreeSelection) {

+            for (Object c : ((ITreeSelection) sel).toList()) {

+                if (c instanceof RepoSource &&

+                        ((RepoSource) c).isUserSource()) {

+                    hasSelectedUserSource = true;

+                    break;

+                }

+            }

+        }

+

+        mAddSiteButton.setEnabled(true);

+        mDeleteSiteButton.setEnabled(hasSelectedUserSource);

+        mRefreshButton.setEnabled(true);

+        mInstallSelectedButton.setEnabled(hasCheckedArchive);

+    }

+

     // End of hiding from SWT Designer

     //$hide<<$

 }

diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/RepoSourcesAdapter.java b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/RepoSourcesAdapter.java
index 4932a5b..c22f456 100755
--- a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/RepoSourcesAdapter.java
+++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/RepoSourcesAdapter.java
@@ -113,7 +113,7 @@
          */

         public Object[] getChildren(Object parentElement) {

             if (parentElement == RepoSourcesAdapter.this) {

-                return mUpdaterData.getSources().getSources().toArray();

+                return mUpdaterData.getSources().getSources();

 

             } else if (parentElement instanceof RepoSource) {

                 final RepoSource source = (RepoSource) parentElement;

diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdateChooserDialog.java b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdateChooserDialog.java
index 8b9a417..14c8bbc 100755
--- a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdateChooserDialog.java
+++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdateChooserDialog.java
@@ -245,6 +245,11 @@
         placeholder = new Label(mDialogShell, SWT.NONE);

         placeholder.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, true, false, 1, 1));

 

+        // for MacOS, the Cancel button should be left.

+        if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_DARWIN) {

+            mCancelButton = new Button(mDialogShell, SWT.PUSH);

+        }

+

         mInstallButton = new Button(mDialogShell, SWT.PUSH);

         mInstallButton.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));

         mInstallButton.setText("Install Accepted");

@@ -255,7 +260,10 @@
             }

         });

 

-        mCancelButton = new Button(mDialogShell, SWT.PUSH);

+        // if we haven't created the cancel button yet (macos?), create it now.

+        if (mCancelButton == null) {

+            mCancelButton = new Button(mDialogShell, SWT.PUSH);

+        }

         mCancelButton.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));

         mCancelButton.setText("Cancel");

         mCancelButton.addSelectionListener(new SelectionAdapter() {

diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterData.java b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterData.java
index f8373dd..109000b 100755
--- a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterData.java
+++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterData.java
@@ -219,7 +219,7 @@
             try {

                 mAvdManager.reloadAvds();

             } catch (AndroidLocationException e) {

-                // FIXME

+                mSdkLog.error(e, null);

             }

         }

     }

@@ -236,7 +236,7 @@
                         try {

                             listener.onSdkChange();

                         } catch (Throwable t) {

-                            // TODO: log error

+                            mSdkLog.error(t, null);

                         }

                     }

                 }

@@ -273,7 +273,7 @@
                             break;

                         }

 

-                        if (archive.install(mOsSdkRoot, forceHttp, monitor)) {

+                        if (archive.install(mOsSdkRoot, forceHttp, mSdkManager, monitor)) {

                             numInstalled++;

                         }

 

@@ -356,8 +356,8 @@
 

         mTaskFactory.start("Refresh Sources",new ITask() {

             public void run(ITaskMonitor monitor) {

-                ArrayList<RepoSource> sources = mSources.getSources();

-                monitor.setProgressMax(sources.size());

+                RepoSource[] sources = mSources.getSources();

+                monitor.setProgressMax(sources.length);

                 for (RepoSource source : sources) {

                     if (forceFetching || source.getPackages() != null) {

                         source.load(monitor.createSubMonitor(1), forceHttp);

@@ -406,7 +406,7 @@
 

         } else {

             // Get all the available archives from all loaded sources

-            ArrayList<RepoSource> remoteSources = getSources().getSources();

+            RepoSource[] remoteSources = getSources().getSources();

 

             for (RepoSource remoteSrc : remoteSources) {

                 Package[] remotePkgs = remoteSrc.getPackages();

diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterWindowImpl.java b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterWindowImpl.java
index 10cadac..2605410 100755
--- a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterWindowImpl.java
+++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterWindowImpl.java
@@ -20,6 +20,7 @@
 import com.android.sdklib.ISdkLog;

 import com.android.sdklib.SdkConstants;

 import com.android.sdklib.internal.repository.RepoSource;

+import com.android.sdklib.internal.repository.RepoSources;

 import com.android.sdklib.repository.SdkRepository;

 import com.android.sdkuilib.internal.repository.icons.ImageFactory;

 

@@ -93,6 +94,8 @@
                 display.sleep();

             }

         }

+

+        dispose();  //$hide$

     }

 

     /**

@@ -215,12 +218,18 @@
         displayPage(0);

         mPageList.setSelection(0);

 

-        // TODO read add-on sources from some file

         setupSources();

         initializeSettings();

         mUpdaterData.notifyListeners();

     }

 

+    /**

+     * Called by the main loop when the window has been disposed.

+     */

+    private void dispose() {

+        mUpdaterData.getSources().saveUserSources(mUpdaterData.getSdkLog());

+    }

+

     // --- page switching ---

 

     /**

@@ -304,17 +313,20 @@
      * Used to initialize the sources.

      */

     private void setupSources() {

-        mUpdaterData.getSources().add(

-                new RepoSource(SdkRepository.URL_GOOGLE_SDK_REPO_SITE, false /* addonOnly */));

+        RepoSources sources = mUpdaterData.getSources();

+        sources.add(new RepoSource(SdkRepository.URL_GOOGLE_SDK_REPO_SITE, false /*userSource*/));

 

         String str = System.getenv("TEMP_SDK_URL"); // TODO STOPSHIP temporary remove before shipping

         if (str != null) {

             String[] urls = str.split(";");

             for (String url : urls) {

-                mUpdaterData.getSources().add(new RepoSource(url, false /* addonOnly */));

+                sources.add(new RepoSource(url, false /*userSource*/));

             }

         }

 

+        // Load user sources

+        sources.loadUserSources(mUpdaterData.getSdkLog());

+

         mRemotePackagesPage.onSdkChange();

     }

 

diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/ImageFactory.java b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/ImageFactory.java
index 8cfe53f..d4c74d7 100755
--- a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/ImageFactory.java
+++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/ImageFactory.java
@@ -19,6 +19,7 @@
 import com.android.sdklib.internal.repository.AddonPackage;

 import com.android.sdklib.internal.repository.Archive;

 import com.android.sdklib.internal.repository.DocPackage;

+import com.android.sdklib.internal.repository.ExtraPackage;

 import com.android.sdklib.internal.repository.Package;

 import com.android.sdklib.internal.repository.PlatformPackage;

 import com.android.sdklib.internal.repository.RepoSource;

@@ -103,8 +104,8 @@
         } else if (object instanceof DocPackage) {

             return getImageByName("doc_icon16.png");

 

-        } else if (object instanceof Package) {

-            return getImageByName("extra_pkg_icon16.png");

+        } else if (object instanceof ExtraPackage) {

+            return getImageByName("extra_icon16.png");

 

         } else if (object instanceof Archive) {

             if (((Archive) object).isCompatible()) {

diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/addon_icon16.png b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/addon_icon16.png
index 1a3e0f2..ca6a231 100755
--- a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/addon_icon16.png
+++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/addon_icon16.png
Binary files differ
diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/extra_icon16.png b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/extra_icon16.png
new file mode 100755
index 0000000..a6529f0
--- /dev/null
+++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/extra_icon16.png
Binary files differ
diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/widgets/AvdSelector.java b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/widgets/AvdSelector.java
index 80fdb1e..37dada9 100644
--- a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/widgets/AvdSelector.java
+++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/widgets/AvdSelector.java
@@ -18,11 +18,13 @@
 
 import com.android.prefs.AndroidLocation.AndroidLocationException;
 import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.ISdkLog;
 import com.android.sdklib.internal.avd.AvdManager;
 import com.android.sdklib.internal.avd.AvdManager.AvdInfo;
 import com.android.sdklib.internal.avd.AvdManager.AvdInfo.AvdStatus;
 import com.android.sdkuilib.internal.repository.icons.ImageFactory;
 
+import org.eclipse.jface.dialogs.MessageDialog;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.events.ControlAdapter;
 import org.eclipse.swt.events.ControlEvent;
@@ -37,11 +39,15 @@
 import org.eclipse.swt.layout.GridLayout;
 import org.eclipse.swt.widgets.Button;
 import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
 import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
 import org.eclipse.swt.widgets.Table;
 import org.eclipse.swt.widgets.TableColumn;
 import org.eclipse.swt.widgets.TableItem;
 
+import java.util.ArrayList;
+
 
 /**
  * The AVD selector is a table that is added to the given parent composite.
@@ -195,7 +201,12 @@
             Button deleteButton = new Button(buttons, SWT.PUSH | SWT.FLAT);
             deleteButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
             deleteButton.setText("Delete");
-            // TODO: callback for button
+            deleteButton.addSelectionListener(new SelectionAdapter() {
+                @Override
+                public void widgetSelected(SelectionEvent arg0) {
+                    onDelete();
+                }
+            });
 
             Label l = new Label(buttons, SWT.SEPARATOR | SWT.HORIZONTAL);
             l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
@@ -610,4 +621,108 @@
             item.setText(3, "--");
         }
     }
+
+    private void onDelete() {
+        final AvdInfo avdInfo = getSelected();
+
+        // get the current Display
+        final Display display = mTable.getDisplay();
+
+        // Confirm you want to delete this AVD
+        final boolean[] result = new boolean[1];
+        display.syncExec(new Runnable() {
+            public void run() {
+                Shell shell = display.getActiveShell();
+                result[0] = MessageDialog.openQuestion(shell,
+                        "Delete Android Virtual Device",
+                        String.format(
+                                "Please confirm that you want to delete the Android Virtual Device named '%s'. This operation cannot be reverted.",
+                                avdInfo.getName()));
+            }
+        });
+
+        if (result[0] == false) {
+            return;
+        }
+
+        // log for this action.
+        SdkLog log = new SdkLog(
+                String.format("Result of deleting AVD '%s':", avdInfo.getName()),
+                display);
+
+        // delete the AVD
+        boolean success = mManager.deleteAvd(avdInfo, log);
+
+        // display the result
+        log.displayResult(success);
+
+        if (success) {
+            refresh(false);
+        }
+    }
+
+
+    /**
+     * Collects all log from the AVD action and displays it in a dialog.
+     */
+    private class SdkLog implements ISdkLog {
+
+        final ArrayList<String> logMessages = new ArrayList<String>();
+        private final String mMessage;
+        private final Display mDisplay;
+
+        public SdkLog(String message, Display display) {
+            mMessage = message;
+            mDisplay = display;
+        }
+
+        public void error(Throwable throwable, String errorFormat, Object... arg) {
+            if (errorFormat != null) {
+                logMessages.add(String.format("Error: " + errorFormat, arg));
+            }
+
+            if (throwable != null) {
+                logMessages.add(throwable.getMessage());
+            }
+        }
+
+        public void warning(String warningFormat, Object... arg) {
+            logMessages.add(String.format("Warning: " + warningFormat, arg));
+        }
+
+        public void printf(String msgFormat, Object... arg) {
+            logMessages.add(String.format(msgFormat, arg));
+        }
+
+        /**
+         * Displays the log if anything was captured.
+         */
+        public void displayResult(final boolean success) {
+            if (logMessages.size() > 0) {
+                final StringBuilder sb = new StringBuilder(mMessage + "\n");
+                for (String msg : logMessages) {
+                    sb.append('\n');
+                    sb.append(msg);
+                }
+
+                // display the message
+                // dialog box only run in ui thread..
+                mDisplay.asyncExec(new Runnable() {
+                    public void run() {
+                        Shell shell = mDisplay.getActiveShell();
+                        if (success) {
+                            MessageDialog.openInformation(shell, "Android Virtual Devices Manager",
+                                    sb.toString());
+                        } else {
+                            MessageDialog.openError(shell, "Android Virtual Devices Manager",
+                                    sb.toString());
+
+                        }
+                    }
+                });
+            }
+        }
+    }
+
+
 }