Home | History | Annotate | Download | only in repository
      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;
     18 
     19 import com.android.sdklib.AndroidVersion;
     20 import com.android.sdklib.IAndroidTarget;
     21 import com.android.sdklib.SdkConstants;
     22 import com.android.sdklib.SdkManager;
     23 import com.android.sdklib.AndroidVersion.AndroidVersionException;
     24 import com.android.sdklib.internal.repository.Archive.Arch;
     25 import com.android.sdklib.internal.repository.Archive.Os;
     26 import com.android.sdklib.repository.PkgProps;
     27 import com.android.sdklib.repository.SdkRepoConstants;
     28 
     29 import org.w3c.dom.Node;
     30 
     31 import java.io.File;
     32 import java.io.FileInputStream;
     33 import java.io.FileOutputStream;
     34 import java.io.IOException;
     35 import java.io.UnsupportedEncodingException;
     36 import java.security.MessageDigest;
     37 import java.security.NoSuchAlgorithmException;
     38 import java.util.Map;
     39 import java.util.Properties;
     40 
     41 /**
     42  * Represents a sample XML node in an SDK repository.
     43  */
     44 public class SamplePackage extends MinToolsPackage
     45     implements IPackageVersion, IMinApiLevelDependency {
     46 
     47     /** The matching platform version. */
     48     private final AndroidVersion mVersion;
     49 
     50     /**
     51      * The minimal API level required by this extra package, if > 0,
     52      * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement.
     53      */
     54     private final int mMinApiLevel;
     55 
     56     /**
     57      * Creates a new sample package from the attributes and elements of the given XML node.
     58      * This constructor should throw an exception if the package cannot be created.
     59      *
     60      * @param source The {@link SdkSource} where this is loaded from.
     61      * @param packageNode The XML element being parsed.
     62      * @param nsUri The namespace URI of the originating XML document, to be able to deal with
     63      *          parameters that vary according to the originating XML schema.
     64      * @param licenses The licenses loaded from the XML originating document.
     65      */
     66     SamplePackage(SdkSource source, Node packageNode, String nsUri, Map<String,String> licenses) {
     67         super(source, packageNode, nsUri, licenses);
     68 
     69         int apiLevel = XmlParserUtils.getXmlInt   (packageNode, SdkRepoConstants.NODE_API_LEVEL, 0);
     70         String codeName = XmlParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_CODENAME);
     71         if (codeName.length() == 0) {
     72             codeName = null;
     73         }
     74         mVersion = new AndroidVersion(apiLevel, codeName);
     75 
     76         mMinApiLevel = XmlParserUtils.getXmlInt(packageNode, SdkRepoConstants.NODE_MIN_API_LEVEL,
     77                 MIN_API_LEVEL_NOT_SPECIFIED);
     78     }
     79 
     80     /**
     81      * Creates a new sample package based on an actual {@link IAndroidTarget} (which
     82      * must have {@link IAndroidTarget#isPlatform()} true) from the {@link SdkManager}.
     83      * <p/>
     84      * The target <em>must</em> have an existing sample directory that uses the /samples
     85      * root form rather than the old form where the samples dir was located under the
     86      * platform dir.
     87      * <p/>
     88      * This is used to list local SDK folders in which case there is one archive which
     89      * URL is the actual samples path location.
     90      * <p/>
     91      * By design, this creates a package with one and only one archive.
     92      */
     93     static Package create(IAndroidTarget target, Properties props) {
     94         return new SamplePackage(target, props);
     95     }
     96 
     97     private SamplePackage(IAndroidTarget target, Properties props) {
     98         super(  null,                                   //source
     99                 props,                                  //properties
    100                 0,                                      //revision will be taken from props
    101                 null,                                   //license
    102                 null,                                   //description
    103                 null,                                   //descUrl
    104                 Os.ANY,                                 //archiveOs
    105                 Arch.ANY,                               //archiveArch
    106                 target.getPath(IAndroidTarget.SAMPLES)  //archiveOsPath
    107                 );
    108 
    109         mVersion = target.getVersion();
    110 
    111         mMinApiLevel = Integer.parseInt(
    112             getProperty(props,
    113                     PkgProps.SAMPLE_MIN_API_LEVEL,
    114                     Integer.toString(MIN_API_LEVEL_NOT_SPECIFIED)));
    115     }
    116 
    117     /**
    118      * Creates a new sample package from an actual directory path and previously
    119      * saved properties.
    120      * <p/>
    121      * This is used to list local SDK folders in which case there is one archive which
    122      * URL is the actual samples path location.
    123      * <p/>
    124      * By design, this creates a package with one and only one archive.
    125      *
    126      * @throws AndroidVersionException if the {@link AndroidVersion} can't be restored
    127      *                                 from properties.
    128      */
    129     static Package create(String archiveOsPath, Properties props) throws AndroidVersionException {
    130         return new SamplePackage(archiveOsPath, props);
    131     }
    132 
    133     private SamplePackage(String archiveOsPath, Properties props) throws AndroidVersionException {
    134         super(null,                                   //source
    135               props,                                  //properties
    136               0,                                      //revision will be taken from props
    137               null,                                   //license
    138               null,                                   //description
    139               null,                                   //descUrl
    140               Os.ANY,                                 //archiveOs
    141               Arch.ANY,                               //archiveArch
    142               archiveOsPath                           //archiveOsPath
    143               );
    144 
    145         mVersion = new AndroidVersion(props);
    146 
    147         mMinApiLevel = Integer.parseInt(
    148             getProperty(props,
    149                     PkgProps.SAMPLE_MIN_API_LEVEL,
    150                     Integer.toString(MIN_API_LEVEL_NOT_SPECIFIED)));
    151     }
    152 
    153     /**
    154      * Save the properties of the current packages in the given {@link Properties} object.
    155      * These properties will later be given to a constructor that takes a {@link Properties} object.
    156      */
    157     @Override
    158     void saveProperties(Properties props) {
    159         super.saveProperties(props);
    160 
    161         mVersion.saveProperties(props);
    162 
    163         if (getMinApiLevel() != MIN_API_LEVEL_NOT_SPECIFIED) {
    164             props.setProperty(PkgProps.SAMPLE_MIN_API_LEVEL, Integer.toString(getMinApiLevel()));
    165         }
    166     }
    167 
    168     /**
    169      * Returns the minimal API level required by this extra package, if > 0,
    170      * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement.
    171      */
    172     public int getMinApiLevel() {
    173         return mMinApiLevel;
    174     }
    175 
    176     /** Returns the matching platform version. */
    177     public AndroidVersion getVersion() {
    178         return mVersion;
    179     }
    180 
    181     /**
    182      * Returns a string identifier to install this package from the command line.
    183      * For samples, we use "sample-N" where N is the API or the preview codename.
    184      * <p/>
    185      * {@inheritDoc}
    186      */
    187     @Override
    188     public String installId() {
    189         return "sample-" + mVersion.getApiString();    //$NON-NLS-1$
    190     }
    191 
    192     /**
    193      * Returns a description of this package that is suitable for a list display.
    194      * <p/>
    195      * {@inheritDoc}
    196      */
    197     @Override
    198     public String getListDescription() {
    199         String s = String.format("Samples for SDK API %1$s%2$s%3$s",
    200                 mVersion.getApiString(),
    201                 mVersion.isPreview() ? " Preview" : "",
    202                 isObsolete() ? " (Obsolete)" : "");
    203         return s;
    204     }
    205 
    206     /**
    207      * Returns a short description for an {@link IDescription}.
    208      */
    209     @Override
    210     public String getShortDescription() {
    211         String s = String.format("Samples for SDK API %1$s%2$s, revision %3$d%4$s",
    212                 mVersion.getApiString(),
    213                 mVersion.isPreview() ? " Preview" : "",
    214                 getRevision(),
    215                 isObsolete() ? " (Obsolete)" : "");
    216         return s;
    217     }
    218 
    219     /**
    220      * Returns a long description for an {@link IDescription}.
    221      *
    222      * The long description is whatever the XML contains for the &lt;description&gt; field,
    223      * or the short description if the former is empty.
    224      */
    225     @Override
    226     public String getLongDescription() {
    227         String s = getDescription();
    228         if (s == null || s.length() == 0) {
    229             s = getShortDescription();
    230         }
    231 
    232         if (s.indexOf("revision") == -1) {
    233             s += String.format("\nRevision %1$d%2$s",
    234                     getRevision(),
    235                     isObsolete() ? " (Obsolete)" : "");
    236         }
    237 
    238         return s;
    239     }
    240 
    241     /**
    242      * Computes a potential installation folder if an archive of this package were
    243      * to be installed right away in the given SDK root.
    244      * <p/>
    245      * A sample package is typically installed in SDK/samples/android-"version".
    246      * However if we can find a different directory that already has this sample
    247      * version installed, we'll use that one.
    248      *
    249      * @param osSdkRoot The OS path of the SDK root folder.
    250      * @param sdkManager An existing SDK manager to list current platforms and addons.
    251      * @return A new {@link File} corresponding to the directory to use to install this package.
    252      */
    253     @Override
    254     public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) {
    255 
    256         // The /samples dir at the root of the SDK
    257         File samplesRoot = new File(osSdkRoot, SdkConstants.FD_SAMPLES);
    258 
    259         // First find if this sample is already installed. If so, reuse the same directory.
    260         for (IAndroidTarget target : sdkManager.getTargets()) {
    261             if (target.isPlatform() &&
    262                     target.getVersion().equals(mVersion)) {
    263                 String p = target.getPath(IAndroidTarget.SAMPLES);
    264                 File f = new File(p);
    265                 if (f.isDirectory()) {
    266                     // We *only* use this directory if it's using the "new" location
    267                     // under SDK/samples. We explicitly do not reuse the "old" location
    268                     // under SDK/platform/android-N/samples.
    269                     if (f.getParentFile().equals(samplesRoot)) {
    270                         return f;
    271                     }
    272                 }
    273             }
    274         }
    275 
    276         // Otherwise, get a suitable default
    277         File folder = new File(samplesRoot,
    278                 String.format("android-%s", getVersion().getApiString())); //$NON-NLS-1$
    279 
    280         for (int n = 1; folder.exists(); n++) {
    281             // Keep trying till we find an unused directory.
    282             folder = new File(samplesRoot,
    283                     String.format("android-%s_%d", getVersion().getApiString(), n)); //$NON-NLS-1$
    284         }
    285 
    286         return folder;
    287     }
    288 
    289     @Override
    290     public boolean sameItemAs(Package pkg) {
    291         if (pkg instanceof SamplePackage) {
    292             SamplePackage newPkg = (SamplePackage)pkg;
    293 
    294             // check they are the same version.
    295             return newPkg.getVersion().equals(this.getVersion());
    296         }
    297 
    298         return false;
    299     }
    300 
    301     /**
    302      * Makes sure the base /samples folder exists before installing.
    303      *
    304      * {@inheritDoc}
    305      */
    306     @Override
    307     public boolean preInstallHook(Archive archive,
    308             ITaskMonitor monitor,
    309             String osSdkRoot,
    310             File installFolder) {
    311 
    312         if (installFolder != null && installFolder.isDirectory()) {
    313             // Get the hash computed during the last installation
    314             String storedHash = readContentHash(installFolder);
    315             if (storedHash != null && storedHash.length() > 0) {
    316 
    317                 // Get the hash of the folder now
    318                 String currentHash = computeContentHash(installFolder);
    319 
    320                 if (!storedHash.equals(currentHash)) {
    321                     // The hashes differ. The content was modified.
    322                     // Ask the user if we should still wipe the old samples.
    323 
    324                     String pkgName = archive.getParentPackage().getShortDescription();
    325 
    326                     String msg = String.format(
    327                             "-= Warning ! =-\n" +
    328                             "You are about to replace the content of the folder:\n " +
    329                             "  %1$s\n" +
    330                             "by the new package:\n" +
    331                             "  %2$s.\n" +
    332                             "\n" +
    333                             "However it seems that the content of the existing samples " +
    334                             "has been modified since it was last installed. Are you sure " +
    335                             "you want to DELETE the existing samples? This cannot be undone.\n" +
    336                             "Please select YES to delete the existing sample and replace them " +
    337                             "by the new ones.\n" +
    338                             "Please select NO to skip this package. You can always install it later.",
    339                             installFolder.getAbsolutePath(),
    340                             pkgName);
    341 
    342                     // Returns true if we can wipe & replace.
    343                     return monitor.displayPrompt("SDK Manager: overwrite samples?", msg);
    344                 }
    345             }
    346         }
    347 
    348         // The default is to allow installation
    349         return super.preInstallHook(archive, monitor, osSdkRoot, installFolder);
    350     }
    351 
    352     /**
    353      * Computes a hash of the installed content (in case of successful install.)
    354      *
    355      * {@inheritDoc}
    356      */
    357     @Override
    358     public void postInstallHook(Archive archive, ITaskMonitor monitor, File installFolder) {
    359         super.postInstallHook(archive, monitor, installFolder);
    360 
    361         if (installFolder != null) {
    362             String h = computeContentHash(installFolder);
    363             saveContentHash(installFolder, h);
    364         }
    365     }
    366 
    367     /**
    368      * Reads the hash from the properties file, if it exists.
    369      * Returns null if something goes wrong, e.g. there's no property file or
    370      * it doesn't contain our hash. Returns an empty string if the hash wasn't
    371      * correctly computed last time by {@link #saveContentHash(File, String)}.
    372      */
    373     private String readContentHash(File folder) {
    374         Properties props = new Properties();
    375 
    376         FileInputStream fis = null;
    377         try {
    378             File f = new File(folder, SdkConstants.FN_CONTENT_HASH_PROP);
    379             if (f.isFile()) {
    380                 fis = new FileInputStream(f);
    381                 props.load(fis);
    382                 return props.getProperty("content-hash", null);  //$NON-NLS-1$
    383             }
    384         } catch (Exception e) {
    385             // ignore
    386         } finally {
    387             if (fis != null) {
    388                 try {
    389                     fis.close();
    390                 } catch (IOException e) {
    391                 }
    392             }
    393         }
    394 
    395         return null;
    396     }
    397 
    398     /**
    399      * Saves the hash using a properties file
    400      */
    401     private void saveContentHash(File folder, String hash) {
    402         Properties props = new Properties();
    403 
    404         props.setProperty("content-hash", hash == null ? "" : hash);  //$NON-NLS-1$ //$NON-NLS-2$
    405 
    406         FileOutputStream fos = null;
    407         try {
    408             File f = new File(folder, SdkConstants.FN_CONTENT_HASH_PROP);
    409             fos = new FileOutputStream(f);
    410             props.store( fos, "## Android - hash of this archive.");  //$NON-NLS-1$
    411         } catch (IOException e) {
    412             e.printStackTrace();
    413         } finally {
    414             if (fos != null) {
    415                 try {
    416                     fos.close();
    417                 } catch (IOException e) {
    418                 }
    419             }
    420         }
    421     }
    422 
    423     /**
    424      * Computes a hash of the files names and sizes installed in the folder
    425      * using the SHA-1 digest.
    426      * Returns null if the digest algorithm is not available.
    427      */
    428     private String computeContentHash(File installFolder) {
    429         MessageDigest md = null;
    430         try {
    431             // SHA-1 is a standard algorithm.
    432             // http://java.sun.com/j2se/1.4.2/docs/guide/security/CryptoSpec.html#AppB
    433             md = MessageDigest.getInstance("SHA-1");    //$NON-NLS-1$
    434         } catch (NoSuchAlgorithmException e) {
    435             // We're unlikely to get there unless this JVM is not spec conforming
    436             // in which case there won't be any hash available.
    437         }
    438 
    439         if (md != null) {
    440             hashDirectoryContent(installFolder, md);
    441             return getDigestHexString(md);
    442         }
    443 
    444         return null;
    445     }
    446 
    447     /**
    448      * Computes a hash of the *content* of this directory. The hash only uses
    449      * the files names and the file sizes.
    450      */
    451     private void hashDirectoryContent(File folder, MessageDigest md) {
    452         if (folder == null || md == null || !folder.isDirectory()) {
    453             return;
    454         }
    455 
    456         for (File f : folder.listFiles()) {
    457             if (f.isDirectory()) {
    458                 hashDirectoryContent(f, md);
    459 
    460             } else {
    461                 String name = f.getName();
    462 
    463                 // Skip the file we use to store the content hash
    464                 if (name == null || SdkConstants.FN_CONTENT_HASH_PROP.equals(name)) {
    465                     continue;
    466                 }
    467 
    468                 try {
    469                     md.update(name.getBytes("UTF-8"));   //$NON-NLS-1$
    470                 } catch (UnsupportedEncodingException e) {
    471                     // There is no valid reason for UTF-8 to be unsupported. Ignore.
    472                 }
    473                 try {
    474                     long len = f.length();
    475                     md.update((byte) (len & 0x0FF));
    476                     md.update((byte) ((len >> 8) & 0x0FF));
    477                     md.update((byte) ((len >> 16) & 0x0FF));
    478                     md.update((byte) ((len >> 24) & 0x0FF));
    479 
    480                 } catch (SecurityException e) {
    481                     // Might happen if file is not readable. Ignore.
    482                 }
    483             }
    484         }
    485     }
    486 
    487     /**
    488      * Returns a digest as an hex string.
    489      */
    490     private String getDigestHexString(MessageDigest digester) {
    491         // Create an hex string from the digest
    492         byte[] digest = digester.digest();
    493         int n = digest.length;
    494         String hex = "0123456789abcdef";                     //$NON-NLS-1$
    495         char[] hexDigest = new char[n * 2];
    496         for (int i = 0; i < n; i++) {
    497             int b = digest[i] & 0x0FF;
    498             hexDigest[i*2 + 0] = hex.charAt(b >>> 4);
    499             hexDigest[i*2 + 1] = hex.charAt(b & 0x0f);
    500         }
    501 
    502         return new String(hexDigest);
    503     }
    504 }
    505