Fix the qualifier match algorithm.

Add proper support for density and keyboard state match.

Change-Id: I410aba52ee0f0d9df31fa2abdc9485054595263f
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/FolderConfiguration.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/FolderConfiguration.java
index 7857997..8a12e19 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/FolderConfiguration.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/FolderConfiguration.java
@@ -49,6 +49,13 @@
     private final static int INDEX_COUNT              = 14;
 
     /**
+     * Returns the number of {@link ResourceQualifier} that make up a Folder configuration.
+     */
+    public static int getQualifierCount() {
+        return INDEX_COUNT;
+    }
+
+    /**
      * Sets the config from the qualifiers of a given <var>config</var>.
      * @param config
      */
@@ -147,6 +154,16 @@
         }
     }
 
+    /**
+     * Returns a qualifier by its index. The total number of qualifiers can be accessed by
+     * {@link #getQualifierCount()}.
+     * @param index the index of the qualifier to return.
+     * @return the qualifier or null if there are none at the index.
+     */
+    public ResourceQualifier getQualifier(int index) {
+        return mQualifiers[index];
+    }
+
     public void setCountryCodeQualifier(CountryCodeQualifier qualifier) {
         mQualifiers[INDEX_COUNTRY_CODE] = qualifier;
     }
@@ -446,7 +463,7 @@
     }
 
     /**
-     * Returns whether the configuration match the given reference config.
+     * Returns whether the configuration is a match for the given reference config.
      * <p/>A match means that:
      * <ul>
      * <li>This config does not use any qualifier not used by the reference config</li>
@@ -454,29 +471,24 @@
      * the reference config.</li>
      * </ul>
      * @param referenceConfig The reference configuration to test against.
-     * @return the number of matching qualifiers or -1 if the configurations are not compatible.
+     * @return true if the configuration matches.
      */
