Add fallback packages to be enabled iff no webview packages are valid

This patch makes it possible to declare a WebView package as a fallback
which means that the package will be enabled iff there exist no other
valid and enabled (and available-by-default) webview packages.

The enabled-state of a fallback package is updated at boot and if a
webview package is changed (it it's been up/downgraded or has had its
enabled-state changed).

This patch also adds 'webviewupdate' shell commands for enabling and
disabling this mechanism.

Bug: 26375524, 26375860
Change-Id: I151915e5d6d932697dab10aeb593687e6b9c817e
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 0074788..5a92fc4 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -7033,6 +7033,14 @@
                 "webview_data_reduction_proxy_key";
 
         /**
+         * Whether or not the WebView fallback mechanism should be enabled.
+         * 0=disabled, 1=enabled.
+         * @hide
+         */
+        public static final String WEBVIEW_FALLBACK_LOGIC_ENABLED =
+                "webview_fallback_logic_enabled";
+
+        /**
          * Name of the package used as WebView provider (if unset the provider is instead determined
          * by the system).
          * @hide
diff --git a/core/java/android/webkit/IWebViewUpdateService.aidl b/core/java/android/webkit/IWebViewUpdateService.aidl
index 89d5d69..5697dfc 100644
--- a/core/java/android/webkit/IWebViewUpdateService.aidl
+++ b/core/java/android/webkit/IWebViewUpdateService.aidl
@@ -38,10 +38,14 @@
     WebViewProviderResponse waitForAndGetProvider();
 
     /**
-     * DevelopmentSettings uses this to notify WebViewUpdateService that a
-     * new provider has been selected by the user.
+     * DevelopmentSettings uses this to notify WebViewUpdateService that a new provider has been
+     * selected by the user. Returns the provider we end up switching to, this could be different to
+     * the one passed as argument to this method since the Dev Setting calling this method could be
+     * stale. I.e. the Dev setting could be letting the user choose uninstalled/disabled packages,
+     * it would then try to update the provider to such a package while in reality the update
+     * service would switch to another one.
      */
-    void changeProviderAndSetting(String newProvider);
+    String changeProviderAndSetting(String newProvider);
 
     /**
      * DevelopmentSettings uses this to get the current available WebView
@@ -53,4 +57,15 @@
      * Used by DevelopmentSetting to get the name of the WebView provider currently in use.
      */
     String getCurrentWebViewPackageName();
+
+    /**
+     * Used by Settings to determine whether a certain package can be enabled/disabled by the user -
+     * the package should not be modifiable in this way if it is a fallback package.
+     */
+    boolean isFallbackPackage(String packageName);
+
+    /**
+     * Enable or disable the WebView package fallback mechanism.
+     */
+    void enableFallbackLogic(boolean enable);
 }
diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java
index b04b4c0..ad50ff6 100644
--- a/core/java/android/webkit/WebViewFactory.java
+++ b/core/java/android/webkit/WebViewFactory.java
@@ -21,6 +21,7 @@
 import android.app.AppGlobals;
 import android.app.Application;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
@@ -134,6 +135,7 @@
     // Whether or not the provider must be explicitly chosen by the user to be used.
     private static String TAG_AVAILABILITY = "availableByDefault";
     private static String TAG_SIGNATURE = "signature";
+    private static String TAG_FALLBACK = "isFallback";
 
     /**
      * Reads all signatures at the current depth (within the current provider) from the XML parser.
@@ -159,6 +161,7 @@
      * @hide
      * */
     public static WebViewProviderInfo[] getWebViewPackages() {
+        int numFallbackPackages = 0;
         XmlResourceParser parser = null;
         List<WebViewProviderInfo> webViewProviders = new ArrayList<WebViewProviderInfo>();
         try {
@@ -182,13 +185,21 @@
                         throw new MissingWebViewPackageException(
                                 "WebView provider in framework resources missing description");
                     }
-                    String availableByDefault = parser.getAttributeValue(null, TAG_AVAILABILITY);
-                    if (availableByDefault == null) {
-                        availableByDefault = "false";
-                    }
-                    webViewProviders.add(
+                    boolean availableByDefault = "true".equals(
+                            parser.getAttributeValue(null, TAG_AVAILABILITY));
+                    boolean isFallback = "true".equals(
+                            parser.getAttributeValue(null, TAG_FALLBACK));
+                    WebViewProviderInfo currentProvider =
                             new WebViewProviderInfo(packageName, description, availableByDefault,
-                                readSignatures(parser)));
+                                isFallback, readSignatures(parser));
+                    if (currentProvider.isFallbackPackage()) {
+                        numFallbackPackages++;
+                        if (numFallbackPackages > 1) {
+                            throw new AndroidRuntimeException(
+                                    "There can be at most one webview fallback package.");
+                        }
+                    }
+                    webViewProviders.add(currentProvider);
                 }
                 else {
                     Log.e(LOGTAG, "Found an element that is not a webview provider");
@@ -641,6 +652,18 @@
         return result;
     }
 
