| /* |
| * Copyright (C) 2015 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.tools.idea.sdk.remote.internal.sources; |
| |
| import com.android.annotations.concurrency.GuardedBy; |
| import com.android.prefs.AndroidLocation; |
| import com.android.prefs.AndroidLocation.AndroidLocationException; |
| import com.android.sdklib.repository.SdkSysImgConstants; |
| import com.android.utils.ILogger; |
| import com.google.common.collect.Lists; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.util.*; |
| import java.util.Map.Entry; |
| |
| /** |
| * A list of sdk-repository and sdk-addon sources, sorted by {@link SdkSourceCategory}. |
| */ |
| public class SdkSources { |
| |
| private static final String KEY_COUNT = "count"; |
| |
| private static final String KEY_SRC = "src"; |
| private static final String KEY_DISPLAY = "disp"; |
| |
| private static final String SRC_FILENAME = "repositories.cfg"; //$NON-NLS-1$ |
| |
| @GuardedBy("itself") |
| private final EnumMap<SdkSourceCategory, ArrayList<SdkSource>> mySources = |
| new EnumMap<SdkSourceCategory, ArrayList<SdkSource>>(SdkSourceCategory.class); |
| |
| 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 (mySources) { |
| ArrayList<SdkSource> list = mySources.get(category); |
| if (list == null) { |
| list = new ArrayList<SdkSource>(); |
| mySources.put(category, list); |
| } |
| |
| list.add(source); |
| } |
| } |
| |
| /** |
| * Replaces the current collection of sources corresponding to a particular category with the given collection. |
| * <p/> |
| * Implementation detail: {@link SdkSources} doesn't invoke {@link #notifyChangeListeners()} |
| * directly. Callers who use {@code set()} 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 set(SdkSourceCategory category, Collection<SdkSource> sources) { |
| synchronized (mySources) { |
| mySources.put(category, Lists.newArrayList(sources)); |
| } |
| } |
| |
| /** |
| * 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 (mySources) { |
| Iterator<Entry<SdkSourceCategory, ArrayList<SdkSource>>> it = mySources.entrySet().iterator(); |
| while (it.hasNext()) { |
| Entry<SdkSourceCategory, ArrayList<SdkSource>> entry = it.next(); |
| ArrayList<SdkSource> list = entry.getValue(); |
| |
| if (list.remove(source)) { |
| if (list.isEmpty()) { |
| // remove the entry since the source list became empty |
| it.remove(); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * 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 (mySources) { |
| mySources.remove(category); |
| } |
| } |
| |
| /** |
| * Returns a set of all categories that must be displayed. This includes all |
| * categories that are to be always displayed as well as all categories which |
| * have at least one source. |
| * Might return a empty array, but never returns null. |
| */ |
| public SdkSourceCategory[] getCategories() { |
| ArrayList<SdkSourceCategory> cats = new ArrayList<SdkSourceCategory>(); |
| |
| for (SdkSourceCategory cat : SdkSourceCategory.values()) { |
| if (cat.getAlwaysDisplay()) { |
| cats.add(cat); |
| } |
| else { |
| synchronized (mySources) { |
| ArrayList<SdkSource> list = mySources.get(cat); |
| if (list != null && !list.isEmpty()) { |
| cats.add(cat); |
| } |
| } |
| } |
| } |
| |
| return cats.toArray(new SdkSourceCategory[cats.size()]); |
| } |
| |
| /** |
| * Returns a new array of sources attached to the given category. |
| * Might return an empty array, but never returns null. |
| */ |
| public SdkSource[] getSources(SdkSourceCategory category) { |
| synchronized (mySources) { |
| ArrayList<SdkSource> list = mySources.get(category); |
| if (list == null) { |
| return new SdkSource[0]; |
| } |
| else { |
| return list.toArray(new SdkSource[list.size()]); |
| } |
| } |
| } |
| |
| /** |
| * Returns true if there are sources for the given category. |
| */ |
| public boolean hasSources(SdkSourceCategory category) { |
| synchronized (mySources) { |
| ArrayList<SdkSource> list = mySources.get(category); |
| return list != null && !list.isEmpty(); |
| } |
| } |
| |
| /** |
| * Returns an array of the sources across all categories. This is never null. |
| */ |
| public SdkSource[] getAllSources() { |
| synchronized (mySources) { |
| int n = 0; |
| |
| for (ArrayList<SdkSource> list : mySources.values()) { |
| n += list.size(); |
| } |
| |
| SdkSource[] sources = new SdkSource[n]; |
| |
| int i = 0; |
| for (ArrayList<SdkSource> list : mySources.values()) { |
| for (SdkSource source : list) { |
| sources[i++] = source; |
| } |
| } |
| |
| return sources; |
| } |
| } |
| |
| /** |
| * Each source keeps a local cache of whatever it loaded recently. |
| * This calls {@link SdkSource#clearPackages()} on all the available sources, |
| * and the next call to {@link SdkSource#getPackages()} will actually reload |
| * the remote package list. |
| */ |
| public void clearAllPackages() { |
| synchronized (mySources) { |
| for (ArrayList<SdkSource> list : mySources.values()) { |
| for (SdkSource source : list) { |
| source.clearPackages(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Returns the category of a given source, or null if the source is unknown. |
| * <p/> |
| * Note that this method uses object identity to find a given source, and does |
| * not identify sources by their URL like {@link #hasSourceUrl(SdkSource)} does. |
| * <p/> |
| * The search is O(N), which should be acceptable on the expectedly small source list. |
| */ |
| public SdkSourceCategory getCategory(SdkSource source) { |
| if (source != null) { |
| synchronized (mySources) { |
| for (Entry<SdkSourceCategory, ArrayList<SdkSource>> entry : mySources.entrySet()) { |
| if (entry.getValue().contains(source)) { |
| return entry.getKey(); |
| } |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns true if there's already a similar source in the sources list |
| * under any category. |
| * <p/> |
| * Important: The match is NOT done on object identity. |
| * Instead, this searches for a <em>similar</em> source, based on |
| * {@link SdkSource#equals(Object)} which compares the source URLs. |
| * <p/> |
| * The search is O(N), which should be acceptable on the expectedly small source list. |
| */ |
| public boolean hasSourceUrl(SdkSource source) { |
| synchronized (mySources) { |
| for (ArrayList<SdkSource> list : mySources.values()) { |
| for (SdkSource s : list) { |
| if (s.equals(source)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| } |
| |
| /** |
| * Returns true if there's already a similar source in the sources list |
| * under the specified category. |
| * <p/> |
| * Important: The match is NOT done on object identity. |
| * Instead, this searches for a <em>similar</em> source, based on |
| * {@link SdkSource#equals(Object)} which compares the source URLs. |
| * <p/> |
| * The search is O(N), which should be acceptable on the expectedly small source list. |
| */ |
| public boolean hasSourceUrl(SdkSourceCategory category, SdkSource source) { |
| synchronized (mySources) { |
| ArrayList<SdkSource> list = mySources.get(category); |
| if (list != null) { |
| for (SdkSource s : list) { |
| if (s.equals(source)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| } |
| |
| /** |
| * 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(ILogger log) { |
| // 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 (mySources) { |
| // 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$ |
| String disp = props.getProperty(String.format("%s%02d", KEY_DISPLAY, i)); //$NON-NLS-1$ |
| if (url != null) { |
| // FIXME: this code originally only dealt with add-on XML sources. |
| // Now we'd like it to deal with system-image sources too, but we |
| // don't know which kind of object it is (at least not without |
| // trying to fetch it.) As a temporary workaround, just take a |
| // guess based on the leaf URI name. However ideally what we can |
| // simply do is add a checkbox "is system-image XML" in the user |
| // dialog and pass this info down here. Another alternative is to |
| // make a "dynamic" source object that tries to guess its type once |
| // the URI has been fetched. |
| SdkSource s; |
| if (url.endsWith(SdkSysImgConstants.URL_DEFAULT_FILENAME)) { |
| s = new SdkSysImgSource(url, disp); |
| } |
| else { |
| s = new SdkAddonSource(url, disp); |
| } |
| if (!hasSourceUrl(s)) { |
| add(SdkSourceCategory.USER_ADDONS, s); |
| } |
| } |
| } |
| } |
| |
| } |
| 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 Logger. Cannot be null. |
| */ |
| public void saveUserAddons(ILogger log) { |
| // See the implementation detail note in loadUserAddons() about the synchronization. |
| synchronized (mySources) { |
| 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 (SdkSource s : getSources(SdkSourceCategory.USER_ADDONS)) { |
| props.setProperty(String.format("%s%02d", KEY_SRC, count), //$NON-NLS-1$ |
| s.getUrl()); |
| if (s.getUiName() != null) { |
| props.setProperty(String.format("%s%02d", KEY_DISPLAY, count), //$NON-NLS-1$ |
| s.getUiName()); |
| } |
| count++; |
| } |
| props.setProperty(KEY_COUNT, Integer.toString(count)); |
| |
| props.store(fos, "## User Sources for Android SDK Manager"); //$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) { |
| } |
| } |
| } |
| } |
| } |
| } |