1/*
2 * Copyright (C) 2012 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
17package com.android.sdklib.devices;
18
19import com.android.SdkConstants;
20import com.android.annotations.Nullable;
21import com.android.prefs.AndroidLocation;
22import com.android.prefs.AndroidLocation.AndroidLocationException;
23import com.android.resources.Keyboard;
24import com.android.resources.KeyboardState;
25import com.android.resources.Navigation;
26import com.android.sdklib.internal.avd.AvdManager;
27import com.android.sdklib.internal.avd.HardwareProperties;
28import com.android.sdklib.repository.PkgProps;
29import com.android.utils.ILogger;
30
31import org.xml.sax.SAXException;
32
33import java.io.BufferedReader;
34import java.io.File;
35import java.io.FileNotFoundException;
36import java.io.FileOutputStream;
37import java.io.FileReader;
38import java.io.IOException;
39import java.util.ArrayList;
40import java.util.Collection;
41import java.util.Collections;
42import java.util.HashMap;
43import java.util.Iterator;
44import java.util.List;
45import java.util.Map;
46import java.util.Set;
47import java.util.regex.Matcher;
48import java.util.regex.Pattern;
49
50import javax.xml.parsers.ParserConfigurationException;
51import javax.xml.transform.TransformerException;
52import javax.xml.transform.TransformerFactoryConfigurationError;
53
54/**
55 * Manager class for interacting with {@link Device}s within the SDK
56 */
57public class DeviceManager {
58
59    private final static String sDeviceProfilesProp = "DeviceProfiles";
60    private final static Pattern sPathPropertyPattern = Pattern.compile("^" + PkgProps.EXTRA_PATH
61            + "=" + sDeviceProfilesProp + "$");
62    private ILogger mLog;
63    // Vendor devices can't be a static list since they change based on the SDK
64    // Location
65    private List<Device> mVendorDevices;
66    // Keeps track of where the currently loaded vendor devices were loaded from
67    private String mVendorDevicesLocation = "";
68    private static List<Device> mUserDevices;
69    private static List<Device> mDefaultDevices;
70    private static final Object sLock = new Object();
71    private static final List<DevicesChangeListener> sListeners =
72                                        new ArrayList<DevicesChangeListener>();
73
74    public static enum DeviceStatus {
75        /**
76         * The device exists unchanged from the given configuration
77         */
78        EXISTS,
79        /**
80         * A device exists with the given name and manufacturer, but has a different configuration
81         */
82        CHANGED,
83        /**
84         * There is no device with the given name and manufacturer
85         */
86        MISSING;
87    }
88
89    // TODO: Refactor this to look more like AvdManager so that we don't have
90    // multiple instances in the same application, which forces us to parse
91    // the XML multiple times when we don't have to.
92    public DeviceManager(ILogger log) {
93        mLog = log;
94    }
95
96    /**
97     * Interface implemented by objects which want to know when changes occur to the {@link Device}
98     * lists.
99     */
100    public static interface DevicesChangeListener {
101        /**
102         * Called after one of the {@link Device} lists has been updated.
103         */
104        public void onDevicesChange();
105    }
106
107    /**
108     * Register a listener to be notified when the device lists are modified.
109     *
110     * @param listener The listener to add. Ignored if already registered.
111     */
112    public void registerListener(DevicesChangeListener listener) {
113        if (listener != null) {
114            synchronized (sListeners) {
115                if (!sListeners.contains(listener)) {
116                    sListeners.add(listener);
117                }
118            }
119        }
120    }
121
122    /**
123     * Removes a listener from the notification list such that it will no longer receive
124     * notifications when modifications to the {@link Device} list occur.
125     *
126     * @param listener The listener to remove.
127     */
128    public boolean unregisterListener(DevicesChangeListener listener) {
129        synchronized (sListeners) {
130            return sListeners.remove(listener);
131        }
132    }
133
134    public DeviceStatus getDeviceStatus(
135            @Nullable String sdkLocation, String name, String manufacturer, int hashCode) {
136        Device d = getDevice(sdkLocation, name, manufacturer);
137        if (d == null) {
138            return DeviceStatus.MISSING;
139        } else {
140            return d.hashCode() == hashCode ? DeviceStatus.EXISTS : DeviceStatus.CHANGED;
141        }
142    }
143
144    public Device getDevice(@Nullable String sdkLocation, String name, String manufacturer) {
145        List<Device> devices;
146        if (sdkLocation != null) {
147            devices = getDevices(sdkLocation);
148        } else {
149            devices = new ArrayList<Device>(getDefaultDevices());
150            devices.addAll(getUserDevices());
151        }
152        for (Device d : devices) {
153            if (d.getName().equals(name) && d.getManufacturer().equals(manufacturer)) {
154                return d;
155            }
156        }
157        return null;
158    }
159
160    /**
161     * Returns both vendor provided and user created {@link Device}s.
162     *
163     * @param sdkLocation Location of the Android SDK
164     * @return A list of both vendor and user provided {@link Device}s
165     */
166    public List<Device> getDevices(String sdkLocation) {
167        List<Device> devices = new ArrayList<Device>(getVendorDevices(sdkLocation));
168        devices.addAll(getDefaultDevices());
169        devices.addAll(getUserDevices());
170        return Collections.unmodifiableList(devices);
171    }
172
173    /**
174     * Gets the {@link List} of {@link Device}s packaged with the SDK.
175     *
176     * @return The {@link List} of default {@link Device}s
177     */
178    public List<Device> getDefaultDevices() {
179        synchronized (sLock) {
180            if (mDefaultDevices == null) {
181                try {
182                    mDefaultDevices = DeviceParser.parse(
183                            DeviceManager.class.getResourceAsStream(SdkConstants.FN_DEVICES_XML));
184                } catch (IllegalStateException e) {
185                    // The device builders can throw IllegalStateExceptions if
186                    // build gets called before everything is properly setup
187                    mLog.error(e, null);
188                    mDefaultDevices = new ArrayList<Device>();
189                } catch (Exception e) {
190                    mLog.error(null, "Error reading default devices");
191                    mDefaultDevices = new ArrayList<Device>();
192                }
193                notifyListeners();
194            }
195        }
196        return Collections.unmodifiableList(mDefaultDevices);
197    }
198
199    /**
200     * Returns all vendor-provided {@link Device}s
201     *
202     * @param sdkLocation Location of the Android SDK
203     * @return A list of vendor-provided {@link Device}s
204     */
205    public List<Device> getVendorDevices(String sdkLocation) {
206        synchronized (sLock) {
207            if (mVendorDevices == null || !mVendorDevicesLocation.equals(sdkLocation)) {
208                mVendorDevicesLocation = sdkLocation;
209                List<Device> devices = new ArrayList<Device>();
210
211                // Load devices from tools folder
212                File toolsDevices = new File(sdkLocation, SdkConstants.OS_SDK_TOOLS_LIB_FOLDER +
213                        File.separator + SdkConstants.FN_DEVICES_XML);
214                if (toolsDevices.isFile()) {
215                    devices.addAll(loadDevices(toolsDevices));
216                }
217
218                // Load devices from vendor extras
219                File extrasFolder = new File(sdkLocation, SdkConstants.FD_EXTRAS);
220                List<File> deviceDirs = getExtraDirs(extrasFolder);
221                for (File deviceDir : deviceDirs) {
222                    File deviceXml = new File(deviceDir, SdkConstants.FN_DEVICES_XML);
223                    if (deviceXml.isFile()) {
224                        devices.addAll(loadDevices(deviceXml));
225                    }
226                }
227                mVendorDevices = devices;
228                notifyListeners();
229            }
230        }
231        return Collections.unmodifiableList(mVendorDevices);
232    }
233
234    /**
235     * Returns all user-created {@link Device}s
236     *
237     * @return All user-created {@link Device}s
238     */
239    public List<Device> getUserDevices() {
240        synchronized (sLock) {
241            if (mUserDevices == null) {
242                // User devices should be saved out to
243                // $HOME/.android/devices.xml
244                mUserDevices = new ArrayList<Device>();
245                File userDevicesFile = null;
246                try {
247                    userDevicesFile = new File(AndroidLocation.getFolder(),
248                            SdkConstants.FN_DEVICES_XML);
249                    if (userDevicesFile.exists()) {
250                        mUserDevices.addAll(DeviceParser.parse(userDevicesFile));
251                        notifyListeners();
252                    }
253                } catch (AndroidLocationException e) {
254                    mLog.warning("Couldn't load user devices: %1$s", e.getMessage());
255                } catch (SAXException e) {
256                    // Probably an old config file which we don't want to overwrite.
257                    if (userDevicesFile != null) {
258                        String base = userDevicesFile.getAbsoluteFile() + ".old";
259                        File renamedConfig = new File(base);
260                        int i = 0;
261                        while (renamedConfig.exists()) {
262                            renamedConfig = new File(base + '.' + (i++));
263                        }
264                        mLog.error(null, "Error parsing %1$s, backing up to %2$s",
265                                userDevicesFile.getAbsolutePath(), renamedConfig.getAbsolutePath());
266                        userDevicesFile.renameTo(renamedConfig);
267                    }
268                } catch (ParserConfigurationException e) {
269                    mLog.error(null, "Error parsing %1$s",
270                            userDevicesFile == null ? "(null)" : userDevicesFile.getAbsolutePath());
271                } catch (IOException e) {
272                    mLog.error(null, "Error parsing %1$s",
273                            userDevicesFile == null ? "(null)" : userDevicesFile.getAbsolutePath());
274                }
275            }
276        }
277        return Collections.unmodifiableList(mUserDevices);
278    }
279
280    public void addUserDevice(Device d) {
281        synchronized (sLock) {
282            if (mUserDevices == null) {
283                getUserDevices();
284            }
285            mUserDevices.add(d);
286        }
287        notifyListeners();
288    }
289
290    public void removeUserDevice(Device d) {
291        synchronized (sLock) {
292            if (mUserDevices == null) {
293                getUserDevices();
294            }
295            Iterator<Device> it = mUserDevices.iterator();
296            while (it.hasNext()) {
297                Device userDevice = it.next();
298                if (userDevice.getName().equals(d.getName())
299                        && userDevice.getManufacturer().equals(d.getManufacturer())) {
300                    it.remove();
301                    notifyListeners();
302                    break;
303                }
304
305            }
306        }
307    }
308
309    public void replaceUserDevice(Device d) {
310        synchronized (sLock) {
311            if (mUserDevices == null) {
312                getUserDevices();
313            }
314            removeUserDevice(d);
315            addUserDevice(d);
316        }
317    }
318
319    /**
320     * Saves out the user devices to {@link SdkConstants#FN_DEVICES_XML} in
321     * {@link AndroidLocation#getFolder()}.
322     */
323    public void saveUserDevices() {
324        synchronized (sLock) {
325            if (mUserDevices != null && mUserDevices.size() != 0) {
326                File userDevicesFile;
327                try {
328                    userDevicesFile = new File(AndroidLocation.getFolder(),
329                            SdkConstants.FN_DEVICES_XML);
330                    DeviceWriter.writeToXml(new FileOutputStream(userDevicesFile), mUserDevices);
331                } catch (AndroidLocationException e) {
332                    mLog.warning("Couldn't find user directory: %1$s", e.getMessage());
333                } catch (FileNotFoundException e) {
334                    mLog.warning("Couldn't open file: %1$s", e.getMessage());
335                } catch (ParserConfigurationException e) {
336                    mLog.warning("Error writing file: %1$s", e.getMessage());
337                } catch (TransformerFactoryConfigurationError e) {
338                    mLog.warning("Error writing file: %1$s", e.getMessage());
339                } catch (TransformerException e) {
340                    mLog.warning("Error writing file: %1$s", e.getMessage());
341                }
342            }
343        }
344    }
345
346    /**
347     * Returns hardware properties (defined in hardware.ini) as a {@link Map}.
348     *
349     * @param s The {@link State} from which to derive the hardware properties.
350     * @return A {@link Map} of hardware properties.
351     */
352    public static Map<String, String> getHardwareProperties(State s) {
353        Hardware hw = s.getHardware();
354        Map<String, String> props = new HashMap<String, String>();
355        props.put(HardwareProperties.HW_MAINKEYS,
356                getBooleanVal(hw.getButtonType().equals(ButtonType.HARD)));
357        props.put(HardwareProperties.HW_TRACKBALL,
358                getBooleanVal(hw.getNav().equals(Navigation.TRACKBALL)));
359        props.put(HardwareProperties.HW_KEYBOARD,
360                getBooleanVal(hw.getKeyboard().equals(Keyboard.QWERTY)));
361        props.put(HardwareProperties.HW_DPAD,
362                getBooleanVal(hw.getNav().equals(Navigation.DPAD)));
363
364        Set<Sensor> sensors = hw.getSensors();
365        props.put(HardwareProperties.HW_GPS, getBooleanVal(sensors.contains(Sensor.GPS)));
366        props.put(HardwareProperties.HW_BATTERY,
367                getBooleanVal(hw.getChargeType().equals(PowerType.BATTERY)));
368        props.put(HardwareProperties.HW_ACCELEROMETER,
369                getBooleanVal(sensors.contains(Sensor.ACCELEROMETER)));
370        props.put(HardwareProperties.HW_ORIENTATION_SENSOR,
371                getBooleanVal(sensors.contains(Sensor.GYROSCOPE)));
372        props.put(HardwareProperties.HW_AUDIO_INPUT, getBooleanVal(hw.hasMic()));
373        props.put(HardwareProperties.HW_SDCARD, getBooleanVal(hw.getRemovableStorage().size() > 0));
374        props.put(HardwareProperties.HW_LCD_DENSITY,
375                Integer.toString(hw.getScreen().getPixelDensity().getDpiValue()));
376        props.put(HardwareProperties.HW_PROXIMITY_SENSOR,
377                getBooleanVal(sensors.contains(Sensor.PROXIMITY_SENSOR)));
378        return props;
379    }
380
381    /**
382     * Returns the hardware properties defined in
383     * {@link AvdManager#HARDWARE_INI} as a {@link Map}.
384     *
385     * @param d The {@link Device} from which to derive the hardware properties.
386     * @return A {@link Map} of hardware properties.
387     */
388    public static Map<String, String> getHardwareProperties(Device d) {
389        Map<String, String> props = getHardwareProperties(d.getDefaultState());
390        for (State s : d.getAllStates()) {
391            if (s.getKeyState().equals(KeyboardState.HIDDEN)) {
392                props.put("hw.keyboard.lid", getBooleanVal(true));
393            }
394        }
395        props.put(AvdManager.AVD_INI_DEVICE_HASH, Integer.toString(d.hashCode()));
396        props.put(AvdManager.AVD_INI_DEVICE_NAME, d.getName());
397        props.put(AvdManager.AVD_INI_DEVICE_MANUFACTURER, d.getManufacturer());
398        return props;
399    }
400
401    /**
402     * Takes a boolean and returns the appropriate value for
403     * {@link HardwareProperties}
404     *
405     * @param bool The boolean value to turn into the appropriate
406     *            {@link HardwareProperties} value.
407     * @return {@code HardwareProperties#BOOLEAN_VALUES[0]} if true,
408     *         {@code HardwareProperties#BOOLEAN_VALUES[1]} otherwise.
409     */
410    private static String getBooleanVal(boolean bool) {
411        if (bool) {
412            return HardwareProperties.BOOLEAN_VALUES[0];
413        }
414        return HardwareProperties.BOOLEAN_VALUES[1];
415    }
416
417    private Collection<Device> loadDevices(File deviceXml) {
418        try {
419            return DeviceParser.parse(deviceXml);
420        } catch (SAXException e) {
421            mLog.error(null, "Error parsing %1$s", deviceXml.getAbsolutePath());
422        } catch (ParserConfigurationException e) {
423            mLog.error(null, "Error parsing %1$s", deviceXml.getAbsolutePath());
424        } catch (IOException e) {
425            mLog.error(null, "Error reading %1$s", deviceXml.getAbsolutePath());
426        } catch (IllegalStateException e) {
427            // The device builders can throw IllegalStateExceptions if
428            // build gets called before everything is properly setup
429            mLog.error(e, null);
430        }
431        return new ArrayList<Device>();
432    }
433
434    private void notifyListeners() {
435        synchronized (sListeners) {
436            for (DevicesChangeListener listener : sListeners) {
437                listener.onDevicesChange();
438            }
439        }
440    }
441
442    /* Returns all of DeviceProfiles in the extras/ folder */
443    private List<File> getExtraDirs(File extrasFolder) {
444        List<File> extraDirs = new ArrayList<File>();
445        // All OEM provided device profiles are in
446        // $SDK/extras/$VENDOR/$ITEM/devices.xml
447        if (extrasFolder != null && extrasFolder.isDirectory()) {
448            for (File vendor : extrasFolder.listFiles()) {
449                if (vendor.isDirectory()) {
450                    for (File item : vendor.listFiles()) {
451                        if (item.isDirectory() && isDevicesExtra(item)) {
452                            extraDirs.add(item);
453                        }
454                    }
455                }
456            }
457        }
458
459        return extraDirs;
460    }
461
462    /*
463     * Returns whether a specific folder for a specific vendor is a
464     * DeviceProfiles folder
465     */
466    private boolean isDevicesExtra(File item) {
467        File properties = new File(item, SdkConstants.FN_SOURCE_PROP);
468        try {
469            BufferedReader propertiesReader = new BufferedReader(new FileReader(properties));
470            try {
471                String line;
472                while ((line = propertiesReader.readLine()) != null) {
473                    Matcher m = sPathPropertyPattern.matcher(line);
474                    if (m.matches()) {
475                        return true;
476                    }
477                }
478            } finally {
479                propertiesReader.close();
480            }
481        } catch (IOException ignore) {
482        }
483        return false;
484    }
485}
486