+    /**
+     * Returns whether the entire package from an ACTION_PACKAGE_CHANGED intent was changed (rather
+     * than just one of its components).
+     * @hide
+     */
+    public static boolean entirePackageChanged(Intent intent) {
+        String[] componentList =
+            intent.getStringArrayExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST);
+        return Arrays.asList(componentList).contains(
+                intent.getDataString().substring("package:".length()));
+    }
+
     private static IWebViewUpdateService getUpdateService() {
         return IWebViewUpdateService.Stub.asInterface(ServiceManager.getService("webviewupdate"));
     }
diff --git a/core/java/android/webkit/WebViewProviderInfo.java b/core/java/android/webkit/WebViewProviderInfo.java
index 94e8b70..64c2caa 100644
--- a/core/java/android/webkit/WebViewProviderInfo.java
+++ b/core/java/android/webkit/WebViewProviderInfo.java
@@ -40,11 +40,12 @@
         public WebViewPackageNotFoundException(Exception e) { super(e); }
     }
 
-    public WebViewProviderInfo(String packageName, String description, String availableByDefault,
-            String[] signatures) {
+    public WebViewProviderInfo(String packageName, String description, boolean availableByDefault,
+            boolean isFallback, String[] signatures) {
         this.packageName = packageName;
         this.description = description;
-        this.availableByDefault = availableByDefault.equals("true");
+        this.availableByDefault = availableByDefault;
+        this.isFallback = isFallback;
         this.signatures = signatures;
     }
 
@@ -114,6 +115,10 @@
         return availableByDefault;
     }
 
