• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.sdklib.internal.repository.sources;
18 
19 import com.android.prefs.AndroidLocation;
20 import com.android.prefs.AndroidLocation.AndroidLocationException;
21 import com.android.sdklib.ISdkLog;
22 
23 import java.io.File;
24 import java.io.FileInputStream;
25 import java.io.FileOutputStream;
26 import java.io.IOException;
27 import java.util.ArrayList;
28 import java.util.EnumMap;
29 import java.util.Iterator;
30 import java.util.Properties;
31 import java.util.Map.Entry;
32 
33 /**
34  * A list of sdk-repository and sdk-addon sources, sorted by {@link SdkSourceCategory}.
35  */
36 public class SdkSources {
37 
38     private static final String KEY_COUNT = "count";
39 
40     private static final String KEY_SRC = "src";
41 
42     private static final String SRC_FILENAME = "repositories.cfg"; //$NON-NLS-1$
43 
44     private final EnumMap<SdkSourceCategory, ArrayList<SdkSource>> mSources =
45         new EnumMap<SdkSourceCategory, ArrayList<SdkSource>>(SdkSourceCategory.class);
46 
47     private ArrayList<Runnable> mChangeListeners; // lazily initialized
48 
49 
SdkSources()50     public SdkSources() {
51     }
52 
53     /**
54      * Adds a new source to the Sources list.
55      * <p/>
56      * Implementation detail: {@link SdkSources} doesn't invoke {@link #notifyChangeListeners()}
57      * directly. Callers who use {@code add()} are responsible for notifying the listeners once
58      * they are done modifying the sources list. The intent is to notify the listeners only once
59      * at the end, not for every single addition.
60      */
add(SdkSourceCategory category, SdkSource source)61     public void add(SdkSourceCategory category, SdkSource source) {
62         synchronized (mSources) {
63             ArrayList<SdkSource> list = mSources.get(category);
64             if (list == null) {
65                 list = new ArrayList<SdkSource>();
66                 mSources.put(category, list);
67             }
68 
69             list.add(source);
70         }
71     }
72 
73     /**
74      * Removes a source from the Sources list.
75      * <p/>
76      * Callers who remove entries are responsible for notifying the listeners using
77      * {@link #notifyChangeListeners()} once they are done modifying the sources list.
78      */
remove(SdkSource source)79     public void remove(SdkSource source) {
80         synchronized (mSources) {
81             Iterator<Entry<SdkSourceCategory, ArrayList<SdkSource>>> it =
82                 mSources.entrySet().iterator();
83             while (it.hasNext()) {
84                 Entry<SdkSourceCategory, ArrayList<SdkSource>> entry = it.next();
85                 ArrayList<SdkSource> list = entry.getValue();
86 
87                 if (list.remove(source)) {
88                     if (list.isEmpty()) {
89                         // remove the entry since the source list became empty
90                         it.remove();
91                     }
92                 }
93             }
94         }
95     }
96 
97     /**
98      * Removes all the sources in the given category.
99      * <p/>
100      * Callers who remove entries are responsible for notifying the listeners using
101      * {@link #notifyChangeListeners()} once they are done modifying the sources list.
102      */
removeAll(SdkSourceCategory category)103     public void removeAll(SdkSourceCategory category) {
104         synchronized (mSources) {
105             mSources.remove(category);
106         }
107     }
108 
109     /**
110      * Returns a set of all categories that must be displayed. This includes all
111      * categories that are to be always displayed as well as all categories which
112      * have at least one source.
113      * Might return a empty array, but never returns null.
114      */
getCategories()115     public SdkSourceCategory[] getCategories() {
116         ArrayList<SdkSourceCategory> cats = new ArrayList<SdkSourceCategory>();
117 
118         for (SdkSourceCategory cat : SdkSourceCategory.values()) {
119             if (cat.getAlwaysDisplay()) {
120                 cats.add(cat);
121             } else {
122                 synchronized (mSources) {
123                     ArrayList<SdkSource> list = mSources.get(cat);
124                     if (list != null && !list.isEmpty()) {
125                         cats.add(cat);
126                     }
127                 }
128             }
129         }
130 
131         return cats.toArray(new SdkSourceCategory[cats.size()]);
132     }
133 
134     /**
135      * Returns a new array of sources attached to the given category.
136      * Might return an empty array, but never returns null.
137      */
getSources(SdkSourceCategory category)138     public SdkSource[] getSources(SdkSourceCategory category) {
139         synchronized (mSources) {
140             ArrayList<SdkSource> list = mSources.get(category);
141             if (list == null) {
142                 return new SdkSource[0];
143             } else {
144                 return list.toArray(new SdkSource[list.size()]);
145             }
146         }
147     }
148 
149     /**
150      * Returns an array of the sources across all categories. This is never null.
151      */
getAllSources()152     public SdkSource[] getAllSources() {
153         synchronized (mSources) {
154             int n = 0;
155 
156             for (ArrayList<SdkSource> list : mSources.values()) {
157                 n += list.size();
158             }
159 
160             SdkSource[] sources = new SdkSource[n];
161 
162             int i = 0;
163             for (ArrayList<SdkSource> list : mSources.values()) {
164                 for (SdkSource source : list) {
165                     sources[i++] = source;
166                 }
167             }
168 
169             return sources;
170         }
171     }
172 
173     /**
174      * Each source keeps a local cache of whatever it loaded recently.
175      * This calls {@link SdkSource#clearPackages()} on all the available sources,
176      * and the next call to {@link SdkSource#getPackages()} will actually reload
177      * the remote package list.
178      */
clearAllPackages()179     public void clearAllPackages() {
180         synchronized (mSources) {
181             for (ArrayList<SdkSource> list : mSources.values()) {
182                 for (SdkSource source : list) {
183                     source.clearPackages();
184                 }
185             }
186         }
187     }
188 
189     /**
190      * Returns the category of a given source, or null if the source is unknown.
191      * <p/>
192      * Note that this method uses object identity to find a given source, and does
193      * not identify sources by their URL like {@link #hasSourceUrl(SdkSource)} does.
194      * <p/>
195      * The search is O(N), which should be acceptable on the expectedly small source list.
196      */
getCategory(SdkSource source)197     public SdkSourceCategory getCategory(SdkSource source) {
198         if (source != null) {
199             synchronized (mSources) {
200                 for (Entry<SdkSourceCategory, ArrayList<SdkSource>> entry : mSources.entrySet()) {
201                     if (entry.getValue().contains(source)) {
202                         return entry.getKey();
203                     }
204                 }
205             }
206         }
207         return null;
208     }
209 
210     /**
211      * Returns true if there's already a similar source in the sources list
212      * under any category.
213      * <p/>
214      * Important: The match is NOT done on object identity.
215      * Instead, this searches for a <em>similar</em> source, based on
216      * {@link SdkSource#equals(Object)} which compares the source URLs.
217      * <p/>
218      * The search is O(N), which should be acceptable on the expectedly small source list.
219      */
hasSourceUrl(SdkSource source)220     public boolean hasSourceUrl(SdkSource source) {
221         synchronized (mSources) {
222             for (ArrayList<SdkSource> list : mSources.values()) {
223                 for (SdkSource s : list) {
224                     if (s.equals(source)) {
225                         return true;
226                     }
227                 }
228             }
229             return false;
230         }
231     }
232 
233     /**
234      * Returns true if there's already a similar source in the sources list
235      * under the specified category.
236      * <p/>
237      * Important: The match is NOT done on object identity.
238      * Instead, this searches for a <em>similar</em> source, based on
239      * {@link SdkSource#equals(Object)} which compares the source URLs.
240      * <p/>
241      * The search is O(N), which should be acceptable on the expectedly small source list.
242      */
hasSourceUrl(SdkSourceCategory category, SdkSource source)243     public boolean hasSourceUrl(SdkSourceCategory category, SdkSource source) {
244         synchronized (mSources) {
245             ArrayList<SdkSource> list = mSources.get(category);
246             if (list != null) {
247                 for (SdkSource s : list) {
248                     if (s.equals(source)) {
249                         return true;
250                     }
251                 }
252             }
253             return false;
254         }
255     }
256 
257     /**
258      * Loads all user sources. This <em>replaces</em> all existing user sources
259      * by the ones from the property file.
260      * <p/>
261      * This calls {@link #notifyChangeListeners()} at the end of the operation.
262      */
loadUserAddons(ISdkLog log)263     public void loadUserAddons(ISdkLog log) {
264         // Implementation detail: synchronize on the sources list to make sure that
265         // a- the source list doesn't change while we load/save it, and most important
266         // b- to make sure it's not being saved while loaded or the reverse.
267         // In most cases we do these operation from the UI thread so it's not really
268         // that necessary. This is more a protection in case of someone calls this
269         // from a worker thread by mistake.
270         synchronized (mSources) {
271             // Remove all existing user sources
272             removeAll(SdkSourceCategory.USER_ADDONS);
273 
274             // Load new user sources from property file
275             FileInputStream fis = null;
276             try {
277                 String folder = AndroidLocation.getFolder();
278                 File f = new File(folder, SRC_FILENAME);
279                 if (f.exists()) {
280                     fis = new FileInputStream(f);
281 
282                     Properties props = new Properties();
283                     props.load(fis);
284 
285                     int count = Integer.parseInt(props.getProperty(KEY_COUNT, "0"));
286 
287                     for (int i = 0; i < count; i++) {
288                         String url = props.getProperty(String.format("%s%02d", KEY_SRC, i));  //$NON-NLS-1$
289                         if (url != null) {
290                             SdkSource s = new SdkAddonSource(url, null/*uiName*/);
291                             if (!hasSourceUrl(s)) {
292                                 add(SdkSourceCategory.USER_ADDONS, s);
293                             }
294                         }
295                     }
296                 }
297 
298             } catch (NumberFormatException e) {
299                 log.error(e, null);
300 
301             } catch (AndroidLocationException e) {
302                 log.error(e, null);
303 
304             } catch (IOException e) {
305                 log.error(e, null);
306 
307             } finally {
308                 if (fis != null) {
309                     try {
310                         fis.close();
311                     } catch (IOException e) {
312                     }
313                 }
314             }
315         }
316         notifyChangeListeners();
317     }
318 
319     /**
320      * Saves all the user sources.
321      * @param log Logger. Cannot be null.
322      */
saveUserAddons(ISdkLog log)323     public void saveUserAddons(ISdkLog log) {
324         // See the implementation detail note in loadUserAddons() about the synchronization.
325         synchronized (mSources) {
326             FileOutputStream fos = null;
327             try {
328                 String folder = AndroidLocation.getFolder();
329                 File f = new File(folder, SRC_FILENAME);
330 
331                 fos = new FileOutputStream(f);
332 
333                 Properties props = new Properties();
334 
335                 int count = 0;
336                 for (SdkSource s : getSources(SdkSourceCategory.USER_ADDONS)) {
337                     props.setProperty(String.format("%s%02d", KEY_SRC, count), //$NON-NLS-1$
338                                       s.getUrl());
339                     count++;
340                 }
341                 props.setProperty(KEY_COUNT, Integer.toString(count));
342 
343                 props.store( fos, "## User Sources for Android SDK Manager");  //$NON-NLS-1$
344 
345             } catch (AndroidLocationException e) {
346                 log.error(e, null);
347 
348             } catch (IOException e) {
349                 log.error(e, null);
350 
351             } finally {
352                 if (fos != null) {
353                     try {
354                         fos.close();
355                     } catch (IOException e) {
356                     }
357                 }
358             }
359         }
360     }
361 
362     /**
363      * Adds a listener that will be notified when the sources list has changed.
364      *
365      * @param changeListener A non-null listener to add. Ignored if already present.
366      * @see SdkSources#notifyChangeListeners()
367      */
addChangeListener(Runnable changeListener)368     public void addChangeListener(Runnable changeListener) {
369         assert changeListener != null;
370         if (mChangeListeners == null) {
371             mChangeListeners = new ArrayList<Runnable>();
372         }
373         synchronized (mChangeListeners) {
374             if (changeListener != null && !mChangeListeners.contains(changeListener)) {
375                 mChangeListeners.add(changeListener);
376             }
377         }
378     }
379 
380     /**
381      * Removes a listener from the list of listeners to notify when the sources change.
382      *
383      * @param changeListener A listener to remove. Ignored if not previously added.
384      */
removeChangeListener(Runnable changeListener)385     public void removeChangeListener(Runnable changeListener) {
386         if (mChangeListeners != null && changeListener != null) {
387             synchronized (mChangeListeners) {
388                 mChangeListeners.remove(changeListener);
389             }
390         }
391     }
392 
393     /**
394      * Invoke all the registered change listeners, if any.
395      * <p/>
396      * This <em>may</em> be called from a worker thread, in which case the runnable
397      * should take care of only updating UI from a main thread.
398      */
notifyChangeListeners()399     public void notifyChangeListeners() {
400         if (mChangeListeners == null) {
401             return;
402         }
403         synchronized (mChangeListeners) {
404             for (Runnable runnable : mChangeListeners) {
405                 try {
406                     runnable.run();
407                 } catch (Throwable ignore) {
408                     assert ignore == null : "A SdkSource.ChangeListener failed with an exception.";
409                 }
410             }
411         }
412     }
413 }
414