-    public int match(FolderConfiguration referenceConfig) {
-        int matchCount = 0;
-
+    public boolean isMatchFor(FolderConfiguration referenceConfig) {
         for (int i = 0 ; i < INDEX_COUNT ; i++) {
             ResourceQualifier testQualifier = mQualifiers[i];
             ResourceQualifier referenceQualifier = referenceConfig.mQualifiers[i];
 
-            // we only care if testQualifier is non null. If it's null, it's a match but
-            // without increasing the matchCount.
+            // we only care if testQualifier is non null.
             if (testQualifier != null) {
-                if (referenceQualifier == null) {
-                    return -1;
-                } else if (testQualifier.equals(referenceQualifier) == false) {
-                    return -1;
+                if (referenceQualifier == null) { // reference config doesn't specify anything
+                                                  // for this qualifier so we refuse it.
+                    return false;
+                } else if (testQualifier.isMatchFor(referenceQualifier) == false) {
+                    return false;
                 }
-
-                // the qualifier match, increment the count
-                matchCount++;
             }
         }
-        return matchCount;
+        return true;
     }
 
     /**
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/KeyboardStateQualifier.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/KeyboardStateQualifier.java
index 6f87510..2777328 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/KeyboardStateQualifier.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/KeyboardStateQualifier.java
@@ -142,6 +142,42 @@
     }
 
     @Override
+    public boolean isMatchFor(ResourceQualifier qualifier) {
+        if (qualifier instanceof KeyboardStateQualifier) {
+            KeyboardStateQualifier referenceQualifier = (KeyboardStateQualifier)qualifier;
+
+            // special case where EXPOSED can be used for SOFT
+            if (referenceQualifier.mValue == KeyboardState.SOFT &&
+                    mValue == KeyboardState.EXPOSED) {
+                return true;
+            }
+
+            return referenceQualifier.mValue == mValue;
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean isBetterMatchThan(ResourceQualifier compareTo, ResourceQualifier reference) {
+        if (compareTo == null) {
+            return true;
+        }
+
+        KeyboardStateQualifier compareQualifier = (KeyboardStateQualifier)compareTo;
+        KeyboardStateQualifier referenceQualifier = (KeyboardStateQualifier)reference;
+        if (referenceQualifier.mValue == KeyboardState.SOFT) { // only case where there could be a
+                                                               // better qualifier
+            // only return true if it's a better value.
+            if (compareQualifier.mValue == KeyboardState.EXPOSED && mValue == KeyboardState.SOFT) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    @Override
     public boolean equals(Object qualifier) {
         if (qualifier instanceof KeyboardStateQualifier) {
             return mValue == ((KeyboardStateQualifier)qualifier).mValue;
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/PixelDensityQualifier.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/PixelDensityQualifier.java
index f75e9cb..d67d4ae 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/PixelDensityQualifier.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/PixelDensityQualifier.java
@@ -184,6 +184,38 @@
     }
 
     @Override
+    public boolean isMatchFor(ResourceQualifier qualifier) {
+        if (qualifier instanceof PixelDensityQualifier) {
+            // as long as there's a density qualifier, it's always a match.
+            // The best match will be found later.
+            return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean isBetterMatchThan(ResourceQualifier compareTo, ResourceQualifier reference) {
+        if (compareTo == null) {
+            return true;
+        }
+
+        PixelDensityQualifier compareQ = (PixelDensityQualifier)compareTo;
+        PixelDensityQualifier referenceQ = (PixelDensityQualifier)reference;
+
+        if (mValue == referenceQ.mValue && compareQ.mValue != referenceQ.mValue) {
+            // got exact value, this is the best!
+            return true;
+        } else {
+            // in all case we're going to prefer the higher dpi.
+            // if reference is high, we want highest dpi.
+            // if reference is medium, we'll prefer to scale down high dpi, than scale up low dpi
+            // if reference if low, we'll prefer to scale down high than medium (2:1 over 4:3)
+            return mValue.mDpiValue > compareQ.mValue.mDpiValue;
+        }
+    }
+
+    @Override
     public boolean equals(Object qualifier) {
         if (qualifier instanceof PixelDensityQualifier) {
             return mValue == ((PixelDensityQualifier)qualifier).mValue;
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/ResourceQualifier.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/ResourceQualifier.java
index bfee8d2..ba54ad0 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/ResourceQualifier.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/configurations/ResourceQualifier.java
@@ -62,6 +62,32 @@
      */
     public abstract String getFolderSegment(IAndroidTarget target);
 
+    /**
+     * Returns whether the given qualifier is a match for the receiver.
+     * <p/>The default implementation returns the result of {@link #equals(Object)}.
+     * <p/>Children class that re-implements this must implement
+     * {@link #isBetterMatchThan(ResourceQualifier, ResourceQualifier)} too.
+     * @param qualifier the reference qualifier
+     * @return true if the receiver is a match.
+     */
+    public boolean isMatchFor(ResourceQualifier qualifier) {
+        return equals(qualifier);
+    }
+
+    /**
+     * Returns true if the receiver is a better match for the given <var>reference</var> than
+     * the given <var>compareTo</var> comparable.
+     * @param compareTo The {@link ResourceQualifier} to compare to. Can be null, in which
+     * case the method must return <code>true</code>.
+     * @param reference The reference qualifier value for which the match is.
+     * @return true if the receiver is a better match.
+     */
+    public boolean isBetterMatchThan(ResourceQualifier compareTo, ResourceQualifier reference) {
+        // the default is to always return false. This gives less overhead than always returning
+        // true, as it would only compare same values anyway.
+        return false;
+    }
+
     @Override
     public String toString() {
         return getFolderSegment(null);
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectResources.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectResources.java
index e8c3687..9d715d0 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectResources.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectResources.java
@@ -22,6 +22,7 @@
 import com.android.ide.eclipse.adt.internal.resources.configurations.FolderConfiguration;
 import com.android.ide.eclipse.adt.internal.resources.configurations.LanguageQualifier;
 import com.android.ide.eclipse.adt.internal.resources.configurations.RegionQualifier;
+import com.android.ide.eclipse.adt.internal.resources.configurations.ResourceQualifier;
 import com.android.ide.eclipse.adt.internal.resources.manager.files.IAbstractFolder;
 import com.android.layoutlib.api.IResourceValue;
 import com.android.layoutlib.utils.ResourceValue;
@@ -505,79 +506,102 @@
      * Returns the best matching {@link Resource}.
      * @param resources the list of {@link Resource} to choose from.
      * @param referenceConfig the {@link FolderConfiguration} to match.
+     * @see http://d.android.com/guide/topics/resources/resources-i18n.html#best-match
      */
     private Resource findMatchingConfiguredResource(List<? extends Resource> resources,
             FolderConfiguration referenceConfig) {
-        // look for resources with the maximum number of qualifier match.
-        int currentMax = -1;
+        //
+        // 1: eliminate resources that contradict the reference configuration
+        // 2: pick next qualifier type
+        // 3: check if any resources use this qualifier, if no, back to 2, else move on to 4.
+        // 4: eliminate resources that don't use this qualifier.
+        // 5: if more than one resource left, go back to 2.
+        //
+        // The precedence of the qualifiers is more important than the number of qualifiers that
+        // exactly match the device.
+
+        // 1: eliminate resources that contradict
         ArrayList<Resource> matchingResources = new ArrayList<Resource>();
         for (int i = 0 ; i < resources.size(); i++) {
             Resource res = resources.get(i);
 
-            int count = res.getConfiguration().match(referenceConfig);
-            if (count > currentMax) {
-                matchingResources.clear();
-                matchingResources.add(res);
-                currentMax = count;
-            } else if (count != -1 && count == currentMax) {
+            if (res.getConfiguration().isMatchFor(referenceConfig)) {
                 matchingResources.add(res);
             }
         }
 
-        // if we have more than one match, we look for the match with the qualifiers with the
-        // highest priority.
-        Resource resMatch = null;
+        // if there is only one match, just take it
         if (matchingResources.size() == 1) {
-            resMatch = matchingResources.get(0);
-        } else if (matchingResources.size() > 1) {
-            // More than one resource with the same number of qualifier match.
-            // We loop, looking for the resource with the highest priority qualifiers.
-            ArrayList<Resource> tmpResources = new ArrayList<Resource>();
-            int startIndex = 0;
-            while (matchingResources.size() > 1) {
-                int highest = -1;
-                for (int i = 0 ; i < matchingResources.size() ; i++) {
-                    Resource folder = matchingResources.get(i);
+            return matchingResources.get(0);
+        } else if (matchingResources.size() == 0) {
+            return null;
+        }
 
-                    // get highest priority qualifiers.
-                    int m = folder.getConfiguration().getHighestPriorityQualifier(startIndex);
+        // 2. Loop on the qualifiers, and eliminate matches
+        final int count = FolderConfiguration.getQualifierCount();
+        for (int q = 0 ; q < count ; q++) {
+            // look to see if one resource has this qualifier.
+            // At the same time also record the best match value for the qualifier (if applicable).
+            ResourceQualifier referenceQualifier = referenceConfig.getQualifier(q);
 
-                    // add to the list if highest.
-                    if (m != -1) {
-                        if (highest == -1 || m == highest) {
-                            tmpResources.add(folder);
-                            highest = m;
-                        } else if (m < highest) { // highest priority == lowest index.
-                            tmpResources.clear();
-                            tmpResources.add(folder);
+            if (referenceQualifier != null) { // no need to check if it's null, since the loop
+                                              // above will have removed the resources anyway.
+                boolean found = false;
+                ResourceQualifier bestMatch = null;
+                for (Resource res : matchingResources) {
+                    ResourceQualifier qualifier = res.getConfiguration().getQualifier(q);
+                    if (qualifier != null) {
+                        // set the flag.
+                        found = true;
+
+                        // now check for a best match.
+                        if (qualifier.isBetterMatchThan(bestMatch, referenceQualifier)) {
+                            bestMatch = qualifier;
                         }
                     }
                 }
 
-                // at this point, we have a list with 1+ resources that all have the same highest
-                // priority qualifiers. Go through the list again looking for the next highest
-                // priority qualifier.
-                startIndex = highest + 1;
+                // if a resources as a qualifier at the current index, remove all the resources that
+                // do not have one.
+                // If there is one, and we have a bestComparable, also check that it's equal to the
+                // best comparable.
+                if (found) {
+                    for (int i = 0 ; i < matchingResources.size(); ) {
+                        Resource res = matchingResources.get(i);
+                        ResourceQualifier qualifier = res.getConfiguration().getQualifier(q);
 
-                // this should not happen, but it's better to check.
-                if (matchingResources.size() == tmpResources.size() && highest == -1) {
-                    // this means all the resources match with the same qualifiers
-                    // (highest == -1 means we reached the end of the qualifier list)
-                    // In this case, we arbitrarily take the first resource.
-                    matchingResources.clear();
-                    matchingResources.add(tmpResources.get(0));
-                } else {
-                    matchingResources.clear();
-                    matchingResources.addAll(tmpResources);
+                        if (qualifier == null) { // no qualifier? remove the resources
+                            matchingResources.remove(res);
+                        } else if (bestMatch != null && bestMatch.equals(qualifier) == false) {
+                            // if there is a best match, only accept the resource if the qualifier
+                            // has the same best value.
+                            matchingResources.remove(res);
+                        } else {
+                            i++;
+                        }
+                    }
+
+                    // at this point we may have run out of matching resources before going
+                    // through all the qualifiers.
+                    if (matchingResources.size() == 1) {
+                        return matchingResources.get(0);
+                    } else if (matchingResources.size() == 0) {
+                        return null;
+                    }
                 }
-                tmpResources.clear();
             }
-
-            // we should have only one match here.
-            resMatch = matchingResources.get(0);
         }
 
-        return resMatch;
+        // went through all the qualifiers. We should not have more than one
+        switch (matchingResources.size()) {
+            case 0:
+                return null;
+            case 1:
+                return matchingResources.get(1);
+            case 2:
+                assert false;
+        }
+        return null;
     }
 
     /**