+    public boolean isFallbackPackage() {
+        return isFallback;
+    }
+
     private void updatePackageInfo() {
         try {
             PackageManager pm = AppGlobals.getInitialApplication().getPackageManager();
@@ -165,6 +170,7 @@
     public String packageName;
     public String description;
     private boolean availableByDefault;
+    private boolean isFallback;
 
     private String[] signatures;
 
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 3c03a4ad..b36b279 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -682,8 +682,8 @@
     <string name="select_webview_provider_title">WebView implementation</string>
     <!-- Developer settings: select WebView provider dialog title [CHAR LIMIT=30] -->
     <string name="select_webview_provider_dialog_title">Set WebView implementation</string>
-    <!-- Developer settings: confirmation dialog text for the WebView provider selection dialog [CHAR LIMIT=NONE] -->
-    <string name="select_webview_provider_confirmation_text">The chosen WebView implementation is disabled, and must be enabled to be used, do you wish to enable it?</string>
+    <!-- Developer settings: text for the WebView provider selection toast shown if an invalid provider was chosen (i.e. the setting list was stale). [CHAR LIMIT=NONE] -->
+    <string name="select_webview_provider_toast_text">The chosen WebView implementation is invalid because the list of implementation choices grew stale. The list should now be updated.</string>
 
     <!-- Developer settings screen, convert userdata to file encryption option name -->
     <string name="convert_to_file_encryption">Convert to file encryption</string>
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateService.java b/services/core/java/com/android/server/webkit/WebViewUpdateService.java
index f3b120f..1a8f506 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateService.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateService.java
@@ -23,13 +23,18 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageDeleteObserver;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.Signature;
+import android.content.pm.UserInfo;
 import android.os.Binder;
+import android.os.PatternMatcher;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.provider.Settings;
 import android.provider.Settings.Global;
 import android.util.AndroidRuntimeException;
@@ -41,6 +46,7 @@
 
 import com.android.server.SystemService;
 
+import java.io.FileDescriptor;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Iterator;
@@ -102,6 +108,27 @@
                         return;
                     }
 
+                    // Ensure that we only heed PACKAGE_CHANGED intents if they change an entire
+                    // package, not just a component
+                    if (intent.getAction().equals(Intent.ACTION_PACKAGE_CHANGED)) {
+                        if (!WebViewFactory.entirePackageChanged(intent)) {
+                            return;
+                        }
+                    }
+
+                    if (intent.getAction().equals(Intent.ACTION_USER_ADDED)) {
+                        int userId =
+                            intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
+                        handleNewUser(userId);
+                        return;
+                    }
+
+                    updateFallbackState(context, intent);
+
+                    // TODO(gsennton) for now don't update WebView on PACKAGE_CHANGED as this will
+                    // change the current behaviour even more, instead do this in a follow-up.
+                    if (intent.getAction().equals(Intent.ACTION_PACKAGE_CHANGED)) return;
+
                     for (WebViewProviderInfo provider : WebViewFactory.getWebViewPackages()) {
                         String webviewPackage = "package:" + provider.packageName;
 
@@ -154,12 +181,167 @@
         IntentFilter filter = new IntentFilter();
         filter.addAction(Intent.ACTION_PACKAGE_ADDED);
         filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
         filter.addDataScheme("package");
+        // Make sure we only receive intents for WebView packages from our config file.
+        for (WebViewProviderInfo provider : WebViewFactory.getWebViewPackages()) {
+            filter.addDataSchemeSpecificPart(provider.packageName, PatternMatcher.PATTERN_LITERAL);
+        }
         getContext().registerReceiver(mWebViewUpdatedReceiver, filter);
 
+        IntentFilter userAddedFilter = new IntentFilter();
+        userAddedFilter.addAction(Intent.ACTION_USER_ADDED);
+        getContext().registerReceiver(mWebViewUpdatedReceiver, userAddedFilter);
+
         publishBinderService("webviewupdate", new BinderService());
     }
 
+    private static boolean existsValidNonFallbackProvider(WebViewProviderInfo[] providers) {
+        for (WebViewProviderInfo provider : providers) {
+            if (provider.isAvailableByDefault() && provider.isEnabled()
+                    && provider.isValidProvider() && !provider.isFallbackPackage()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static void enablePackageForUser(String packageName, boolean enable, int userId) {
+        try {
+            AppGlobals.getPackageManager().setApplicationEnabledSetting(
+                    packageName,
+                    enable ? PackageManager.COMPONENT_ENABLED_STATE_DEFAULT :
+                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0,
+                    userId, null);
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Tried to disable " + packageName + " for user " + userId + ": " + e);
+        }
+    }
+
+    /**
+     * Called when a new user has been added to update the state of its fallback package.
+     */
+    void handleNewUser(int userId) {
+        if (!isFallbackLogicEnabled()) return;
+
+        WebViewProviderInfo[] webviewProviders = WebViewFactory.getWebViewPackages();
+        WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewProviders);
+        if (fallbackProvider == null) return;
+        boolean existsValidNonFallbackProvider =
+            existsValidNonFallbackProvider(webviewProviders);
+
+        enablePackageForUser(fallbackProvider.packageName, !existsValidNonFallbackProvider,
+                userId);
+    }
+
+    /**
+     * Handle the enabled-state of our fallback package, i.e. if there exists some non-fallback
+     * package that is valid (and available by default) then disable the fallback package,
+     * otherwise, enable the fallback package.
+     */
+    void updateFallbackState(final Context context, final Intent intent) {
+        if (!isFallbackLogicEnabled()) return;
+
+        WebViewProviderInfo[] webviewProviders = WebViewFactory.getWebViewPackages();
+
+        if (intent != null && (intent.getAction().equals(Intent.ACTION_PACKAGE_ADDED)
+                    || intent.getAction().equals(Intent.ACTION_PACKAGE_CHANGED))) {
+            // A package was changed / updated / downgraded, early out if it is not one of the
+            // webview packages that are available by default.
+            String changedPackage = null;
+            for (WebViewProviderInfo provider : webviewProviders) {
+                String webviewPackage = "package:" + provider.packageName;
+                if (webviewPackage.equals(intent.getDataString())) {
+                    if (provider.isAvailableByDefault()) {
+                        changedPackage = provider.packageName;
+                    }
+                    break;
+                }
+            }
+            if (changedPackage == null) return;
+        }
+
+        // If there exists a valid and enabled non-fallback package - disable the fallback
+        // package, otherwise, enable it.
+        WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewProviders);
+        if (fallbackProvider == null) return;
+        boolean existsValidNonFallbackProvider = existsValidNonFallbackProvider(webviewProviders);
+
+        if (existsValidNonFallbackProvider
+                // During an OTA the primary user's WebView state might differ from other users', so
+                // ignore the state of that user during boot.
+                && (fallbackProvider.isEnabled() || intent == null)) {
+            // Uninstall and disable fallback package for all users.
+            context.getPackageManager().deletePackage(fallbackProvider.packageName,
+                    new IPackageDeleteObserver.Stub() {
+                public void packageDeleted(String packageName, int returnCode) {
+                    // Ignore returnCode since the deletion could fail, e.g. we might be trying
+                    // to delete a non-updated system-package (and we should still disable the
+                    // package)
+                    UserManager userManager =
+                        (UserManager)context.getSystemService(Context.USER_SERVICE);
+                    // Disable the fallback package for all users.
+                    for(UserInfo userInfo : userManager.getUsers()) {
+                        enablePackageForUser(packageName, false, userInfo.id);
+                    }
+                }
+            }, PackageManager.DELETE_SYSTEM_APP | PackageManager.DELETE_ALL_USERS);
+        } else if (!existsValidNonFallbackProvider
+                // During an OTA the primary user's WebView state might differ from other users', so
+                // ignore the state of that user during boot.
+                && (!fallbackProvider.isEnabled() || intent==null)) {
+            // Enable the fallback package for all users.
+            UserManager userManager =
+                (UserManager)context.getSystemService(Context.USER_SERVICE);
+            for(UserInfo userInfo : userManager.getUsers()) {
+                enablePackageForUser(fallbackProvider.packageName, true, userInfo.id);
+            }
+        }
+    }
+
+    private static boolean isFallbackLogicEnabled() {
+        // Note that this is enabled by default (i.e. if the setting hasn't been set).
+        return Settings.Global.getInt(AppGlobals.getInitialApplication().getContentResolver(),
+                Settings.Global.WEBVIEW_FALLBACK_LOGIC_ENABLED, 1) == 1;
+    }
+
+    private static void enableFallbackLogic(boolean enable) {
+        Settings.Global.putInt(AppGlobals.getInitialApplication().getContentResolver(),
+                Settings.Global.WEBVIEW_FALLBACK_LOGIC_ENABLED, enable ? 1 : 0);
+    }
+
+    /**
+     * Returns the only fallback provider, or null if there is none.
+     */
+    private static WebViewProviderInfo getFallbackProvider(WebViewProviderInfo[] webviewPackages) {
+        for (WebViewProviderInfo provider : webviewPackages) {
+            if (provider.isFallbackPackage()) {
+                return provider;
+            }
+        }
+        return null;
+    }
+
+    private static boolean containsAvailableNonFallbackProvider(
+            WebViewProviderInfo[] webviewPackages) {
+        for (WebViewProviderInfo provider : webviewPackages) {
+            if (provider.isAvailableByDefault() && provider.isEnabled()
+                    && provider.isValidProvider() && !provider.isFallbackPackage()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean isFallbackPackage(String packageName) {
+        if (packageName == null || !isFallbackLogicEnabled()) return false;
+
+        WebViewProviderInfo[] webviewPackages = WebViewFactory.getWebViewPackages();
+        WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewPackages);
+        return (fallbackProvider != null
+                && packageName.equals(fallbackProvider.packageName));
+    }
+
     /**
      * Perform any WebView loading preparations that must happen at boot from the system server,
      * after the package manager has started or after an update to the webview is installed.
@@ -167,6 +349,7 @@
      * Currently, this means spawning the child processes which will create the relro files.
      */
     public void prepareWebViewInSystemServer() {
+        updateFallbackState(getContext(), null);
         try {
             synchronized(this) {
                 updateValidWebViewPackages();
@@ -182,8 +365,10 @@
 
     /**
      * Change WebView provider and provider setting and kill packages using the old provider.
+     * Return the new provider (in case we are in the middle of creating relro files this new
+     * provider will not be in use directly, but will when the relros are done).
      */
-    private void changeProviderAndSetting(String newProviderName) {
+    private String changeProviderAndSetting(String newProviderName) {
         PackageInfo oldPackage = null;
         PackageInfo newPackage = null;
         synchronized(this) {
@@ -195,14 +380,14 @@
                 if (oldPackage != null && newPackage.packageName.equals(oldPackage.packageName)) {
                     // If we don't perform the user change, revert the settings change.
                     updateUserSetting(newPackage.packageName);
-                    return;
+                    return newPackage.packageName;
                 }
             } catch (WebViewFactory.MissingWebViewPackageException e) {
                 Slog.e(TAG, "Tried to change WebView provider but failed to fetch WebView package "
                         + e);
                 // If we don't perform the user change but don't have an installed WebView package,
                 // we will have changed the setting and it will be used when a package is available.
-                return;
+                return newProviderName;
             }
             onWebViewProviderChanged(newPackage);
         }
@@ -214,7 +399,7 @@
             }
         } catch (RemoteException e) {
         }
-        return;
+        return newPackage.packageName;
     }
 
     /**
@@ -349,6 +534,14 @@
 
     private class BinderService extends IWebViewUpdateService.Stub {
 
+        @Override
+        public void onShellCommand(FileDescriptor in, FileDescriptor out,
+                FileDescriptor err, String[] args, ResultReceiver resultReceiver) {
+            (new WebViewUpdateServiceShellCommand(this)).exec(
+                    this, in, out, err, args, resultReceiver);
+        }
+
+
         /**
          * The shared relro process calls this to notify us that it's done trying to create a relro
          * file. This method gets called even if the relro creation has failed or the process
@@ -423,7 +616,7 @@
          * This is called from DeveloperSettings when the user changes WebView provider.
          */
         @Override // Binder call
-        public void changeProviderAndSetting(String newProvider) {
+        public String changeProviderAndSetting(String newProvider) {
             if (getContext().checkCallingPermission(
                         android.Manifest.permission.WRITE_SECURE_SETTINGS)
                     != PackageManager.PERMISSION_GRANTED) {
@@ -435,7 +628,7 @@
                 throw new SecurityException(msg);
             }
 
-            WebViewUpdateService.this.changeProviderAndSetting(newProvider);
+            return WebViewUpdateService.this.changeProviderAndSetting(newProvider);
         }
 
         @Override // Binder call
@@ -453,5 +646,26 @@
                 return WebViewUpdateService.this.mCurrentWebViewPackage.packageName;
             }
         }
+
+        @Override // Binder call
+        public boolean isFallbackPackage(String packageName) {
+            return WebViewUpdateService.isFallbackPackage(packageName);
+        }
+
+        @Override // Binder call
+        public void enableFallbackLogic(boolean enable) {
+            if (getContext().checkCallingPermission(
+                        android.Manifest.permission.WRITE_SECURE_SETTINGS)
+                    != PackageManager.PERMISSION_GRANTED) {
+                String msg = "Permission Denial: enableFallbackLogic() from pid="
+                        + Binder.getCallingPid()
+                        + ", uid=" + Binder.getCallingUid()
+                        + " requires " + android.Manifest.permission.WRITE_SECURE_SETTINGS;
+                Slog.w(TAG, msg);
+                throw new SecurityException(msg);
+            }
+
+            WebViewUpdateService.enableFallbackLogic(enable);
+        }
     }
 }
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceShellCommand.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceShellCommand.java
new file mode 100644
index 0000000..a9461e8
--- /dev/null
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceShellCommand.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+package com.android.server.webkit;
+
+import android.os.RemoteException;
+import android.os.ShellCommand;
+import android.webkit.IWebViewUpdateService;
+
+import java.io.PrintWriter;
+
+class WebViewUpdateServiceShellCommand extends ShellCommand {
+    final IWebViewUpdateService mInterface;
+
+    WebViewUpdateServiceShellCommand(IWebViewUpdateService service) {
+        mInterface = service;
+    }
+
+    @Override
+    public int onCommand(String cmd) {
+        if (cmd == null) {
+            return handleDefaultCommands(cmd);
+        }
+
+        final PrintWriter pw = getOutPrintWriter();
+        try {
+            // TODO(gsennton) add command for changing WebView provider
+            switch(cmd) {
+                case "enable-redundant-packages":
+                    return enableFallbackLogic(false);
+                case "disable-redundant-packages":
+                    return enableFallbackLogic(true);
+                default:
+                    return handleDefaultCommands(cmd);
+            }
+        } catch (RemoteException e) {
+            pw.println("Remote exception: " + e);
+        }
+        return -1;
+    }
+
+    private int enableFallbackLogic(boolean enable) throws RemoteException {
+        final PrintWriter pw = getOutPrintWriter();
+        mInterface.enableFallbackLogic(enable);
+        pw.println("Success");
+        return 0;
+    }
+
+    @Override
+    public void onHelp() {
+        PrintWriter pw = getOutPrintWriter();
+        pw.println("WebView updater commands:");
+        pw.println("  help");
+        pw.println("    Print this help text.");
+        pw.println("");
+        pw.println("  enable-redundant-packages");
+        pw.println("    Allow a fallback package to be installed and enabled even when a");
+        pw.println("    more-preferred package is available. This command is useful when testing");
+        pw.println("    fallback packages.");
+        pw.println("  disable-redundant-packages");
+        pw.println("    Disallow installing and enabling fallback packages when a more-preferred");
+        pw.println("    package is available.");
+        pw.println();
+    }
+}