aboutsummaryrefslogtreecommitdiffstats
path: root/sdkmanager/libs/sdklib
diff options
context:
space:
mode:
authorRaphael Moll <ralf@android.com>2012-03-28 16:43:24 -0700
committerRaphael Moll <ralf@android.com>2012-04-03 16:00:01 -0700
commit6462650a787c96cbb371d6aca201af3d613aad24 (patch)
treed5df3571837f38332db68f8f5b44c21a8fba50fe /sdkmanager/libs/sdklib
parentda3dd4690065f3bb547c8827e4efc6e24af4c61d (diff)
downloadsdk-6462650a787c96cbb371d6aca201af3d613aad24.zip
sdk-6462650a787c96cbb371d6aca201af3d613aad24.tar.gz
sdk-6462650a787c96cbb371d6aca201af3d613aad24.tar.bz2
SDK Manager dialog to enable 3rd party addons.
- Change AddonUser dialog: transform current dialog in 2 pages. One is for user to add custom addon site URLs (like the dialog was doing before, unchanged.) - Other tab is to select which official addon sites should be enabled. - Support enable/disable state for each source. - Display disabled sources as such in the tree when in "per-repository" view. - Persist the enable bit state via local pref file. - Refactor a few inner classes out of PackagesPage. Change-Id: Icc8e392d90550e53f1c76dd7aefb31669219973b
Diffstat (limited to 'sdkmanager/libs/sdklib')
-rwxr-xr-xsdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkSource.java131
-rwxr-xr-xsdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkSources.java193
2 files changed, 264 insertions, 60 deletions
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkSource.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkSource.java
index 7933602..9a04226 100755
--- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkSource.java
+++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkSource.java
@@ -19,6 +19,8 @@ package com.android.sdklib.internal.repository;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.annotations.VisibleForTesting.Visibility;
+import com.android.prefs.AndroidLocation;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
import com.android.sdklib.internal.repository.UrlOpener.CanceledByUserException;
import com.android.sdklib.repository.RepoConstants;
import com.android.sdklib.repository.SdkAddonConstants;
@@ -33,7 +35,10 @@ import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
@@ -41,6 +46,7 @@ import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -68,6 +74,9 @@ public abstract class SdkSource implements IDescription, Comparable<SdkSource> {
private String mFetchError;
private final String mUiName;
+ private static final Properties sDisabledSourceUrls = new Properties();
+ private static final String SRC_FILENAME = "sites-settings.cfg"; //$NON-NLS-1$
+
/**
* Constructs a new source for the given repository URL.
* @param url The source URL. Cannot be null. If the URL ends with a /, the default
@@ -183,7 +192,8 @@ public abstract class SdkSource implements IDescription, Comparable<SdkSource> {
/**
* Returns the list of known packages found by the last call to load().
- * This is null when the source hasn't been loaded yet.
+ * This is null when the source hasn't been loaded yet -- caller should
+ * then call {@link #load(ITaskMonitor, boolean)} to load the packages.
*/
public Package[] getPackages() {
return mPackages;
@@ -208,6 +218,110 @@ public abstract class SdkSource implements IDescription, Comparable<SdkSource> {
}
/**
+ * Indicates if the source is enabled.
+ * <p/>
+ * A 3rd-party add-on source can be disabled by the user to prevent from loading it.
+ * This loads the persistent state from a settings file when first called.
+ *
+ * @return True if the source is enabled (default is true).
+ */
+ public boolean isEnabled() {
+ synchronized (sDisabledSourceUrls) {
+ if (sDisabledSourceUrls.isEmpty()) {
+ // Load state from persistent file
+
+ FileInputStream fis = null;
+ try {
+ String folder = AndroidLocation.getFolder();
+ File f = new File(folder, SRC_FILENAME);
+ if (f.exists()) {
+ fis = new FileInputStream(f);
+
+ sDisabledSourceUrls.load(fis);
+ }
+ } catch (IOException ignore) {
+ // nop
+ } catch (AndroidLocationException ignore) {
+ // nop
+ } finally {
+ if (fis != null) {
+ try {
+ fis.close();
+ } catch (IOException ignore) {}
+ }
+ }
+
+ if (sDisabledSourceUrls.isEmpty()) {
+ // Nothing was loaded. Initialize the storage with a version
+ // identified. This isn't currently checked back, but we might
+ // want it later if we decide to change the way this works.
+ // The version key is choosen on purpose to not match any valid URL.
+ sDisabledSourceUrls.setProperty("@version", "1"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ }
+
+ // A URL is enabled if it's not in the disabled list.
+ return sDisabledSourceUrls.getProperty(mUrl) == null;
+ }
+ }
+
+ /**
+ * Changes whether the source is marked as enabled.
+ * <p/>
+ * When <em>changing> the enable state, the current package list is purged
+ * and the next {@code load} will either return an empty list (if disabled) or
+ * the actual package list (if enabled.)
+ * <p/>
+ * This also persistent the change by updating a settings file.
+ *
+ * @param enabled True for the source to be enabled (can be loaded), false otherwise.
+ */
+ public void setEnabled(boolean enabled) {
+ // Comparing using isEnabled() has the voluntary side-effect of also
+ // loading the map from the persistent file the first time.
+ if (enabled != isEnabled()) {
+ // First we clear the current package list, which will force the
+ // next load() to actually set the package list as desired.
+ clearPackages();
+
+ synchronized (sDisabledSourceUrls) {
+ // Change the map
+ if (enabled) {
+ sDisabledSourceUrls.remove(mUrl);
+ } else {
+ // The "disabled" value is not being checked when reloading the map.
+ // We might want to do something with it later if a URL can have
+ // more attributes than just disabled.
+ sDisabledSourceUrls.setProperty(mUrl, "disabled"); //$NON-NLS-1$
+ }
+
+ // Persist it to the file
+ FileOutputStream fos = null;
+ try {
+ String folder = AndroidLocation.getFolder();
+ File f = new File(folder, SRC_FILENAME);
+
+ fos = new FileOutputStream(f);
+
+ sDisabledSourceUrls.store(fos,
+ "## Disabled Sources for Android SDK Manager"); //$NON-NLS-1$
+
+ } catch (AndroidLocationException ignore) {
+ // nop
+ } catch (IOException ignore) {
+ // nop
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException ignore) {}
+ }
+ }
+ }
+ }
+ }
+
+ /**
* Returns the short description of the source, if not null.
* Otherwise returns the default Object toString result.
* <p/>
@@ -258,13 +372,24 @@ public abstract class SdkSource implements IDescription, Comparable<SdkSource> {
}
/**
- * Tries to fetch the repository index for the given URL.
+ * Tries to fetch the repository index for the given URL and updates the package list.
+ * When a source is disabled, this create an empty non-null package list.
+ * <p/>
+ * Callers can get the package list using {@link #getPackages()} after this. It will be
+ * null in case of error, in which case {@link #getFetchError()} can be used to an
+ * error message.
*/
public void load(ITaskMonitor monitor, boolean forceHttp) {
+ setDefaultDescription();
monitor.setProgressMax(7);
- setDefaultDescription();
+ if (!isEnabled()) {
+ setPackages(new Package[0]);
+ mDescription += "\nSource is disabled.";
+ monitor.incProgress(7);
+ return;
+ }
String url = mUrl;
if (forceHttp) {
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkSources.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkSources.java
index 22311a7..dc33966 100755
--- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkSources.java
+++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkSources.java
@@ -44,11 +44,19 @@ public class SdkSources {
private final EnumMap<SdkSourceCategory, ArrayList<SdkSource>> mSources =
new EnumMap<SdkSourceCategory, ArrayList<SdkSource>>(SdkSourceCategory.class);
+ private ArrayList<Runnable> mChangeListeners; // lazily initialized
+
+
public SdkSources() {
}
/**
* Adds a new source to the Sources list.
+ * <p/>
+ * Implementation detail: {@link SdkSources} doesn't invoke {@link #notifyChangeListeners()}
+ * directly. Callers who use {@code add()} are responsible for notifying the listeners once
+ * they are done modifying the sources list. The intent is to notify the listeners only once
+ * at the end, not for every single addition.
*/
public void add(SdkSourceCategory category, SdkSource source) {
synchronized (mSources) {
@@ -64,6 +72,9 @@ public class SdkSources {
/**
* Removes a source from the Sources list.
+ * <p/>
+ * Callers who remove entries are responsible for notifying the listeners using
+ * {@link #notifyChangeListeners()} once they are done modifying the sources list.
*/
public void remove(SdkSource source) {
synchronized (mSources) {
@@ -85,6 +96,9 @@ public class SdkSources {
/**
* Removes all the sources in the given category.
+ * <p/>
+ * Callers who remove entries are responsible for notifying the listeners using
+ * {@link #notifyChangeListeners()} once they are done modifying the sources list.
*/
public void removeAll(SdkSourceCategory category) {
synchronized (mSources) {
@@ -243,53 +257,63 @@ public class SdkSources {
/**
* Loads all user sources. This <em>replaces</em> all existing user sources
* by the ones from the property file.
+ * <p/>
+ * This calls {@link #notifyChangeListeners()} at the end of the operation.
*/
public void loadUserAddons(ISdkLog log) {
-
- // Remove all existing user sources
- removeAll(SdkSourceCategory.USER_ADDONS);
-
- // 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, i)); //$NON-NLS-1$
- if (url != null) {
- SdkSource s = new SdkAddonSource(url, null/*uiName*/);
- if (!hasSourceUrl(s)) {
- add(SdkSourceCategory.USER_ADDONS, s);
+ // Implementation detail: synchronize on the sources list to make sure that
+ // a- the source list doesn't change while we load/save it, and most important
+ // b- to make sure it's not being saved while loaded or the reverse.
+ // In most cases we do these operation from the UI thread so it's not really
+ // that necessary. This is more a protection in case of someone calls this
+ // from a worker thread by mistake.
+ synchronized (mSources) {
+ // Remove all existing user sources
+ removeAll(SdkSourceCategory.USER_ADDONS);
+
+ // 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, i)); //$NON-NLS-1$
+ if (url != null) {
+ SdkSource s = new SdkAddonSource(url, null/*uiName*/);
+ if (!hasSourceUrl(s)) {
+ add(SdkSourceCategory.USER_ADDONS, s);
+ }
}
}
}
- }
- } catch (NumberFormatException e) {
- log.error(e, null);
+ } catch (NumberFormatException e) {
+ log.error(e, null);
- } catch (AndroidLocationException e) {
- log.error(e, null);
+ } catch (AndroidLocationException e) {
+ log.error(e, null);
- } catch (IOException e) {
- log.error(e, null);
+ } catch (IOException e) {
+ log.error(e, null);
- } finally {
- if (fis != null) {
- try {
- fis.close();
- } catch (IOException e) {
+ } finally {
+ if (fis != null) {
+ try {
+ fis.close();
+ } catch (IOException e) {
+ }
}
}
}
+ notifyChangeListeners();
}
/**
@@ -297,38 +321,93 @@ public class SdkSources {
* @param log Logger. Cannot be null.
*/
public void saveUserAddons(ISdkLog log) {
- FileOutputStream fos = null;
- try {
- String folder = AndroidLocation.getFolder();
- File f = new File(folder, SRC_FILENAME);
+ // See the implementation detail note in loadUserAddons() about the synchronization.
+ synchronized (mSources) {
+ FileOutputStream fos = null;
+ try {
+ String folder = AndroidLocation.getFolder();
+ File f = new File(folder, SRC_FILENAME);
- fos = new FileOutputStream(f);
+ fos = new FileOutputStream(f);
- Properties props = new Properties();
+ Properties props = new Properties();
- int count = 0;
- for (SdkSource s : getSources(SdkSourceCategory.USER_ADDONS)) {
- props.setProperty(String.format("%s%02d", KEY_SRC, count), s.getUrl()); //$NON-NLS-1$
- count++;
- }
- props.setProperty(KEY_COUNT, Integer.toString(count));
+ int count = 0;
+ for (SdkSource s : getSources(SdkSourceCategory.USER_ADDONS)) {
+ props.setProperty(String.format("%s%02d", KEY_SRC, count), //$NON-NLS-1$
+ s.getUrl());
+ count++;
+ }
+ props.setProperty(KEY_COUNT, Integer.toString(count));
- props.store( fos, "## User Sources for Android tool"); //$NON-NLS-1$
+ props.store( fos, "## User Sources for Android SDK Manager"); //$NON-NLS-1$
- } catch (AndroidLocationException e) {
- log.error(e, null);
+ } catch (AndroidLocationException e) {
+ log.error(e, null);
- } catch (IOException e) {
- log.error(e, null);
+ } catch (IOException e) {
+ log.error(e, null);
- } finally {
- if (fos != null) {
- try {
- fos.close();
- } catch (IOException e) {
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException e) {
+ }
}
}
}
+ }
+
+ /**
+ * Adds a listener that will be notified when the sources list has changed.
+ *
+ * @param changeListener A non-null listener to add. Ignored if already present.
+ * @see SdkSources#notifyChangeListeners()
+ */
+ public void addChangeListener(Runnable changeListener) {
+ assert changeListener != null;
+ if (mChangeListeners == null) {
+ mChangeListeners = new ArrayList<Runnable>();
+ }
+ synchronized (mChangeListeners) {
+ if (changeListener != null && !mChangeListeners.contains(changeListener)) {
+ mChangeListeners.add(changeListener);
+ }
+ }
+ }
+
+ /**
+ * Removes a listener from the list of listeners to notify when the sources change.
+ *
+ * @param changeListener A listener to remove. Ignored if not previously added.
+ */
+ public void removeChangeListener(Runnable changeListener) {
+ if (mChangeListeners != null && changeListener != null) {
+ synchronized (mChangeListeners) {
+ mChangeListeners.remove(changeListener);
+ }
+ }
+ }
+ /**
+ * Invoke all the registered change listeners, if any.
+ * <p/>
+ * This <em>may</em> be called from a worker thread, in which case the runnable
+ * should take care of only updating UI from a main thread.
+ */
+ public void notifyChangeListeners() {
+ if (mChangeListeners == null) {
+ return;
+ }
+ synchronized (mChangeListeners) {
+ for (Runnable runnable : mChangeListeners) {
+ try {
+ runnable.run();
+ } catch (Throwable ignore) {
+ assert ignore == null : "A SdkSource.ChangeListener failed with an exception.";
+ }
+ }
+ }
}
}