From: Ryan Archer <ryan.bradley.archer@gmail.com>
Date: Wed, 2 Aug 2017 01:41:28 -0400
Subject: Add an always-incognito mode

More specifically, add a preference that causes all new tabs and all
clicked links to launch as incognito.
---
 .../android/java/res/xml/privacy_preferences.xml   |  5 ++
 .../browser/AlwaysIncognitoLinkInterceptor.java    | 73 ++++++++++++++++++++++
 .../chromium/chrome/browser/ChromeActivity.java    |  5 ++
 .../chrome/browser/appmenu/AppMenuHandler.java     | 12 ++++
 .../contextmenu/ChromeContextMenuPopulator.java    |  9 ++-
 .../chrome/browser/tabmodel/ChromeTabCreator.java  | 24 ++++++-
 .../java/strings/android_chrome_strings.grd        |  6 ++
 chrome/android/java_sources.gni                    |  1 +
 8 files changed, 133 insertions(+), 2 deletions(-)
 create mode 100644 chrome/android/java/src/org/chromium/chrome/browser/AlwaysIncognitoLinkInterceptor.java

diff --git a/chrome/android/java/res/xml/privacy_preferences.xml b/chrome/android/java/res/xml/privacy_preferences.xml
--- a/chrome/android/java/res/xml/privacy_preferences.xml
+++ b/chrome/android/java/res/xml/privacy_preferences.xml
@@ -39,6 +39,11 @@
         android:key="usage_and_crash_reports"
         android:title="@string/usage_and_crash_reports_title_legacy"
         android:fragment="org.chromium.chrome.browser.preferences.privacy.UsageAndCrashReportsPreferenceFragment" />
+    <org.chromium.chrome.browser.preferences.ChromeBaseCheckBoxPreference
+        android:key="always_incognito"
+        android:title="@string/always_incognito_title"
+        android:summary="@string/always_incognito_summary"
+        android:defaultValue="false" />
     <Preference
         android:fragment="org.chromium.chrome.browser.preferences.privacy.DoNotTrackPreference"
         android:key="do_not_track"
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/AlwaysIncognitoLinkInterceptor.java b/chrome/android/java/src/org/chromium/chrome/browser/AlwaysIncognitoLinkInterceptor.java
new file mode 100644
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/AlwaysIncognitoLinkInterceptor.java
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.chromium.chrome.browser;
+
+import android.content.SharedPreferences;
+
+import org.chromium.chrome.browser.tab.EmptyTabObserver;
+import org.chromium.chrome.browser.tab.Tab;
+import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
+import org.chromium.chrome.browser.tabmodel.TabLaunchType;
+import org.chromium.chrome.browser.tabmodel.TabModel;
+import org.chromium.content_public.browser.LoadUrlParams;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+/**
+ * A {@link TabObserver} that implements the always-incognito preference behavior for links. When the preference is set
+ * to true, it intercepts links opened within observed {@link Tab}s and opens them in new incognito <code>Tab</code>s instead.
+ */
+public class AlwaysIncognitoLinkInterceptor extends EmptyTabObserver {
+
+    private static final String PREF_ALWAYS_INCOGNITO = "always_incognito";
+
+    private final SharedPreferences alwaysIncognitoContainer;
+    private final Map<Tab, String> lastUrls;
+    private final Set<Tab> revertingTabs;
+
+    public AlwaysIncognitoLinkInterceptor(final SharedPreferences alwaysIncognitoContainer) {
+
+        assert alwaysIncognitoContainer != null;
+
+        this.alwaysIncognitoContainer = alwaysIncognitoContainer;
+        lastUrls = new HashMap<Tab, String>();
+        revertingTabs = new HashSet<Tab>();
+    }
+
+    @Override
+    public void onDestroyed(final Tab tab)
+    {
+        lastUrls.remove(tab);
+        revertingTabs.remove(tab);
+    }
+
+    @Override
+    public void onUpdateUrl(final Tab tab, final String url) {
+
+        if (tab == null) return;
+        if (url == null) return;
+        if (tab.isIncognito()) return;
+        if (alwaysIncognitoContainer == null) return;
+
+        final String lastUrl = lastUrls.put(tab, url);
+
+        if (!alwaysIncognitoContainer.getBoolean(PREF_ALWAYS_INCOGNITO, false)) return;
+        if (revertingTabs.contains(tab)) {
+            revertingTabs.remove(tab);
+            return;
+        }
+
+        tab.getActivity().getTabCreator(true).createNewTab(new LoadUrlParams(url), TabLaunchType.FROM_LINK, tab);
+
+        if ((url.equals(lastUrl)) || (!tab.canGoBack())) {
+            // this call was triggered by a reload
+        } else {
+            revertingTabs.add(tab);
+            tab.goBack();
+        }
+    }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ChromeActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/ChromeActivity.java
--- a/chrome/android/java/src/org/chromium/chrome/browser/ChromeActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ChromeActivity.java
@@ -217,6 +217,8 @@ public abstract class ChromeActivity<C extends ChromeActivityComponent>
 
     private static final int RECORD_MULTI_WINDOW_SCREEN_WIDTH_DELAY_MS = 5000;
 
+    private static final String PREF_ALWAYS_INCOGNITO = "always_incognito";
+
     /**
      * Timeout in ms for reading PartnerBrowserCustomizations provider.
      */
@@ -1697,6 +1699,9 @@ public abstract class ChromeActivity<C extends ChromeActivityComponent>
             throw new IllegalStateException(
                     "Attempting to access TabCreator before initialization");
         }
