Add <feature-group> tag and change aapt badging

A <feature-group> represents a set of features required
for an app to be compatible with a device. Multiple
<feature-group> elements represent a logical 'or'
of required features.

Features defined in the old way with <uses-feature> tags
under the <manifest> tag are automatically added to each
feature-group defined.

Defining a <feature-group> means that any default
features are not included (such as android.hardware.touchscreen)
and declared permissions do not imply any features.

Change-Id: I45626f0fdc546e47bcf2aead7ef05ebcca12b023
diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java
index 9866200..db87cf7 100644
--- a/core/java/android/content/pm/PackageParser.java
+++ b/core/java/android/content/pm/PackageParser.java
@@ -1530,6 +1530,11 @@
 
                 XmlUtils.skipCurrentTag(parser);
 
+            } else if (tagName.equals("feature-group")) {
+                // Skip this for now until we know what to do with it.
+
+                XmlUtils.skipCurrentTag(parser);
+
             } else if (tagName.equals("uses-sdk")) {
                 if (SDK_VERSION > 0) {
                     sa = res.obtainAttributes(attrs,
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 7311a60..c268d97 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -1285,6 +1285,19 @@
         <attr name="required" format="boolean" />
     </declare-styleable>
 
+    <!-- The <code>feature-group</code> tag specifies
+         a set of one or more <code>uses-feature</code> elements that
+         the application can utilize. An application uses multiple
+         <code>feature-group</code> sets to indicate that it can support
+         different combinations of features.
+
+         <p>This appears as a child tag of the root
+         {@link #AndroidManifest manifest} tag. -->
+    <declare-styleable name="AndroidManifestFeatureGroup">
+        <!-- The human-readable name of the feature group. -->
+        <attr name="label" />
+    </declare-styleable>
+
     <!-- The <code>uses-sdk</code> tag describes the SDK features that the
          containing package must be running on to operate correctly.
          
diff --git a/tests/UsesFeature2Test/Android.mk b/tests/UsesFeature2Test/Android.mk
new file mode 100644
index 0000000..cc784d7
--- /dev/null
+++ b/tests/UsesFeature2Test/Android.mk
@@ -0,0 +1,25 @@
+#
+# Copyright (C) 2014 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.
+#
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_PACKAGE_NAME := UsesFeature2Test
+
+LOCAL_MODULE_TAGS := tests
+
+include $(BUILD_PACKAGE)
diff --git a/tests/UsesFeature2Test/AndroidManifest.xml b/tests/UsesFeature2Test/AndroidManifest.xml
new file mode 100644
index 0000000..724d186
--- /dev/null
+++ b/tests/UsesFeature2Test/AndroidManifest.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.test.usesfeature2">
+
+    <uses-sdk android:minSdkVersion="10" android:targetSdkVersion="19" />
+
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+    <uses-permission android:name="android.permission.BLUETOOTH" />
+
+    <uses-feature android:name="android.hardware.sensor.accelerometer" />
+    <feature-group android:label="@string/minimal">
+        <uses-feature android:name="android.hardware.dpad" />
+        <uses-feature android:name="android.hardware.touchscreen.multitouch.distinct" />
+    </feature-group>
+    <feature-group android:label="@string/gamepad">
+        <uses-feature android:name="android.hardware.gamepad" />
+    </feature-group>
+
+    <application android:label="@string/app_title">
+        <activity android:name="ActivityMain">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/tests/UsesFeature2Test/res/values/values.xml b/tests/UsesFeature2Test/res/values/values.xml
new file mode 100644
index 0000000..2ee9107
--- /dev/null
+++ b/tests/UsesFeature2Test/res/values/values.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<resources>
+    <string name="app_title">Uses Feature 2.0</string>
+    <string name="minimal">Crippled experience</string>
+    <string name="gamepad">Gamer experience</string>
+</resources>
diff --git a/tools/aapt/Command.cpp b/tools/aapt/Command.cpp
index 5fefab6..ac1ae70 100644
--- a/tools/aapt/Command.cpp
+++ b/tools/aapt/Command.cpp
@@ -4,20 +4,23 @@
 // Android Asset Packaging Tool main entry point.
 //
 #include "ApkBuilder.h"
-#include "Main.h"
 #include "Bundle.h"
+#include "Images.h"
+#include "Main.h"
 #include "ResourceFilter.h"
 #include "ResourceTable.h"
-#include "Images.h"
 #include "XMLNode.h"
 
-#include <utils/Log.h>
-#include <utils/threads.h>
-#include <utils/List.h>
 #include <utils/Errors.h>
+#include <utils/KeyedVector.h>
+#include <utils/List.h>
+#include <utils/Log.h>
+#include <utils/SortedVector.h>
+#include <utils/threads.h>
+#include <utils/Vector.h>
 
-#include <fcntl.h>
 #include <errno.h>
+#include <fcntl.h>
 
 using namespace android;
 
@@ -588,6 +591,106 @@
     printf("provides-component:'%s'\n", componentName);
 }
 
+/**
+ * Represents a feature that has been automatically added due to
+ * a pre-requisite or some other reason.
+ */
+struct ImpliedFeature {
+    /**
+     * Name of the implied feature.
+     */
+    String8 name;
+
+    /**
+     * List of human-readable reasons for why this feature was implied.
+     */
+    SortedVector<String8> reasons;
+};
+
+/**
+ * Represents a <feature-group> tag in the AndroidManifest.xml
+ */
+struct FeatureGroup {
+    /**
+     * Human readable label
+     */
+    String8 label;
+
+    /**
+     * Explicit features defined in the group
+     */
+    KeyedVector<String8, bool> features;
+};
+
+static void addImpliedFeature(KeyedVector<String8, ImpliedFeature>* impliedFeatures,
+        const char* name, const char* reason) {
+    String8 name8(name);
+    ssize_t idx = impliedFeatures->indexOfKey(name8);
+    if (idx < 0) {
+        idx = impliedFeatures->add(name8, ImpliedFeature());
+        impliedFeatures->editValueAt(idx).name = name8;
+    }
+    impliedFeatures->editValueAt(idx).reasons.add(String8(reason));
+}
+
+static void printFeatureGroup(const FeatureGroup& grp,
+        const KeyedVector<String8, ImpliedFeature>* impliedFeatures = NULL) {
+    printf("feature-group: label='%s'\n", grp.label.string());
+
+    const size_t numFeatures = grp.features.size();
+    for (size_t i = 0; i < numFeatures; i++) {
+        if (!grp.features[i]) {
+            continue;
+        }
+
+        const String8& featureName = grp.features.keyAt(i);
+        printf("  uses-feature: name='%s'\n",
+                ResTable::normalizeForOutput(featureName.string()).string());
+    }
+
+    const size_t numImpliedFeatures =
+        (impliedFeatures != NULL) ? impliedFeatures->size() : 0;
+    for (size_t i = 0; i < numImpliedFeatures; i++) {
+        const ImpliedFeature& impliedFeature = impliedFeatures->valueAt(i);
+        if (grp.features.indexOfKey(impliedFeature.name) >= 0) {
+            // The feature is explicitly set, no need to use implied
+            // definition.
+            continue;
+        }
+
+        String8 printableFeatureName(ResTable::normalizeForOutput(
+                    impliedFeature.name.string()));
+        printf("  uses-feature: name='%s'\n", printableFeatureName.string());
+        printf("  uses-implied-feature: name='%s' reason='",
+                printableFeatureName.string());
+        const size_t numReasons = impliedFeature.reasons.size();
+        for (size_t j = 0; j < numReasons; j++) {
+            printf("%s", impliedFeature.reasons[j].string());
+            if (j + 2 < numReasons) {
+                printf(", ");
+            } else if (j + 1 < numReasons) {
+                printf(", and ");
+            }
+        }
+        printf("'\n");
+    }
+}
+
+static void addParentFeatures(FeatureGroup* grp, const String8& name) {
+    if (name == "android.hardware.camera.autofocus" ||
+            name == "android.hardware.camera.flash") {
+        grp->features.add(String8("android.hardware.camera"), true);
+    } else if (name == "android.hardware.location.gps" ||
+            name == "android.hardware.location.network") {
+        grp->features.add(String8("android.hardware.location"), true);
+    } else if (name == "android.hardware.touchscreen.multitouch") {
+        grp->features.add(String8("android.hardware.touchscreen"), true);
+    } else if (name == "android.hardware.touchscreen.multitouch.distinct") {
+        grp->features.add(String8("android.hardware.touchscreen.multitouch"), true);
+        grp->features.add(String8("android.hardware.touchscreen"), true);
+    }
+}
+
 /*
  * Handle the "dump" command, to extract select data from an archive.
  */
@@ -797,6 +900,7 @@
             bool isSearchable = false;
             bool withinApplication = false;
             bool withinSupportsInput = false;
+            bool withinFeatureGroup = false;
             bool withinReceiver = false;
             bool withinService = false;
             bool withinProvider = false;
@@ -869,36 +973,7 @@
             // some new uses-feature constants in 2.1 and 2.2. In most cases, the
             // heuristic is "if an app requests a permission but doesn't explicitly
             // request the corresponding <uses-feature>, presume it's there anyway".
-            bool specCameraFeature = false; // camera-related
-            bool specCameraAutofocusFeature = false;
-            bool reqCameraAutofocusFeature = false;
-            bool reqCameraFlashFeature = false;
-            bool hasCameraPermission = false;
-            bool specLocationFeature = false; // location-related
-            bool specNetworkLocFeature = false;
-            bool reqNetworkLocFeature = false;
-            bool specGpsFeature = false;
-            bool reqGpsFeature = false;
-            bool hasMockLocPermission = false;
-            bool hasCoarseLocPermission = false;
-            bool hasGpsPermission = false;
-            bool hasGeneralLocPermission = false;
-            bool specBluetoothFeature = false; // Bluetooth API-related
-            bool hasBluetoothPermission = false;
-            bool specMicrophoneFeature = false; // microphone-related
-            bool hasRecordAudioPermission = false;
-            bool specWiFiFeature = false;
-            bool hasWiFiPermission = false;
-            bool specTelephonyFeature = false; // telephony-related
-            bool reqTelephonySubFeature = false;
-            bool hasTelephonyPermission = false;
-            bool specTouchscreenFeature = false; // touchscreen-related
-            bool specMultitouchFeature = false;
-            bool reqDistinctMultitouchFeature = false;
-            bool specScreenPortraitFeature = false;
-            bool specScreenLandscapeFeature = false;
-            bool reqScreenPortraitFeature = false;
-            bool reqScreenLandscapeFeature = false;
+
             // 2.2 also added some other features that apps can request, but that
             // have no corresponding permission, so we cannot implement any
             // back-compatibility heuristic for them. The below are thus unnecessary
@@ -926,6 +1001,11 @@
             String8 receiverName;
             String8 serviceName;
             Vector<String8> supportedInput;
+
+            FeatureGroup commonFeatures;
+            Vector<FeatureGroup> featureGroups;
+            KeyedVector<String8, ImpliedFeature> impliedFeatures;
+
             while ((code=tree.next()) != ResXMLTree::END_DOCUMENT && code != ResXMLTree::BAD_DOCUMENT) {
                 if (code == ResXMLTree::END_TAG) {
                     depth--;
@@ -946,6 +1026,7 @@
                         }
                         withinApplication = false;
                         withinSupportsInput = false;
+                        withinFeatureGroup = false;
                     } else if (depth < 3) {
                         if (withinActivity && isMainActivity) {
                             String8 aName(getComponentName(pkg, activityName));
@@ -1210,59 +1291,27 @@
                                 COMPATIBLE_WIDTH_LIMIT_DP_ATTR, NULL, 0);
                         largestWidthLimitDp = getIntegerAttribute(tree,
                                 LARGEST_WIDTH_LIMIT_DP_ATTR, NULL, 0);
+                    } else if (tag == "feature-group") {
+                        withinFeatureGroup = true;
+                        FeatureGroup group;
+                        group.label = getResolvedAttribute(&res, tree, LABEL_ATTR, &error);
+                        if (error != "") {
+                            fprintf(stderr, "ERROR getting 'android:label' attribute:"
+                                    " %s\n", error.string());
+                            goto bail;
+                        }
+                        featureGroups.add(group);
+
                     } else if (tag == "uses-feature") {
                         String8 name = getAttribute(tree, NAME_ATTR, &error);
-
                         if (name != "" && error == "") {
                             int req = getIntegerAttribute(tree,
                                     REQUIRED_ATTR, NULL, 1);
 
-                            if (name == "android.hardware.camera") {
-                                specCameraFeature = true;
-                            } else if (name == "android.hardware.camera.autofocus") {
-                                // these have no corresponding permission to check for,
-                                // but should imply the foundational camera permission
-                                reqCameraAutofocusFeature = reqCameraAutofocusFeature || req;
-                                specCameraAutofocusFeature = true;
-                            } else if (req && (name == "android.hardware.camera.flash")) {
-                                // these have no corresponding permission to check for,
-                                // but should imply the foundational camera permission
-                                reqCameraFlashFeature = true;
-                            } else if (name == "android.hardware.location") {
-                                specLocationFeature = true;
-                            } else if (name == "android.hardware.location.network") {
-                                specNetworkLocFeature = true;
-                                reqNetworkLocFeature = reqNetworkLocFeature || req;
-                            } else if (name == "android.hardware.location.gps") {
-                                specGpsFeature = true;
-                                reqGpsFeature = reqGpsFeature || req;
-                            } else if (name == "android.hardware.bluetooth") {
-                                specBluetoothFeature = true;
-                            } else if (name == "android.hardware.touchscreen") {
-                                specTouchscreenFeature = true;
-                            } else if (name == "android.hardware.touchscreen.multitouch") {
-                                specMultitouchFeature = true;
-                            } else if (name == "android.hardware.touchscreen.multitouch.distinct") {
-                                reqDistinctMultitouchFeature = reqDistinctMultitouchFeature || req;
-                            } else if (name == "android.hardware.microphone") {
-                                specMicrophoneFeature = true;
-                            } else if (name == "android.hardware.wifi") {
-                                specWiFiFeature = true;
-                            } else if (name == "android.hardware.telephony") {
-                                specTelephonyFeature = true;
-                            } else if (req && (name == "android.hardware.telephony.gsm" ||
-                                               name == "android.hardware.telephony.cdma")) {
-                                // these have no corresponding permission to check for,
-                                // but should imply the foundational telephony permission
-                                reqTelephonySubFeature = true;
-                            } else if (name == "android.hardware.screen.portrait") {
-                                specScreenPortraitFeature = true;
-                            } else if (name == "android.hardware.screen.landscape") {
-                                specScreenLandscapeFeature = true;
+                            commonFeatures.features.add(name, req);
+                            if (req) {
+                                addParentFeatures(&commonFeatures, name);
                             }
-                            printf("uses-feature%s:'%s'\n",
-                                    req ? "" : "-not-required",
-                                            ResTable::normalizeForOutput(name.string()).string());
                         } else {
                             int vers = getIntegerAttribute(tree,
                                     GL_ES_VERSION_ATTR, &error);
@@ -1274,25 +1323,51 @@
                         String8 name = getAttribute(tree, NAME_ATTR, &error);
                         if (name != "" && error == "") {
                             if (name == "android.permission.CAMERA") {
-                                hasCameraPermission = true;
+                                addImpliedFeature(&impliedFeatures, "android.hardware.feature",
+                                        String8::format("requested %s permission", name.string())
+                                        .string());
                             } else if (name == "android.permission.ACCESS_FINE_LOCATION") {
-                                hasGpsPermission = true;
+                                addImpliedFeature(&impliedFeatures, "android.hardware.location.gps",
+                                        String8::format("requested %s permission", name.string())
+                                        .string());
+                                addImpliedFeature(&impliedFeatures, "android.hardware.location",
+                                        String8::format("requested %s permission", name.string())
+                                        .string());
                             } else if (name == "android.permission.ACCESS_MOCK_LOCATION") {
-                                hasMockLocPermission = true;
+                                addImpliedFeature(&impliedFeatures, "android.hardware.location",
+                                        String8::format("requested %s permission", name.string())
+                                        .string());
                             } else if (name == "android.permission.ACCESS_COARSE_LOCATION") {
-                                hasCoarseLocPermission = true;
+                                addImpliedFeature(&impliedFeatures, "android.hardware.location.network",
+                                        String8::format("requested %s permission", name.string())
+                                        .string());
+                                addImpliedFeature(&impliedFeatures, "android.hardware.location",
+                                        String8::format("requested %s permission", name.string())
+                                        .string());
                             } else if (name == "android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" ||
                                        name == "android.permission.INSTALL_LOCATION_PROVIDER") {
-                                hasGeneralLocPermission = true;
+                                addImpliedFeature(&impliedFeatures, "android.hardware.location",
+                                        String8::format("requested %s permission", name.string())
+                                        .string());
                             } else if (name == "android.permission.BLUETOOTH" ||
                                        name == "android.permission.BLUETOOTH_ADMIN") {
-                                hasBluetoothPermission = true;
+                                if (targetSdk > 4) {
+                                    addImpliedFeature(&impliedFeatures, "android.hardware.bluetooth",
+                                            String8::format("requested %s permission", name.string())
+                                            .string());
+                                    addImpliedFeature(&impliedFeatures, "android.hardware.bluetooth",
+                                            "targetSdkVersion > 4");
+                                }
                             } else if (name == "android.permission.RECORD_AUDIO") {
-                                hasRecordAudioPermission = true;
+                                addImpliedFeature(&impliedFeatures, "android.hardware.microphone",
+                                        String8::format("requested %s permission", name.string())
+                                        .string());
                             } else if (name == "android.permission.ACCESS_WIFI_STATE" ||
                                        name == "android.permission.CHANGE_WIFI_STATE" ||
                                        name == "android.permission.CHANGE_WIFI_MULTICAST_STATE") {
-                                hasWiFiPermission = true;
+                                addImpliedFeature(&impliedFeatures, "android.hardware.wifi",
+                                        String8::format("requested %s permission", name.string())
+                                        .string());
                             } else if (name == "android.permission.CALL_PHONE" ||
                                        name == "android.permission.CALL_PRIVILEGED" ||
                                        name == "android.permission.MODIFY_PHONE_STATE" ||
@@ -1304,7 +1379,8 @@
                                        name == "android.permission.SEND_SMS" ||
                                        name == "android.permission.WRITE_APN_SETTINGS" ||
                                        name == "android.permission.WRITE_SMS") {
-                                hasTelephonyPermission = true;
+                                addImpliedFeature(&impliedFeatures, "android.hardware.telephony",
+                                        String8("requested a telephony permission").string());
                             } else if (name == "android.permission.WRITE_EXTERNAL_STORAGE") {
                                 hasWriteExternalStoragePermission = true;
                             } else if (name == "android.permission.READ_EXTERNAL_STORAGE") {
@@ -1430,10 +1506,12 @@
                             if (error == "") {
                                 if (orien == 0 || orien == 6 || orien == 8) {
                                     // Requests landscape, sensorLandscape, or reverseLandscape.
-                                    reqScreenLandscapeFeature = true;
+                                    addImpliedFeature(&impliedFeatures, "android.hardware.screen.landscape",
+                                            "one or more activities have specified a landscape orientation");
                                 } else if (orien == 1 || orien == 7 || orien == 9) {
                                     // Requests portrait, sensorPortrait, or reversePortrait.
-                                    reqScreenPortraitFeature = true;
+                                    addImpliedFeature(&impliedFeatures, "android.hardware.screen.portrait",
+                                            "one or more activities have specified a portrait orientation");
                                 }
                             }
                         } else if (tag == "uses-library") {
@@ -1560,6 +1638,20 @@
                                 goto bail;
                             }
                         }
+                    } else if (withinFeatureGroup && tag == "uses-feature") {
+                        String8 name = getResolvedAttribute(&res, tree, NAME_ATTR, &error);
+                        if (error != "") {
+                            fprintf(stderr, "ERROR getting 'android:name' attribute: %s\n",
+                                    error.string());
+                            goto bail;
+                        }
+
+                        int required = getIntegerAttribute(tree, REQUIRED_ATTR, NULL, 1);
+                        FeatureGroup& top = featureGroups.editTop();
+                        top.features.add(name, required);
+                        if (required) {
+                            addParentFeatures(&top, name);
+                        }
                     }
                 } else if (depth == 4) {
                     if (tag == "intent-filter") {
@@ -1734,137 +1826,34 @@
                 }
             }
 
-            /* The following blocks handle printing "inferred" uses-features, based
-             * on whether related features or permissions are used by the app.
-             * Note that the various spec*Feature variables denote whether the
-             * relevant tag was *present* in the AndroidManfest, not that it was
-             * present and set to true.
-             */
-            // Camera-related back-compatibility logic
-            if (!specCameraFeature) {
-                if (reqCameraFlashFeature) {
-                    // if app requested a sub-feature (autofocus or flash) and didn't
-                    // request the base camera feature, we infer that it meant to
-                    printf("uses-feature:'android.hardware.camera'\n");
-                    printf("uses-implied-feature:'android.hardware.camera'," \
-                            "'requested android.hardware.camera.flash feature'\n");
-                } else if (reqCameraAutofocusFeature) {
-                    // if app requested a sub-feature (autofocus or flash) and didn't
-                    // request the base camera feature, we infer that it meant to
-                    printf("uses-feature:'android.hardware.camera'\n");
-                    printf("uses-implied-feature:'android.hardware.camera'," \
-                            "'requested android.hardware.camera.autofocus feature'\n");
-                } else if (hasCameraPermission) {
-                    // if app wants to use camera but didn't request the feature, we infer
-                    // that it meant to, and further that it wants autofocus
-                    // (which was the 1.0 - 1.5 behavior)
-                    printf("uses-feature:'android.hardware.camera'\n");
-                    if (!specCameraAutofocusFeature) {
-                        printf("uses-feature:'android.hardware.camera.autofocus'\n");
-                        printf("uses-implied-feature:'android.hardware.camera.autofocus'," \
-                                "'requested android.permission.CAMERA permission'\n");
+            addImpliedFeature(&impliedFeatures, "android.hardware.touchscreen",
+                    "default feature for all apps");
+
+            const size_t numFeatureGroups = featureGroups.size();
+            if (numFeatureGroups == 0) {
+                // If no <feature-group> tags were defined, apply auto-implied features.
+                printFeatureGroup(commonFeatures, &impliedFeatures);
+
+            } else {
+                // <feature-group> tags are defined, so we ignore implied features and
+                for (size_t i = 0; i < numFeatureGroups; i++) {
+                    FeatureGroup& grp = featureGroups.editItemAt(i);
+
+                    // Merge the features defined in the top level (not inside a <feature-group>)
+                    // with this feature group.
+                    const size_t numCommonFeatures = commonFeatures.features.size();
+                    for (size_t j = 0; j < numCommonFeatures; j++) {
+                        if (grp.features.indexOfKey(commonFeatures.features.keyAt(j)) < 0) {
+                            grp.features.add(commonFeatures.features.keyAt(j), commonFeatures.features[j]);
+                        }
+                    }
+
+                   if (!grp.features.isEmpty()) {
+                        printFeatureGroup(grp);
                     }
                 }
             }
 
-            // Location-related back-compatibility logic
-            if (!specLocationFeature &&
-                (hasMockLocPermission || hasCoarseLocPermission || hasGpsPermission ||
-                 hasGeneralLocPermission || reqNetworkLocFeature || reqGpsFeature)) {
-                // if app either takes a location-related permission or requests one of the
-                // sub-features, we infer that it also meant to request the base location feature
-                printf("uses-feature:'android.hardware.location'\n");
-                printf("uses-implied-feature:'android.hardware.location'," \
-                        "'requested a location access permission'\n");
-            }
-            if (!specGpsFeature && hasGpsPermission) {
-                // if app takes GPS (FINE location) perm but does not request the GPS
-                // feature, we infer that it meant to
-                printf("uses-feature:'android.hardware.location.gps'\n");
-                printf("uses-implied-feature:'android.hardware.location.gps'," \
-                        "'requested android.permission.ACCESS_FINE_LOCATION permission'\n");
-            }
-            if (!specNetworkLocFeature && hasCoarseLocPermission) {
-                // if app takes Network location (COARSE location) perm but does not request the
-                // network location feature, we infer that it meant to
-                printf("uses-feature:'android.hardware.location.network'\n");
-                printf("uses-implied-feature:'android.hardware.location.network'," \
-                        "'requested android.permission.ACCESS_COARSE_LOCATION permission'\n");
-            }
-
-            // Bluetooth-related compatibility logic
-            if (!specBluetoothFeature && hasBluetoothPermission && (targetSdk > 4)) {
-                // if app takes a Bluetooth permission but does not request the Bluetooth
-                // feature, we infer that it meant to
-                printf("uses-feature:'android.hardware.bluetooth'\n");
-                printf("uses-implied-feature:'android.hardware.bluetooth'," \
-                        "'requested android.permission.BLUETOOTH or android.permission.BLUETOOTH_ADMIN " \
-                        "permission and targetSdkVersion > 4'\n");
-            }
-
-            // Microphone-related compatibility logic
-            if (!specMicrophoneFeature && hasRecordAudioPermission) {
-                // if app takes the record-audio permission but does not request the microphone
-                // feature, we infer that it meant to
-                printf("uses-feature:'android.hardware.microphone'\n");
-                printf("uses-implied-feature:'android.hardware.microphone'," \
-                        "'requested android.permission.RECORD_AUDIO permission'\n");
-            }
-
-            // WiFi-related compatibility logic
-            if (!specWiFiFeature && hasWiFiPermission) {
-                // if app takes one of the WiFi permissions but does not request the WiFi
-                // feature, we infer that it meant to
-                printf("uses-feature:'android.hardware.wifi'\n");
-                printf("uses-implied-feature:'android.hardware.wifi'," \
-                        "'requested android.permission.ACCESS_WIFI_STATE, " \
-                        "android.permission.CHANGE_WIFI_STATE, or " \
-                        "android.permission.CHANGE_WIFI_MULTICAST_STATE permission'\n");
-            }
-
-            // Telephony-related compatibility logic
-            if (!specTelephonyFeature && (hasTelephonyPermission || reqTelephonySubFeature)) {
-                // if app takes one of the telephony permissions or requests a sub-feature but
-                // does not request the base telephony feature, we infer that it meant to
-                printf("uses-feature:'android.hardware.telephony'\n");
-                printf("uses-implied-feature:'android.hardware.telephony'," \
-                        "'requested a telephony-related permission or feature'\n");
-            }
-
-            // Touchscreen-related back-compatibility logic
-            if (!specTouchscreenFeature) { // not a typo!
-                // all apps are presumed to require a touchscreen, unless they explicitly say
-                // <uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
-                // Note that specTouchscreenFeature is true if the tag is present, regardless
-                // of whether its value is true or false, so this is safe
-                printf("uses-feature:'android.hardware.touchscreen'\n");
-                printf("uses-implied-feature:'android.hardware.touchscreen'," \
-                        "'assumed you require a touch screen unless explicitly made optional'\n");
-            }
-            if (!specMultitouchFeature && reqDistinctMultitouchFeature) {
-                // if app takes one of the telephony permissions or requests a sub-feature but
-                // does not request the base telephony feature, we infer that it meant to
-                printf("uses-feature:'android.hardware.touchscreen.multitouch'\n");
-                printf("uses-implied-feature:'android.hardware.touchscreen.multitouch'," \
-                        "'requested android.hardware.touchscreen.multitouch.distinct feature'\n");
-            }
-
-            // Landscape/portrait-related compatibility logic
-            if (!specScreenLandscapeFeature && !specScreenPortraitFeature) {
-                // If the app has specified any activities in its manifest
-                // that request a specific orientation, then assume that
-                // orientation is required.
-                if (reqScreenLandscapeFeature) {
-                    printf("uses-feature:'android.hardware.screen.landscape'\n");
-                    printf("uses-implied-feature:'android.hardware.screen.landscape'," \
-                            "'one or more activities have specified a landscape orientation'\n");
-                }
-                if (reqScreenPortraitFeature) {
-                    printf("uses-feature:'android.hardware.screen.portrait'\n");
-                    printf("uses-implied-feature:'android.hardware.screen.portrait'," \
-                            "'one or more activities have specified a portrait orientation'\n");
-                }
-            }
 
             if (hasWidgetReceivers) {
                 printComponentPresence("app-widget");