+        if (ContextUtils.getAppSharedPreferences().getBoolean(PREF_ALWAYS_INCOGNITO, false)) {
+            return mIncognitoTabCreator;
+        }
         return incognito ? mIncognitoTabCreator : mRegularTabCreator;
     }
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/appmenu/AppMenuHandler.java b/chrome/android/java/src/org/chromium/chrome/browser/appmenu/AppMenuHandler.java
--- a/chrome/android/java/src/org/chromium/chrome/browser/appmenu/AppMenuHandler.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/appmenu/AppMenuHandler.java
@@ -17,6 +17,7 @@ import android.view.View;
 import android.widget.PopupMenu;
 
 import org.chromium.base.metrics.RecordUserAction;
+import org.chromium.base.ContextUtils;
 import org.chromium.chrome.R;
 
 import java.util.ArrayList;
@@ -26,6 +27,9 @@ import java.util.ArrayList;
  * AppMenuObservers about these actions.
  */
 public class AppMenuHandler {
+
+    private static final String PREF_ALWAYS_INCOGNITO = "always_incognito";
+
     private AppMenu mAppMenu;
     private AppMenuDragHelper mAppMenuDragHelper;
     private Menu mMenu;
@@ -182,6 +186,14 @@ public class AppMenuHandler {
         if (mDelegate.shouldShowHeader(appRect.height())) {
             headerResourceId = mDelegate.getHeaderResourceId();
         }
+
+        if (ContextUtils.getAppSharedPreferences().getBoolean(PREF_ALWAYS_INCOGNITO, false)) {
+            final MenuItem newTabOption = mAppMenu.getMenu().findItem(R.id.new_tab_menu_id);
+            if (newTabOption != null) {
+                newTabOption.setVisible(false);
+            }
+        }
+
         mAppMenu.show(wrapper, anchorView, isByPermanentButton, rotation, appRect, pt.y,
                 footerResourceId, headerResourceId, mHighlightMenuId, mCircleHighlight,
                 showFromBottom);
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ChromeContextMenuPopulator.java b/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ChromeContextMenuPopulator.java
--- a/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ChromeContextMenuPopulator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ChromeContextMenuPopulator.java
@@ -14,6 +14,7 @@ import android.webkit.MimeTypeMap;
 
 import org.chromium.base.library_loader.LibraryProcessType;
 import org.chromium.base.metrics.RecordHistogram;
+import org.chromium.base.ContextUtils;
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.ChromeFeatureList;
 import org.chromium.chrome.browser.compositor.bottombar.ephemeraltab.EphemeralTabPanel;
@@ -44,6 +45,7 @@ public class ChromeContextMenuPopulator implements ContextMenuPopulator {
     private static final String TAG = "CCMenuPopulator";
     private final ContextMenuItemDelegate mDelegate;
     private final @ContextMenuMode int mMode;
+    private static final String PREF_ALWAYS_INCOGNITO = "always_incognito";
 
     /**
      * Defines the Groups of each Context Menu Item
@@ -252,7 +254,12 @@ public class ChromeContextMenuPopulator implements ContextMenuPopulator {
             if (FirstRunStatus.getFirstRunFlowComplete() && mMode == ContextMenuMode.NORMAL
                     && !isEmptyUrl(params.getUrl())
                     && UrlUtilities.isAcceptedScheme(params.getUrl())) {
-                linkTab.add(new ChromeContextMenuItem(Item.OPEN_IN_NEW_TAB));
+                if (ContextUtils.getAppSharedPreferences().getBoolean(PREF_ALWAYS_INCOGNITO, false)
+                       && !mDelegate.isIncognito()) {
+                    // disallow open in new tab
+                } else {
+                    linkTab.add(new ChromeContextMenuItem(Item.OPEN_IN_NEW_TAB));
+                }
                 if (!mDelegate.isIncognito() && mDelegate.isIncognitoSupported()) {
                     linkTab.add(new ChromeContextMenuItem(Item.OPEN_IN_INCOGNITO_TAB));
                 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/ChromeTabCreator.java b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/ChromeTabCreator.java
--- a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/ChromeTabCreator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/ChromeTabCreator.java
@@ -7,8 +7,10 @@ package org.chromium.chrome.browser.tabmodel;
 import android.content.Intent;
 import android.text.TextUtils;
 
+import org.chromium.base.ContextUtils;
 import org.chromium.base.SysUtils;
 import org.chromium.base.TraceEvent;
+import org.chromium.chrome.browser.AlwaysIncognitoLinkInterceptor;
 import org.chromium.chrome.browser.ChromeActivity;
 import org.chromium.chrome.browser.IntentHandler;
 import org.chromium.chrome.browser.ServiceTabLauncher;
@@ -16,6 +18,7 @@ import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
 import org.chromium.chrome.browser.net.spdyproxy.DataReductionProxySettings;
 import org.chromium.chrome.browser.tab.Tab;
 import org.chromium.chrome.browser.tab.TabBuilder;
+import org.chromium.chrome.browser.tab.TabObserver;
 import org.chromium.chrome.browser.tab.TabDelegateFactory;
 import org.chromium.chrome.browser.tab.TabParentIntent;
 import org.chromium.chrome.browser.tab.TabRedirectHandler;
@@ -35,6 +38,7 @@ public class ChromeTabCreator extends TabCreatorManager.TabCreator {
 
     private final ChromeActivity mActivity;
     private final boolean mIncognito;
+    private final TabObserver mExtraLogic;
 
     private WindowAndroid mNativeWindow;
     private TabModel mTabModel;
@@ -46,6 +50,11 @@ public class ChromeTabCreator extends TabCreatorManager.TabCreator {
         mActivity = activity;
         mNativeWindow = nativeWindow;
         mIncognito = incognito;
+        if (!mIncognito) {
+            mExtraLogic = new AlwaysIncognitoLinkInterceptor(ContextUtils.getAppSharedPreferences());
+        } else {
+            mExtraLogic = null;
+        }
     }
 
     @Override
@@ -170,6 +179,10 @@ public class ChromeTabCreator extends TabCreatorManager.TabCreator {
                         tab.getWebContents());
             }
 
+            if (mExtraLogic != null) {
+                tab.addObserver(mExtraLogic);
+            }
+
             mTabModel.addTab(tab, position, type);
             return tab;
         } finally {
@@ -198,6 +211,11 @@ public class ChromeTabCreator extends TabCreatorManager.TabCreator {
                           .setLaunchType(type)
                           .build();
         tab.initialize(webContents, mTabContentManager, delegateFactory, !openInForeground, false);
+
+        if (mExtraLogic != null) {
+            tab.addObserver(mExtraLogic);
+        }
+
         mTabModel.addTab(tab, position, type);
         return true;
     }
@@ -240,7 +258,6 @@ public class ChromeTabCreator extends TabCreatorManager.TabCreator {
      */
     public Tab launchUrlFromExternalApp(String url, String referer, String headers,
             String appId, boolean forceNewTab, Intent intent, long intentTimestamp) {
-        assert !mIncognito;
         boolean isLaunchedFromChrome = TextUtils.equals(appId, mActivity.getPackageName());
 
         // If an external app sends an intent for a Weblite URL and the Data Reduction Proxy is
@@ -304,6 +321,11 @@ public class ChromeTabCreator extends TabCreatorManager.TabCreator {
         tab.initialize(
                 null, mTabContentManager, createDefaultTabDelegateFactory(), !selectTab, false);
         assert state.isIncognito() == mIncognito;
+
+        if (mExtraLogic != null) {
+            tab.addObserver(mExtraLogic);
+        }
+
         mTabModel.addTab(tab, index, TabLaunchType.FROM_RESTORE);
         return tab;
     }
diff --git a/chrome/android/java/strings/android_chrome_strings.grd b/chrome/android/java/strings/android_chrome_strings.grd
--- a/chrome/android/java/strings/android_chrome_strings.grd
+++ b/chrome/android/java/strings/android_chrome_strings.grd
@@ -716,6 +716,12 @@ CHAR-LIMIT guidelines:
       <message name="IDS_SAFE_BROWSING_SCOUT_REPORTING_SUMMARY" desc="Body text for Chrome setting under 'Help Improve Safe Browsing'. This feature sends small amounts of data to Google about potentially-dangerous pages to help Safe Browsing improve Chrome's protection against malware and phishing.">
         Sends some system information and page content to Google
       </message>
+      <message name="IDS_ALWAYS_INCOGNITO_TITLE" desc="Title for always incognito mode">
+        Open links in incognito tabs always
+      </message>
+      <message name="IDS_ALWAYS_INCOGNITO_SUMMARY" desc="Summary for always incognito mode">
+        Opens links in incognito tabs when you click on new tab or on a link
+      </message>
       <message name="IDS_SAFE_BROWSING_TITLE" desc="Title for safe browsing.">
         Safe Browsing
       </message>
diff --git a/chrome/android/java_sources.gni b/chrome/android/java_sources.gni
--- a/chrome/android/java_sources.gni
+++ b/chrome/android/java_sources.gni
@@ -10,6 +10,7 @@ import("//components/offline_pages/buildflags/features.gni")
 import("//device/vr/buildflags/buildflags.gni")
 
 chrome_java_sources = [
+  "java/src/org/chromium/chrome/browser/AlwaysIncognitoLinkInterceptor.java",
   "java/src/com/google/android/apps/chrome/appwidget/bookmarks/BookmarkThumbnailWidgetProvider.java",
   "java/src/org/chromium/chrome/browser/ActivityTabProvider.java",
   "java/src/org/chromium/chrome/browser/ActivityTabTaskDescriptionHelper.java",
-- 
2.11.0

