From: uazo <uazo@users.noreply.github.com>
Date: Thu, 7 Oct 2021 14:27:12 +0000
Subject: Bromite auto updater

Enable checking for new versions, with notifications and proxy support.
Restore InlineUpdateFlow feature.
Some parts authored by csagan5.

License: GPL-3.0-only - https://spdx.org/licenses/GPL-3.0-only.html
---
 .../java/templates/BuildConfig.template       |   2 +
 build/config/android/rules.gni                |   3 +
 chrome/android/chrome_java_sources.gni        |   3 +
 .../java/res/xml/about_chrome_preferences.xml |   5 +
 .../about_settings/AboutChromeSettings.java   |  28 +-
 .../chrome/browser/omaha/OmahaBase.java       |  57 +++-
 .../chrome/browser/omaha/UpdateConfigs.java   |  30 ++-
 .../browser/omaha/UpdateMenuItemHelper.java   |  82 +++++-
 .../browser/omaha/UpdateStatusProvider.java   | 161 +++++++++---
 .../browser/omaha/VersionNumberGetter.java    |   3 +-
 .../inline/BromiteInlineUpdateController.java | 243 ++++++++++++++++++
 .../omaha/inline/InlineUpdateController.java  |  51 ++++
 .../inline/InlineUpdateControllerFactory.java |  21 ++
 chrome/browser/endpoint_fetcher/BUILD.gn      |   2 +
 .../endpoint_fetcher_android.cc               |  52 +++-
 .../endpoint_fetcher/EndpointFetcher.java     |  22 +-
 .../EndpointHeaderResponse.java               |  31 +++
 .../flags/android/chrome_feature_list.cc      |   5 +
 .../flags/android/chrome_feature_list.h       |   1 +
 .../browser/flags/ChromeFeatureList.java      |   1 +
 .../strings/android_chrome_strings.grd        |  23 +-
 .../endpoint_fetcher/endpoint_fetcher.cc      | 102 +++++++-
 .../endpoint_fetcher/endpoint_fetcher.h       |  22 +-
 23 files changed, 897 insertions(+), 53 deletions(-)
 create mode 100644 chrome/android/java/src/org/chromium/chrome/browser/omaha/inline/BromiteInlineUpdateController.java
 create mode 100644 chrome/android/java/src/org/chromium/chrome/browser/omaha/inline/InlineUpdateController.java
 create mode 100644 chrome/android/java/src/org/chromium/chrome/browser/omaha/inline/InlineUpdateControllerFactory.java
 create mode 100644 chrome/browser/endpoint_fetcher/java/src/org/chromium/chrome/browser/endpoint_fetcher/EndpointHeaderResponse.java

diff --git a/build/android/java/templates/BuildConfig.template b/build/android/java/templates/BuildConfig.template
--- a/build/android/java/templates/BuildConfig.template
+++ b/build/android/java/templates/BuildConfig.template
@@ -92,4 +92,6 @@ public class BuildConfig {
 #else
     public static MAYBE_FINAL boolean IS_FOR_TEST MAYBE_FALSE;
 #endif
+
+    public static MAYBE_FINAL String BUILD_TARGET_CPU = _BUILD_TARGET_CPU;
 }
diff --git a/build/config/android/rules.gni b/build/config/android/rules.gni
--- a/build/config/android/rules.gni
+++ b/build/config/android/rules.gni
@@ -2242,6 +2242,9 @@ if (enable_java_templates) {
       if (defined(testonly) && testonly) {
         defines += [ "_IS_FOR_TEST" ]
       }
+
+      # add arch to org.chromium.build.BuildConfig
+      defines += [ "_BUILD_TARGET_CPU=\"${target_cpu}\"" ]
     }
   }
 
diff --git a/chrome/android/chrome_java_sources.gni b/chrome/android/chrome_java_sources.gni
--- a/chrome/android/chrome_java_sources.gni
+++ b/chrome/android/chrome_java_sources.gni
@@ -812,6 +812,9 @@ chrome_java_sources = [
   "java/src/org/chromium/chrome/browser/omaha/UpdateConfigs.java",
   "java/src/org/chromium/chrome/browser/omaha/UpdateMenuItemHelper.java",
   "java/src/org/chromium/chrome/browser/omaha/UpdateStatusProvider.java",
+  "java/src/org/chromium/chrome/browser/omaha/inline/InlineUpdateController.java",
+  "java/src/org/chromium/chrome/browser/omaha/inline/InlineUpdateControllerFactory.java",
+  "java/src/org/chromium/chrome/browser/omaha/inline/BromiteInlineUpdateController.java",
   "java/src/org/chromium/chrome/browser/omaha/VersionNumberGetter.java",
   "java/src/org/chromium/chrome/browser/omaha/metrics/HistogramUtils.java",
   "java/src/org/chromium/chrome/browser/omaha/metrics/TrackingProvider.java",
diff --git a/chrome/android/java/res/xml/about_chrome_preferences.xml b/chrome/android/java/res/xml/about_chrome_preferences.xml
--- a/chrome/android/java/res/xml/about_chrome_preferences.xml
+++ b/chrome/android/java/res/xml/about_chrome_preferences.xml
@@ -9,6 +9,11 @@ found in the LICENSE file.
     <Preference
         android:key="application_version"
         android:title="@string/application_version_title" />
+    <org.chromium.components.browser_ui.settings.ChromeSwitchPreference
+        android:key="allow_inline_update"
+        android:title="@string/allow_inline_update_title"
+        android:summary="@string/allow_inline_update_summary"
+        android:defaultValue="false" />
     <org.chromium.chrome.browser.about_settings.AboutChromePreferenceOSVersion
         android:key="os_version"
         android:title="@string/os_version_title" />
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/about_settings/AboutChromeSettings.java b/chrome/android/java/src/org/chromium/chrome/browser/about_settings/AboutChromeSettings.java
--- a/chrome/android/java/src/org/chromium/chrome/browser/about_settings/AboutChromeSettings.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/about_settings/AboutChromeSettings.java
@@ -21,14 +21,20 @@ import org.chromium.ui.widget.Toast;
 
 import java.util.Calendar;
 
+import android.content.SharedPreferences;
+import org.chromium.chrome.browser.omaha.OmahaBase;
+import org.chromium.components.browser_ui.settings.ChromeSwitchPreference;
+
 /**
  * Settings fragment that displays information about Chrome.
  */
 public class AboutChromeSettings
-        extends PreferenceFragmentCompat implements Preference.OnPreferenceClickListener {
+        extends PreferenceFragmentCompat implements Preference.OnPreferenceClickListener,
+                                                    Preference.OnPreferenceChangeListener {
     private static final int TAPS_FOR_DEVELOPER_SETTINGS = 7;
 
     private static final String PREF_APPLICATION_VERSION = "application_version";
+    private static final String PREF_ALLOW_INLINE_UPDATE = "allow_inline_update"; // switch preference
     private static final String PREF_OS_VERSION = "os_version";
     private static final String PREF_LEGAL_INFORMATION = "legal_information";
 
@@ -59,6 +65,13 @@ public class AboutChromeSettings
         p = findPreference(PREF_LEGAL_INFORMATION);
         int currentYear = Calendar.getInstance().get(Calendar.YEAR);
         p.setSummary(getString(R.string.legal_information_summary, currentYear));
+
+        ChromeSwitchPreference allowInlineUpdate =
+                (ChromeSwitchPreference) findPreference(PREF_ALLOW_INLINE_UPDATE);
+        allowInlineUpdate.setChecked(
+            OmahaBase.getSharedPreferences()
+                    .getBoolean(OmahaBase.PREF_ALLOW_INLINE_UPDATE, false));
+        allowInlineUpdate.setOnPreferenceChangeListener(this);
     }
 
     /**
@@ -122,4 +135,17 @@ public class AboutChromeSettings
         }
         return true;
     }
+
+    @Override
+    public boolean onPreferenceChange(Preference preference, Object newValue) {
+        String key = preference.getKey();
+        if (PREF_ALLOW_INLINE_UPDATE.equals(key)) {
+            SharedPreferences.Editor sharedPreferenceEditor = OmahaBase.getSharedPreferences().edit();
+            sharedPreferenceEditor.putBoolean(OmahaBase.PREF_ALLOW_INLINE_UPDATE, (boolean) newValue);
+            sharedPreferenceEditor.apply();
+
+            OmahaBase.resetUpdatePrefs();
+        }
+        return true;
+    }
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omaha/OmahaBase.java b/chrome/android/java/src/org/chromium/chrome/browser/omaha/OmahaBase.java
--- a/chrome/android/java/src/org/chromium/chrome/browser/omaha/OmahaBase.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omaha/OmahaBase.java
@@ -33,6 +33,8 @@ import java.net.HttpURLConnection;
 import java.net.URL;
 import java.util.Date;
 
+import org.chromium.build.BuildConfig;
+
 /**
  * Keeps tabs on the current state of Chrome, tracking if and when a request should be sent to the
  * Omaha Server.
@@ -99,7 +101,10 @@ public class OmahaBase {
     static final String PREF_TIMESTAMP_FOR_NEW_REQUEST = "timestampForNewRequest";
     static final String PREF_TIMESTAMP_FOR_NEXT_POST_ATTEMPT = "timestampForNextPostAttempt";
     static final String PREF_TIMESTAMP_OF_INSTALL = "timestampOfInstall";
-    static final String PREF_TIMESTAMP_OF_REQUEST = "timestampOfRequest";
+    public static final String PREF_TIMESTAMP_OF_REQUEST = "timestampOfRequest";
+    public static final String PREF_LATEST_MODIFIED_VERSION = "latestModifiedVersion";
+    public static final String PREF_LATEST_UPSTREAM_VERSION = "latestUpstreamVersion";
+    public static final String PREF_ALLOW_INLINE_UPDATE = "allowInlineUpdate";
 
     static final int MIN_API_JOB_SCHEDULER = Build.VERSION_CODES.M;
 
@@ -160,7 +165,8 @@ public class OmahaBase {
 
     /** See {@link #sIsDisabled}. */
     static boolean isDisabled() {
-        return sIsDisabled;
+        // do not enable version control via Omaha Update Server
+        return true;
     }
 
     /**
@@ -584,6 +590,10 @@ public class OmahaBase {
     /** Sends the request to the server and returns the response. */
     static String sendRequestToServer(HttpURLConnection urlConnection, String request)
             throws RequestFailureException {
+        if ((true)) {
+            throw new RequestFailureException("Requests to Omaha server are forbidden.",
+                        RequestFailureException.ERROR_CONNECTIVITY);
+        }
         try {
             OutputStream out = new BufferedOutputStream(urlConnection.getOutputStream());
             OutputStreamWriter writer = new OutputStreamWriter(out);
@@ -654,4 +664,47 @@ public class OmahaBase {
                 // updateStatus is only used for the on-demand check.
                 null);
     }
+
+    public static boolean isNewVersionAvailableByVersion(VersionNumber latestVersion) {
+        VersionNumber mCurrentProductVersion = VersionNumber.fromString(VersionInfo.getProductVersion());
+        if (mCurrentProductVersion == null) {
+            Log.e(TAG, "BromiteUpdater: current product version is null");
+            return false;
+        }
+
+        Log.i(TAG, "BromiteUpdater: currentProductVersion=%s, latestVersion=%s",
+                mCurrentProductVersion.toString(), latestVersion.toString());
+
+        return mCurrentProductVersion.isSmallerThan(latestVersion);
+    }
+
+    public static void updateLastPushedTimeStamp(long timeMillis) {
+        SharedPreferences preferences = OmahaBase.getSharedPreferences();
+        SharedPreferences.Editor editor = preferences.edit();
+        editor.putLong(OmahaBase.PREF_TIMESTAMP_OF_REQUEST, timeMillis);
+        editor.apply();
+    }
+
+    public static void setLatestModifiedVersion(String version) {
+        SharedPreferences preferences = OmahaBase.getSharedPreferences();
+        SharedPreferences.Editor editor = preferences.edit();
+        editor.putString(OmahaBase.PREF_LATEST_MODIFIED_VERSION, version);
+        editor.apply();
+    }
+
+    public static void setLatestUpstreamVersion(String version) {
+        SharedPreferences preferences = OmahaBase.getSharedPreferences();
+        SharedPreferences.Editor editor = preferences.edit();
+        editor.putString(OmahaBase.PREF_LATEST_UPSTREAM_VERSION, version);
+        editor.apply();
+    }
+
+    public static void resetUpdatePrefs() {
+        SharedPreferences preferences = OmahaBase.getSharedPreferences();
+        SharedPreferences.Editor editor = preferences.edit();
+        editor.putLong(OmahaBase.PREF_TIMESTAMP_OF_REQUEST, 0);
+        editor.putString(OmahaBase.PREF_LATEST_MODIFIED_VERSION, "");
+        editor.putString(OmahaBase.PREF_LATEST_UPSTREAM_VERSION, "");
+        editor.apply();
+    }
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omaha/UpdateConfigs.java b/chrome/android/java/src/org/chromium/chrome/browser/omaha/UpdateConfigs.java
--- a/chrome/android/java/src/org/chromium/chrome/browser/omaha/UpdateConfigs.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omaha/UpdateConfigs.java
@@ -11,6 +11,7 @@ import androidx.annotation.IntDef;
 import androidx.annotation.Nullable;
 
 import org.chromium.base.CommandLine;
+import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
 import org.chromium.chrome.browser.omaha.UpdateStatusProvider.UpdateState;
 import org.chromium.components.variations.VariationsAssociatedData;
@@ -37,10 +38,12 @@ public class UpdateConfigs {
     private static final String UPDATE_AVAILABLE_SWITCH_VALUE = "update_available";
     private static final String UNSUPPORTED_OS_VERSION_SWITCH_VALUE = "unsupported_os_version";
 
+    private static final long DEFAULT_UPDATE_NOTIFICATION_INTERVAL = 3 * DateUtils.DAY_IN_MILLIS;
     private static final long DEFAULT_UPDATE_ATTRIBUTION_WINDOW_MS = 2 * DateUtils.DAY_IN_MILLIS;
 
     /** Possible update flow configurations. */
-    @IntDef({UpdateFlowConfiguration.NEVER_SHOW, UpdateFlowConfiguration.INTENT_ONLY})
+    @IntDef({UpdateFlowConfiguration.NEVER_SHOW, UpdateFlowConfiguration.INTENT_ONLY,
+            UpdateFlowConfiguration.INLINE_ONLY})
     @Retention(RetentionPolicy.SOURCE)
     public @interface UpdateFlowConfiguration {
         /** Turns off all update indicators. */
@@ -50,6 +53,12 @@ public class UpdateConfigs {
          * Requires Omaha to say an update is available, and only ever Intents out to Play Store.
          */
         int INTENT_ONLY = 2;
+
+        /**
+         * Inline updates that contact Bromite official GitHub repository to say whether an update is available.
+         * Only ever uses the inline update flow.
+         */
+        int INLINE_ONLY = 3;
     }
 
     /**
@@ -124,6 +133,13 @@ public class UpdateConfigs {
         return DEFAULT_UPDATE_ATTRIBUTION_WINDOW_MS;
     }
 
+    /**
+     * @return A time interval for scheduling update notification. Unit: mills.
+     */
+    public static long getUpdateNotificationInterval() {
+        return DEFAULT_UPDATE_NOTIFICATION_INTERVAL;
+    }
+
     /**
      * Gets a String VariationsAssociatedData parameter. Also checks for a command-line switch
      * with the same name, for easy local testing.
@@ -139,4 +155,14 @@ public class UpdateConfigs {
         }
         return value;
     }
-}
\ No newline at end of file
+
+    @UpdateFlowConfiguration
+    public static int getConfiguration() {
+        if (!ChromeFeatureList.isEnabled(ChromeFeatureList.INLINE_UPDATE_FLOW)) {
+            // Always use the the old flow if the inline update flow feature is not enabled.
+            return UpdateFlowConfiguration.INLINE_ONLY;
+        }
+
+        return UpdateFlowConfiguration.NEVER_SHOW;
+    }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omaha/UpdateMenuItemHelper.java b/chrome/android/java/src/org/chromium/chrome/browser/omaha/UpdateMenuItemHelper.java
--- a/chrome/android/java/src/org/chromium/chrome/browser/omaha/UpdateMenuItemHelper.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omaha/UpdateMenuItemHelper.java
@@ -11,6 +11,7 @@ import android.text.TextUtils;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
 import androidx.annotation.VisibleForTesting;
 
 import org.chromium.base.BuildInfo;
@@ -165,8 +166,21 @@ public class UpdateMenuItemHelper {
                     recordItemClickedHistogram(ITEM_CLICKED_INTENT_FAILED);
                 }
                 break;
+            case UpdateState.VULNERABLE_VERSION:
+            // Intentional fall through.
+            case UpdateState.INLINE_UPDATE_AVAILABLE:
+                UpdateStatusProvider.getInstance().startInlineUpdate(activity);
+                break;
+            case UpdateState.INLINE_UPDATE_READY:
+                UpdateStatusProvider.getInstance().finishInlineUpdate();
+                break;
+            case UpdateState.INLINE_UPDATE_FAILED:
+                UpdateStatusProvider.getInstance().retryInlineUpdate(activity);
+                break;
             case UpdateState.UNSUPPORTED_OS_VERSION:
             // Intentional fall through.
+            case UpdateState.INLINE_UPDATE_DOWNLOADING:
+            // Intentional fall through.
             default:
                 return;
         }
@@ -215,7 +229,7 @@ public class UpdateMenuItemHelper {
 
         mMenuUiState = new MenuUiState();
         switch (mStatus.updateState) {
-            case UpdateState.UPDATE_AVAILABLE:
+            case UpdateState.UPDATE_AVAILABLE: // this is not used in Bromite
                 // The badge is hidden if the update menu item has been clicked until there is an
                 // even newer version of Chrome available.
                 showBadge |= !TextUtils.equals(
@@ -270,6 +284,72 @@ public class UpdateMenuItemHelper {
                 mMenuUiState.itemState.icon = R.drawable.ic_error_24dp_filled;
                 mMenuUiState.itemState.enabled = false;
                 break;
+            case UpdateState.VULNERABLE_VERSION:
+            // Intentional fall through.
+            case UpdateState.INLINE_UPDATE_AVAILABLE:
+                // The badge is hidden if the update menu item has been clicked until there is an
+                // even newer version of Chrome available.
+                @StringRes int defaultUpdateSummary = R.string.menu_update_summary_default;
+                if (mStatus.updateState == UpdateState.VULNERABLE_VERSION) {
+                    // always show badge in case of vulnerable version
+                    showBadge = true;
+                    mMenuUiState.buttonState = new MenuButtonState();
+                    mMenuUiState.buttonState.menuContentDescription = R.string.accessibility_toolbar_btn_menu_update;
+                    mMenuUiState.buttonState.darkBadgeIcon =
+                            R.drawable.ic_error_grey800_24dp_filled;
+                    mMenuUiState.buttonState.lightBadgeIcon = R.drawable.ic_error_white_24dp_filled;
+                    mMenuUiState.buttonState.adaptiveBadgeIcon = R.drawable.ic_error_24dp_filled;
+                    defaultUpdateSummary = R.string.menu_update_summary_vulnerable;
+                } else {
+                    showBadge |= !TextUtils.equals(
+                        getPrefService().getString(
+                                Pref.LATEST_VERSION_WHEN_CLICKED_UPDATE_MENU_ITEM),
+                        mStatus.latestUnsupportedVersion);
+                    if (showBadge) {
+                        mMenuUiState.buttonState = new MenuButtonState();
+                        mMenuUiState.buttonState.menuContentDescription = R.string.accessibility_toolbar_btn_menu_update;
+                        mMenuUiState.buttonState.darkBadgeIcon = R.drawable.badge_update_dark;
+                        mMenuUiState.buttonState.lightBadgeIcon = R.drawable.badge_update_light;
+                        mMenuUiState.buttonState.adaptiveBadgeIcon = R.drawable.badge_update;
+                    }
+                }
+
+                mMenuUiState.itemState = new MenuItemState();
+                mMenuUiState.itemState.title = R.string.menu_update;
+                mMenuUiState.itemState.titleColorId = R.color.default_text_color_blue_dark;
+                mMenuUiState.itemState.summary = UpdateConfigs.getCustomSummary();
+                if (TextUtils.isEmpty(mMenuUiState.itemState.summary)) {
+                    mMenuUiState.itemState.summary =
+                            resources.getString(defaultUpdateSummary);
+                }
+                mMenuUiState.itemState.icon = R.drawable.ic_history_googblue_24dp;
+                mMenuUiState.itemState.iconTintId = R.color.default_icon_color_blue_light;
+                mMenuUiState.itemState.enabled = true;
+                break;
+            case UpdateState.INLINE_UPDATE_DOWNLOADING:
+                mMenuUiState.itemState = new MenuItemState();
+                mMenuUiState.itemState.title = R.string.menu_inline_update_downloading;
+                mMenuUiState.itemState.titleColorId = R.color.default_text_color_secondary_dark;
+                break;
+            case UpdateState.INLINE_UPDATE_READY:
+                mMenuUiState.itemState = new MenuItemState();
+                mMenuUiState.itemState.title = R.string.menu_inline_update_ready;
+                mMenuUiState.itemState.titleColorId = R.color.default_text_color_blue_dark;
+                mMenuUiState.itemState.summary =
+                        resources.getString(R.string.menu_inline_update_ready_summary);
+                mMenuUiState.itemState.icon = R.drawable.infobar_chrome;
+                mMenuUiState.itemState.iconTintId = R.color.default_icon_color_blue_light;
+                mMenuUiState.itemState.enabled = true;
+                break;
+            case UpdateState.INLINE_UPDATE_FAILED:
+                mMenuUiState.itemState = new MenuItemState();
+                mMenuUiState.itemState.title = R.string.menu_inline_update_failed;
+                mMenuUiState.itemState.titleColorId = R.color.default_text_color_blue_dark;
+                mMenuUiState.itemState.summary = resources.getString(R.string.try_again);
+                mMenuUiState.itemState.icon = R.drawable.ic_history_googblue_24dp;
+                mMenuUiState.itemState.iconTintId = R.color.default_icon_color_blue_light;
+                mMenuUiState.itemState.enabled = true;
+                break;
             case UpdateState.NONE:
             // Intentional fall through.
             default:
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omaha/UpdateStatusProvider.java b/chrome/android/java/src/org/chromium/chrome/browser/omaha/UpdateStatusProvider.java
--- a/chrome/android/java/src/org/chromium/chrome/browser/omaha/UpdateStatusProvider.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omaha/UpdateStatusProvider.java
@@ -4,6 +4,7 @@
 
 package org.chromium.chrome.browser.omaha;
 
+import android.app.Activity;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
@@ -16,7 +17,11 @@ import android.text.TextUtils;
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
+import org.chromium.base.ActivityState;
+import org.chromium.base.ApplicationStatus;
+import org.chromium.base.ApplicationStatus.ActivityStateListener;
 import org.chromium.base.BuildInfo;
 import org.chromium.base.Callback;
 import org.chromium.base.ContextUtils;
@@ -26,6 +31,10 @@ import org.chromium.base.metrics.RecordHistogram;
 import org.chromium.base.task.AsyncTask;
 import org.chromium.base.task.AsyncTask.Status;
 import org.chromium.base.task.PostTask;
+import org.chromium.chrome.browser.app.ChromeActivity;
+import org.chromium.chrome.browser.omaha.inline.BromiteInlineUpdateController;
+import org.chromium.chrome.browser.omaha.inline.InlineUpdateController;
+import org.chromium.chrome.browser.omaha.inline.InlineUpdateControllerFactory;
 import org.chromium.chrome.browser.omaha.metrics.UpdateSuccessMetrics;
 import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
 import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
@@ -36,30 +45,37 @@ import java.io.File;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
+import org.chromium.base.Log;
+import android.content.SharedPreferences;
+import android.os.Build;
+import org.chromium.build.BuildConfig;
+
 /**
  * Provides the current update state for Chrome.  This update state is asynchronously determined and
  * can change as Chrome runs.
  *
  * For manually testing this functionality, see {@link UpdateConfigs}.
  */
-public class UpdateStatusProvider {
+public class UpdateStatusProvider implements ActivityStateListener {
     /**
      * Possible update states.
      * Treat this as append only as it is used by UMA.
      */
-    @IntDef({UpdateState.NONE, UpdateState.UPDATE_AVAILABLE, UpdateState.UNSUPPORTED_OS_VERSION})
+    @IntDef({UpdateState.NONE, UpdateState.UPDATE_AVAILABLE, UpdateState.UNSUPPORTED_OS_VERSION,
+            UpdateState.INLINE_UPDATE_AVAILABLE, UpdateState.INLINE_UPDATE_DOWNLOADING,
+            UpdateState.INLINE_UPDATE_READY, UpdateState.INLINE_UPDATE_FAILED, UpdateState.VULNERABLE_VERSION})
     @Retention(RetentionPolicy.SOURCE)
     public @interface UpdateState {
         int NONE = 0;
         int UPDATE_AVAILABLE = 1;
         int UNSUPPORTED_OS_VERSION = 2;
-        // Inline updates are deprecated.
-        // int INLINE_UPDATE_AVAILABLE = 3;
-        // int INLINE_UPDATE_DOWNLOADING = 4;
-        // int INLINE_UPDATE_READY = 5;
-        // int INLINE_UPDATE_FAILED = 6;
+        int INLINE_UPDATE_AVAILABLE = 3;
+        int INLINE_UPDATE_DOWNLOADING = 4;
+        int INLINE_UPDATE_READY = 5;
+        int INLINE_UPDATE_FAILED = 6;
+        int VULNERABLE_VERSION = 7;
 
-        int NUM_ENTRIES = 7;
+        int NUM_ENTRIES = 8;
     }
 
     /** A set of properties that represent the current update state for Chrome. */
@@ -93,6 +109,12 @@ public class UpdateStatusProvider {
          */
         private boolean mIsSimulated;
 
+        /**
+         * Whether or not we are currently trying to simulate an inline flow.  Used to allow
+         * overriding Omaha update state, which usually supersedes inline update states.
+         */
+        private boolean mIsInlineSimulated;
+
         public UpdateStatus() {}
 
         UpdateStatus(UpdateStatus other) {
@@ -101,11 +123,13 @@ public class UpdateStatusProvider {
             latestVersion = other.latestVersion;
             latestUnsupportedVersion = other.latestUnsupportedVersion;
             mIsSimulated = other.mIsSimulated;
+            mIsInlineSimulated = other.mIsInlineSimulated;
         }
     }
 
     private final ObserverList<Callback<UpdateStatus>> mObservers = new ObserverList<>();
 
+    private final InlineUpdateController mInlineController;
     private final UpdateQuery mOmahaQuery;
     private final UpdateSuccessMetrics mMetrics;
     private @Nullable UpdateStatus mStatus;
@@ -173,6 +197,30 @@ public class UpdateStatusProvider {
         pingObservers();
     }
 
+    /**
+     * Starts the inline update process, if possible.
+     * @param activity An {@link Activity} that will be used to interact with Play.
+     */
+    public void startInlineUpdate(Activity activity) {
+        if (mStatus == null || (mStatus.updateState != UpdateState.INLINE_UPDATE_AVAILABLE && mStatus.updateState != UpdateState.VULNERABLE_VERSION)) return;
+        mInlineController.startUpdate(activity);
+    }
+
+    /**
+     * Retries the inline update process, if possible.
+     * @param activity An {@link Activity} that will be used to interact with Play.
+     */
+    public void retryInlineUpdate(Activity activity) {
+        if (mStatus == null || (mStatus.updateState != UpdateState.INLINE_UPDATE_AVAILABLE && mStatus.updateState != UpdateState.VULNERABLE_VERSION)) return;
+        mInlineController.startUpdate(activity);
+    }
+
+    /** Finishes the inline update process, which may involve restarting the app. */
+    public void finishInlineUpdate() {
+        if (mStatus == null || mStatus.updateState != UpdateState.INLINE_UPDATE_READY) return;
+        mInlineController.completeUpdate();
+    }
+
     /**
      * Starts the intent update process, if possible
      * @param context An {@link Context} that will be used to fire off the update intent.
@@ -180,12 +228,11 @@ public class UpdateStatusProvider {
      * @return        Whether or not the update intent was sent and had a valid handler.
      */
     public boolean startIntentUpdate(Context context, boolean newTask) {
+        // currently not used in Bromite
         if (mStatus == null || mStatus.updateState != UpdateState.UPDATE_AVAILABLE) return false;
         if (TextUtils.isEmpty(mStatus.updateUrl)) return false;
 
         try {
-            mMetrics.startUpdate();
-
             Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(mStatus.updateUrl));
             if (newTask) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
             context.startActivity(intent);
@@ -196,9 +243,29 @@ public class UpdateStatusProvider {
         return true;
     }
 
+    // ApplicationStateListener implementation.
+    @Override
+    public void onActivityStateChange(Activity changedActivity, @ActivityState int newState) {
+        boolean hasActiveActivity = false;
+
+        for (Activity activity : ApplicationStatus.getRunningActivities()) {
+            if (activity == null || !(activity instanceof ChromeActivity)) continue;
+
+            hasActiveActivity |=
+                    ApplicationStatus.getStateForActivity(activity) == ActivityState.RESUMED;
+            if (hasActiveActivity) break;
+        }
+
+        mInlineController.setEnabled(hasActiveActivity);
+    }
+
     private UpdateStatusProvider() {
+        mInlineController = InlineUpdateControllerFactory.create(this::resolveStatus);
         mOmahaQuery = new UpdateQuery(this::resolveStatus);
         mMetrics = new UpdateSuccessMetrics();
+
+        // Note that as a singleton this class never unregisters.
+        ApplicationStatus.registerStateListenerForAllActivities(this);
     }
 
     private void pingObservers() {
@@ -206,21 +273,38 @@ public class UpdateStatusProvider {
     }
 
     private void resolveStatus() {
-        if (mOmahaQuery.getStatus() != Status.FINISHED) {
+        if (mOmahaQuery.getStatus() != Status.FINISHED || mInlineController.getStatus() == null) {
             return;
         }
 
         // We pull the Omaha result once as it will never change.
         if (mStatus == null) mStatus = new UpdateStatus(mOmahaQuery.getResult());
 
-        if (!mStatus.mIsSimulated) {
-            mStatus.updateState = mOmahaQuery.getResult().updateState;
+        if (mStatus.mIsSimulated) { // used only during tests
+            if (mStatus.mIsInlineSimulated) {
+                @UpdateState
+                int inlineState = mInlineController.getStatus();
+                String updateUrl = mInlineController.getUpdateUrl();
+
+                if (inlineState == UpdateState.NONE) {
+                    mStatus.updateState = mOmahaQuery.getResult().updateState;
+                } else {
+                    mStatus.updateState = inlineState;
+                    mStatus.updateUrl = updateUrl;
+                }
+            }
+        } else {
+            // used by Bromite to resolve update status
+            // ignores Omaha status
+            @UpdateState
+            int inlineState = mInlineController.getStatus();
+            mStatus.updateState = inlineState;
+            mStatus.updateUrl = mInlineController.getUpdateUrl();
         }
 
         if (!mRecordedInitialStatus) {
             RecordHistogram.recordEnumeratedHistogram(
                     "GoogleUpdate.StartUp.State", mStatus.updateState, UpdateState.NUM_ENTRIES);
-            mMetrics.analyzeFirstStatus();
             mRecordedInitialStatus = true;
         }
 
@@ -232,6 +316,7 @@ public class UpdateStatusProvider {
     }
 
     private static final class UpdateQuery extends AsyncTask<UpdateStatus> {
+        static final String TAG = "UpdateStatusProvider";
         private final Context mContext = ContextUtils.getApplicationContext();
         private final Runnable mCallback;
 
@@ -249,7 +334,7 @@ public class UpdateStatusProvider {
         protected UpdateStatus doInBackground() {
             UpdateStatus testStatus = getTestStatus();
             if (testStatus != null) return testStatus;
-            return getRealStatus(mContext);
+            return getActualStatus(mContext);
         }
 
         @Override
@@ -268,6 +353,8 @@ public class UpdateStatusProvider {
             status.mIsSimulated = true;
             status.updateState = forcedUpdateState;
 
+            status.mIsInlineSimulated = forcedUpdateState == UpdateState.INLINE_UPDATE_AVAILABLE;
+
             // Push custom configurations for certain update states.
             switch (forcedUpdateState) {
                 case UpdateState.UPDATE_AVAILABLE:
@@ -284,27 +371,33 @@ public class UpdateStatusProvider {
             return status;
         }
 
-        private UpdateStatus getRealStatus(Context context) {
+        private UpdateStatus getActualStatus(Context context) {
             UpdateStatus status = new UpdateStatus();
 
-            if (VersionNumberGetter.isNewerVersionAvailable(context)) {
-                status.updateUrl = MarketURLGetter.getMarketUrl();
-                status.latestVersion =
-                        VersionNumberGetter.getInstance().getLatestKnownVersion(context);
-
-                boolean allowedToUpdate =
-                        checkForSufficientStorage() && isGooglePlayStoreAvailable(context);
-                status.updateState =
-                        allowedToUpdate ? UpdateState.UPDATE_AVAILABLE : UpdateState.NONE;
-
-                SharedPreferencesManager.getInstance().removeKey(
-                        ChromePreferenceKeys.LATEST_UNSUPPORTED_VERSION);
-            } else if (!VersionNumberGetter.isCurrentOsVersionSupported()) {
-                status.updateState = UpdateState.UNSUPPORTED_OS_VERSION;
-                status.latestUnsupportedVersion = SharedPreferencesManager.getInstance().readString(
-                        ChromePreferenceKeys.LATEST_UNSUPPORTED_VERSION, null);
-            } else {
-                status.updateState = UpdateState.NONE;
+            SharedPreferences preferences = OmahaBase.getSharedPreferences();
+            status.latestVersion = preferences.getString(OmahaBase.PREF_LATEST_MODIFIED_VERSION, "");
+
+            status.updateState = UpdateState.NONE;
+            if (status.latestVersion != null && status.latestVersion.length() != 0) {
+                VersionNumber latestVersion = VersionNumber.fromString(status.latestVersion);
+                if (latestVersion == null) {
+                   Log.e(TAG, "BromiteUpdater: stored latest version '%s' is invalid", status.latestVersion);
+                } else if (OmahaBase.isNewVersionAvailableByVersion(latestVersion)) {
+                   status.updateState = UpdateState.INLINE_UPDATE_AVAILABLE;
+                   status.updateUrl = BromiteInlineUpdateController.getDownloadUrl();
+                   return status;
+                }
+                String latestUpstreamVersion = preferences.getString(OmahaBase.PREF_LATEST_UPSTREAM_VERSION, "");
+                if (latestUpstreamVersion != null && latestUpstreamVersion.length() != 0) {
+                   VersionNumber upstreamVersion = VersionNumber.fromString(latestUpstreamVersion);
+                   if (upstreamVersion == null) {
+                       Log.e(TAG, "BromiteUpdater: stored latest upstream version '%s' is invalid", latestUpstreamVersion);
+                   } else if (OmahaBase.isNewVersionAvailableByVersion(upstreamVersion)) {
+                       status.updateUrl = BromiteInlineUpdateController.VULNERABLE_VERSION_DOC_URL;
+                       status.updateState = UpdateState.VULNERABLE_VERSION;
+                       return status;
+                   }
+                }
             }
 
             return status;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omaha/VersionNumberGetter.java b/chrome/android/java/src/org/chromium/chrome/browser/omaha/VersionNumberGetter.java
--- a/chrome/android/java/src/org/chromium/chrome/browser/omaha/VersionNumberGetter.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omaha/VersionNumberGetter.java
@@ -48,7 +48,8 @@ public class VersionNumberGetter {
     private static VersionNumberGetter sInstanceForTests;
 
     /** If false, OmahaClient will never report that a newer version is available. */
-    private static boolean sEnableUpdateDetection = true;
+    // it must be false to disable version control via Omaha server
+    private static boolean sEnableUpdateDetection = false;
 
     protected VersionNumberGetter() { }
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omaha/inline/BromiteInlineUpdateController.java b/chrome/android/java/src/org/chromium/chrome/browser/omaha/inline/BromiteInlineUpdateController.java
new file mode 100644
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omaha/inline/BromiteInlineUpdateController.java
@@ -0,0 +1,243 @@
+// Copyright 2021 The Ungoogled Chromium Authors. All rights reserved.
+//
+// This file is part of Ungoogled Chromium Android.
+//
+// Ungoogled Chromium Android is free software: you can redistribute it
+// and/or modify it under the terms of the GNU General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or any later version.
+//
+// Ungoogled Chromium Android is distributed in the hope that it will be
+// useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Ungoogled Chromium Android.  If not,
+// see <https://www.gnu.org/licenses/>.
+
+package org.chromium.chrome.browser.omaha.inline;
+
+import static org.chromium.chrome.browser.omaha.UpdateConfigs.getUpdateNotificationInterval;
+
+import android.app.Activity;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.text.format.DateUtils;
+import org.chromium.build.BuildConfig;
+
+import androidx.annotation.Nullable;
+
+import org.chromium.base.Callback;
+import org.chromium.base.Log;
+import org.chromium.base.task.AsyncTask;
+import org.chromium.base.task.PostTask;
+import org.chromium.chrome.browser.app.ChromeActivity;
+import org.chromium.chrome.browser.omaha.OmahaBase;
+import org.chromium.chrome.browser.omaha.UpdateConfigs;
+import org.chromium.chrome.browser.omaha.UpdateStatusProvider;
+import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.chrome.browser.tab.TabLaunchType;
+import org.chromium.chrome.browser.tabmodel.TabCreator;
+import org.chromium.content_public.browser.LoadUrlParams;
+import org.chromium.content_public.browser.UiThreadTaskTraits;
+import org.chromium.ui.base.PageTransition;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.HttpURLConnection;
+import java.util.regex.Pattern;
+
+import org.chromium.chrome.browser.endpoint_fetcher.EndpointFetcher;
+import org.chromium.chrome.browser.endpoint_fetcher.EndpointResponse;
+import org.chromium.chrome.browser.omaha.VersionNumber;
+
+public class BromiteInlineUpdateController implements InlineUpdateController {
+
+    private static final String TAG = "BromiteInlineUpdateController";
+    private final String REDIRECT_URL_PREFIX = "https://github.com/bromite/bromite/releases/download/";
+    private static final String UPDATE_VERSION_URL = "https://github.com/bromite/bromite/releases/latest/download/";
+    private final String UPSTREAM_VERSION_URL = "https://www.bromite.org/upstream.txt";
+    public static final String VULNERABLE_VERSION_DOC_URL = "https://www.bromite.org/vulnerable-version";
+
+    public static String getDownloadUrl() {
+        return UPDATE_VERSION_URL + BuildConfig.BUILD_TARGET_CPU + "_ChromePublic.apk";
+    }
+
+    private boolean mEnabled = true;
+    private Runnable mCallback;
+    private @Nullable @UpdateStatusProvider.UpdateState Integer mUpdateState =
+                                                    UpdateStatusProvider.UpdateState.NONE;
+    private String mUpdateUrl = "";
+
+    BromiteInlineUpdateController(Runnable callback) {
+        mCallback = callback;
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        if (mEnabled == enabled) return;
+
+        mEnabled = enabled;
+        // check for an update when state changes
+        if (mEnabled) pullCurrentState();
+    }
+
+    @Override
+    public @Nullable @UpdateStatusProvider.UpdateState Integer getStatus() {
+        if (mEnabled) pullCurrentState();
+        return mUpdateState;
+    }
+
+    @Override
+    public String getUpdateUrl() {
+        // relies on a prior call to getStatus() to have state and URL correctly pulled
+        return mUpdateUrl;
+    }
+
+    @Override
+    public void startUpdate(Activity activity) {
+        assert ChromeActivity.class.isInstance(activity);
+        ChromeActivity thisActivity = (ChromeActivity) activity;
+        // Always open in new incognito tab
+        TabCreator tabCreator = thisActivity.getTabCreator(true);
+        tabCreator.createNewTab(new LoadUrlParams(mUpdateUrl, PageTransition.AUTO_BOOKMARK),
+                TabLaunchType.FROM_LINK, thisActivity.getActivityTab());
+    }
+
+    @Override
+    public void completeUpdate() {
+    }
+
+    private void pullCurrentState() {
+        if (OmahaBase.getSharedPreferences()
+                .getBoolean(OmahaBase.PREF_ALLOW_INLINE_UPDATE, false) == false) {
+            Log.i(TAG, "BromiteUpdater: disabled by user");
+            return;
+        }
+
+        // do not pull state if there is already a state set
+        if (mUpdateState != UpdateStatusProvider.UpdateState.NONE)
+            return;
+
+        if (shallUpdate() == false)
+            return;
+
+        switch (mUpdateState) {
+            case UpdateStatusProvider.UpdateState.INLINE_UPDATE_AVAILABLE:
+                break;
+            case UpdateStatusProvider.UpdateState.NONE:
+                OmahaBase.resetUpdatePrefs();
+                checkLatestVersion((latestVersion) -> {
+                    if (latestVersion == null) return;
+
+                    if (OmahaBase.isNewVersionAvailableByVersion(latestVersion)) {
+                        postStatus(UpdateStatusProvider.UpdateState.INLINE_UPDATE_AVAILABLE, getDownloadUrl());
+                    } else {
+                        checkLatestUpstreamVersion((latestUpstreamVersion) -> {
+                           if (latestUpstreamVersion == null) return;
+                           if (OmahaBase.isNewVersionAvailableByVersion(latestUpstreamVersion)) {
+                               postStatus(UpdateStatusProvider.UpdateState.VULNERABLE_VERSION, VULNERABLE_VERSION_DOC_URL);
+                           }
+                        });
+                    }
+                });
+                break;
+            case UpdateStatusProvider.UpdateState.INLINE_UPDATE_READY:
+                // Intentional fall through.
+            case UpdateStatusProvider.UpdateState.INLINE_UPDATE_FAILED:
+                // Intentional fall through.
+            case UpdateStatusProvider.UpdateState.INLINE_UPDATE_DOWNLOADING:
+                // Intentional fall through.
+            case UpdateStatusProvider.UpdateState.UNSUPPORTED_OS_VERSION:
+                // Intentional fall through.
+            case UpdateStatusProvider.UpdateState.VULNERABLE_VERSION:
+                // Intentional fall through.
+            default:
+                return;
+        }
+    }
+
+    private boolean shallUpdate() {
+        long currentTime = System.currentTimeMillis();
+        SharedPreferences preferences = OmahaBase.getSharedPreferences();
+        long lastPushedTimeStamp = preferences.getLong(OmahaBase.PREF_TIMESTAMP_OF_REQUEST, 0);
+        return currentTime - lastPushedTimeStamp >= getUpdateNotificationInterval();
+    }
+
+    private void checkLatestVersion(final Callback<VersionNumber> callback) {
+        assert UPDATE_VERSION_URL != null;
+
+        String urlToCheck = getDownloadUrl();
+        Log.i(TAG, "BromiteUpdater: fetching with HEAD '%s'", urlToCheck);
+
+        EndpointFetcher.nativeHeadWithNoAuth(
+                (endpointResponse) -> {
+                    boolean versionFound = false;
+                    String redirectURL = endpointResponse.getRedirectUrl();
+                    if (redirectURL != null) {
+                        Log.i(TAG, "BromiteUpdater: obtained response '%s' and redirect URL '%s'", endpointResponse.getResponseString(), redirectURL);
+                        if (redirectURL.indexOf(REDIRECT_URL_PREFIX) == 0) {
+                            redirectURL = redirectURL.substring(REDIRECT_URL_PREFIX.length());
+                            String[] parts = redirectURL.split(Pattern.quote("/"));
+                            if (parts.length > 0) {
+                                VersionNumber version = VersionNumber.fromString(parts[0]);
+                                if (version != null) {
+                                    versionFound = true;
+                                    OmahaBase.setLatestModifiedVersion(parts[0]);
+                                    callback.onResult(version);
+                                    return;
+                                }
+                            }
+                        }
+                    }
+                    if (!versionFound) {
+                        // retry after 1 hour
+                        OmahaBase.updateLastPushedTimeStamp(
+                            System.currentTimeMillis() - getUpdateNotificationInterval() -
+                            DateUtils.HOUR_IN_MILLIS);
+                        Log.e(TAG, "BromiteUpdater: failed, will retry in 1 hour");
+                    }
+
+                    callback.onResult(null);
+                },
+                Profile.getLastUsedRegularProfile(),
+                urlToCheck, /*timeout*/5000, /*follow_redirect*/true);
+    }
+
+    private void checkLatestUpstreamVersion(final Callback<VersionNumber> callback) {
+        Log.i(TAG, "BromiteUpdater: fetching with GET '%s'", UPSTREAM_VERSION_URL);
+
+        EndpointFetcher.nativeFetchWithNoAuth(
+                (endpointResponse) -> {
+                    String response = endpointResponse.getResponseString().trim();
+                    Log.i(TAG, "BromiteUpdater: obtained upstream version update response '%s'", response);
+                    VersionNumber version = VersionNumber.fromString(response);
+                    if (version != null) {
+                        OmahaBase.updateLastPushedTimeStamp(System.currentTimeMillis());
+                        OmahaBase.setLatestUpstreamVersion(response);
+                        callback.onResult(version);
+                        return;
+                    }
+                    // retry after 1 hour
+                    OmahaBase.updateLastPushedTimeStamp(
+                        System.currentTimeMillis() - getUpdateNotificationInterval() -
+                        DateUtils.HOUR_IN_MILLIS);
+                    Log.e(TAG, "BromiteUpdater: failed to fetch upstream version, will retry in 1 hour");
+
+                    callback.onResult(null);
+                },
+                Profile.getLastUsedRegularProfile(),
+                UPSTREAM_VERSION_URL, /*timeout*/5000, /*follow_redirect*/false);
+    }
+
+    private void postStatus(@UpdateStatusProvider.UpdateState int status, String updateUrl) {
+        mUpdateState = status;
+        mUpdateUrl = updateUrl;
+        PostTask.postTask(UiThreadTaskTraits.DEFAULT, mCallback);
+    }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omaha/inline/InlineUpdateController.java b/chrome/android/java/src/org/chromium/chrome/browser/omaha/inline/InlineUpdateController.java
new file mode 100644
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omaha/inline/InlineUpdateController.java
@@ -0,0 +1,51 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.omaha.inline;
+
+import android.app.Activity;
+import android.content.Intent;
+
+import androidx.annotation.Nullable;
+
+import org.chromium.chrome.browser.omaha.UpdateStatusProvider;
+
+/**
+ * Helper for gluing interactions with the Play store's AppUpdateManager with Chrome.  This
+ * involves hooking up to Play as a listener for install state changes, should only happen if we are
+ * in the foreground.
+ */
+public interface InlineUpdateController {
+    /**
+     * Enables or disables the controller. It will trigger an update check when previously disabled.
+     * @param enabled true iff the controller should be enabled.
+     */
+    void setEnabled(boolean enabled);
+
+    /**
+     * @return The current state of the inline update process.  May be {@code null} if the state
+     * hasn't been determined yet.
+     */
+    @Nullable
+    @UpdateStatusProvider.UpdateState
+    Integer getStatus();
+
+    /**
+     * @return The current update URL for the inline update process.  May be an empty string if the state
+     * hasn't been determined yet or if state does not specify one.
+     */
+    String getUpdateUrl();
+
+    /**
+     * Starts the update, if possible.  This will send an {@link Intent} out to play, which may
+     * cause Chrome to move to the background.
+     * @param activity The {@link Activity} to use to interact with Play.
+     */
+    void startUpdate(Activity activity);
+
+    /**
+     * Completes the Play installation process, if possible.  This may cause Chrome to restart.
+     */
+    void completeUpdate();
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omaha/inline/InlineUpdateControllerFactory.java b/chrome/android/java/src/org/chromium/chrome/browser/omaha/inline/InlineUpdateControllerFactory.java
new file mode 100644
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omaha/inline/InlineUpdateControllerFactory.java
@@ -0,0 +1,21 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.omaha.inline;
+
+import org.chromium.base.ContextUtils;
+import org.chromium.chrome.browser.flags.ChromeFeatureList;
+import org.chromium.chrome.browser.omaha.UpdateConfigs;
+
+/**
+ * A factory that creates an {@link InlineUpdateController} instance.
+ */
+public class InlineUpdateControllerFactory {
+    /**
+     * @return A new {@link InlineUpdateController}.
+     */
+    public static InlineUpdateController create(Runnable callback) {
+        return new BromiteInlineUpdateController(callback);
+    }
+}
diff --git a/chrome/browser/endpoint_fetcher/BUILD.gn b/chrome/browser/endpoint_fetcher/BUILD.gn
--- a/chrome/browser/endpoint_fetcher/BUILD.gn
+++ b/chrome/browser/endpoint_fetcher/BUILD.gn
@@ -17,6 +17,7 @@ android_library("java") {
   sources = [
     "java/src/org/chromium/chrome/browser/endpoint_fetcher/EndpointFetcher.java",
     "java/src/org/chromium/chrome/browser/endpoint_fetcher/EndpointResponse.java",
+    "java/src/org/chromium/chrome/browser/endpoint_fetcher/EndpointHeaderResponse.java",
   ]
   annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ]
 }
@@ -25,5 +26,6 @@ generate_jni("jni_headers") {
   sources = [
     "java/src/org/chromium/chrome/browser/endpoint_fetcher/EndpointFetcher.java",
     "java/src/org/chromium/chrome/browser/endpoint_fetcher/EndpointResponse.java",
+    "java/src/org/chromium/chrome/browser/endpoint_fetcher/EndpointHeaderResponse.java",
   ]
 }
diff --git a/chrome/browser/endpoint_fetcher/endpoint_fetcher_android.cc b/chrome/browser/endpoint_fetcher/endpoint_fetcher_android.cc
--- a/chrome/browser/endpoint_fetcher/endpoint_fetcher_android.cc
+++ b/chrome/browser/endpoint_fetcher/endpoint_fetcher_android.cc
@@ -9,6 +9,7 @@
 #include "base/android/jni_string.h"
 #include "chrome/browser/endpoint_fetcher/jni_headers/EndpointFetcher_jni.h"
 #include "chrome/browser/endpoint_fetcher/jni_headers/EndpointResponse_jni.h"
+#include "chrome/browser/endpoint_fetcher/jni_headers/EndpointHeaderResponse_jni.h"
 #include "chrome/browser/profiles/profile_android.h"
 #include "chrome/browser/signin/identity_manager_factory.h"
 #include "chrome/common/channel_info.h"
@@ -30,6 +31,24 @@ static void OnEndpointFetcherComplete(
                        base::android::AttachCurrentThread(),
                        std::move(endpoint_response->response))));
 }
+
+static void OnEndpointFetcherHeadComplete(
+    const base::android::JavaRef<jobject>& jcaller,
+    // Passing the endpoint_fetcher ensures the endpoint_fetcher's
+    // lifetime extends to the callback and is not destroyed
+    // prematurely (which would result in cancellation of the request).
+    std::unique_ptr<EndpointFetcher> endpoint_fetcher,
+    std::unique_ptr<EndpointResponse> endpoint_response) {
+  base::android::RunObjectCallbackAndroid(
+      jcaller, Java_EndpointHeaderResponse_createEndpointResponse(
+                   base::android::AttachCurrentThread(),
+                   base::android::ConvertUTF8ToJavaString(
+                       base::android::AttachCurrentThread(),
+                       std::move(endpoint_response->response)),
+                   base::android::ConvertUTF8ToJavaString(
+                       base::android::AttachCurrentThread(),
+                       std::move(endpoint_response->redirect_url))));
+}
 }  // namespace
 
 // TODO(crbug.com/1077537) Create a KeyProvider so
@@ -109,15 +128,17 @@ static void JNI_EndpointFetcher_NativeFetchWithNoAuth(
     JNIEnv* env,
     const base::android::JavaParamRef<jobject>& jprofile,
     const base::android::JavaParamRef<jstring>& jurl,
-    jint jannotation_hash_code,
+    jlong jtimeout, jboolean intercept_redirect,
     const base::android::JavaParamRef<jobject>& jcallback) {
   auto endpoint_fetcher = std::make_unique<EndpointFetcher>(
       ProfileAndroid::FromProfileAndroid(jprofile)
           ->GetDefaultStoragePartition()
           ->GetURLLoaderFactoryForBrowserProcess(),
       GURL(base::android::ConvertJavaStringToUTF8(env, jurl)),
-      net::NetworkTrafficAnnotationTag::FromJavaAnnotation(
-          jannotation_hash_code));
+      "GET",
+      jtimeout,
+      intercept_redirect,
+      NO_TRAFFIC_ANNOTATION_YET);
   auto* const endpoint_fetcher_ptr = endpoint_fetcher.get();
   endpoint_fetcher_ptr->PerformRequest(
       base::BindOnce(&OnEndpointFetcherComplete,
@@ -127,3 +148,28 @@ static void JNI_EndpointFetcher_NativeFetchWithNoAuth(
                      std::move(endpoint_fetcher)),
       nullptr);
 }
+
+static void JNI_EndpointFetcher_NativeHeadWithNoAuth(
+    JNIEnv* env,
+    const base::android::JavaParamRef<jobject>& jprofile,
+    const base::android::JavaParamRef<jstring>& jurl,
+    jlong jtimeout, jboolean intercept_redirect,
+    const base::android::JavaParamRef<jobject>& jcallback) {
+  auto endpoint_fetcher = std::make_unique<EndpointFetcher>(
+      ProfileAndroid::FromProfileAndroid(jprofile)
+        ->GetDefaultStoragePartition()
+        ->GetURLLoaderFactoryForBrowserProcess(),
+      GURL(base::android::ConvertJavaStringToUTF8(env, jurl)),
+      "HEAD",
+      jtimeout,
+      intercept_redirect,
+      NO_TRAFFIC_ANNOTATION_YET);
+  auto* const endpoint_fetcher_ptr = endpoint_fetcher.get();
+  endpoint_fetcher_ptr->PerformRequest(
+      base::BindOnce(&OnEndpointFetcherHeadComplete,
+                     base::android::ScopedJavaGlobalRef<jobject>(jcallback),
+                     // unique_ptr endpoint_fetcher is passed until the callback
+                     // to ensure its lifetime across the request.
+                     std::move(endpoint_fetcher)),
+      nullptr);
+}
diff --git a/chrome/browser/endpoint_fetcher/java/src/org/chromium/chrome/browser/endpoint_fetcher/EndpointFetcher.java b/chrome/browser/endpoint_fetcher/java/src/org/chromium/chrome/browser/endpoint_fetcher/EndpointFetcher.java
--- a/chrome/browser/endpoint_fetcher/java/src/org/chromium/chrome/browser/endpoint_fetcher/EndpointFetcher.java
+++ b/chrome/browser/endpoint_fetcher/java/src/org/chromium/chrome/browser/endpoint_fetcher/EndpointFetcher.java
@@ -70,6 +70,22 @@ public final class EndpointFetcher {
                 postData, timeout, headers, annotation.getHashCode(), callback);
     }
 
+    @MainThread
+    public static void nativeHeadWithNoAuth(
+            Callback<EndpointHeaderResponse> callback, Profile profile,
+            String url, long timeout, boolean allow_redirect) {
+        EndpointFetcherJni.get().nativeHeadWithNoAuth(
+                profile, url, timeout, allow_redirect, callback);
+    }
+
+    @MainThread
+    public static void nativeFetchWithNoAuth(
+            Callback<EndpointResponse> callback, Profile profile,
+            String url, long timeout, boolean allow_redirect) {
+        EndpointFetcherJni.get().nativeFetchWithNoAuth(
+                profile, url, timeout, allow_redirect, callback);
+    }
+
     @NativeMethods
     public interface Natives {
         void nativeFetchOAuth(Profile profile, String oathConsumerName, String url,
@@ -78,7 +94,11 @@ public final class EndpointFetcher {
         void nativeFetchChromeAPIKey(Profile profile, String url, String httpsMethod,
                 String contentType, String postData, long timeout, String[] headers,
                 int annotationHashCode, Callback<EndpointResponse> callback);
-        void nativeFetchWithNoAuth(Profile profile, String url, int annotationHashCode,
+        void nativeFetchWithNoAuth(
+                Profile profile, String url, long timeout, boolean allow_redirect,
                 Callback<EndpointResponse> callback);
+        void nativeHeadWithNoAuth(
+                Profile profile, String url, long timeout, boolean allow_redirect,
+                Callback<EndpointHeaderResponse> callback);
     }
 }
diff --git a/chrome/browser/endpoint_fetcher/java/src/org/chromium/chrome/browser/endpoint_fetcher/EndpointHeaderResponse.java b/chrome/browser/endpoint_fetcher/java/src/org/chromium/chrome/browser/endpoint_fetcher/EndpointHeaderResponse.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/endpoint_fetcher/java/src/org/chromium/chrome/browser/endpoint_fetcher/EndpointHeaderResponse.java
@@ -0,0 +1,31 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.endpoint_fetcher;
+
+import org.chromium.base.annotations.CalledByNative;
+
+public class EndpointHeaderResponse {
+    private final String mResponseString;
+    private final String mRedirectUrl;
+
+    public EndpointHeaderResponse(String responseString, String redirectUrl) {
+        mResponseString = responseString;
+        mRedirectUrl = redirectUrl;
+    }
+
+    public String getResponseString() {
+        return mResponseString;
+    }
+
+    public String getRedirectUrl() {
+        return mRedirectUrl;
+    }
+
+    @CalledByNative
+    private static EndpointHeaderResponse createEndpointResponse(
+            String response, String redirectUrl) {
+        return new EndpointHeaderResponse(response, redirectUrl);
+    }
+}
diff --git a/chrome/browser/flags/android/chrome_feature_list.cc b/chrome/browser/flags/android/chrome_feature_list.cc
--- a/chrome/browser/flags/android/chrome_feature_list.cc
+++ b/chrome/browser/flags/android/chrome_feature_list.cc
@@ -251,6 +251,7 @@ const base::Feature* const kFeaturesExposedToJava[] = {
     &kNewWindowAppMenu,
     &kNotificationPermissionVariant,
     &kPageAnnotationsService,
+    &kInlineUpdateFlow,
     &kBookmarksImprovedSaveFlow,
     &kBookmarksRefresh,
     &kBackGestureRefactorAndroid,
@@ -693,6 +694,10 @@ BASE_FEATURE(kContextMenuTranslateWithGoogleLens,
              "ContextMenuTranslateWithGoogleLens",
              base::FEATURE_DISABLED_BY_DEFAULT);
 
+BASE_FEATURE(kInlineUpdateFlow,
+             "InlineUpdateFlow",
+             base::FEATURE_ENABLED_BY_DEFAULT);
+
 BASE_FEATURE(kLensCameraAssistedSearch,
              "LensCameraAssistedSearch",
              base::FEATURE_ENABLED_BY_DEFAULT);
diff --git a/chrome/browser/flags/android/chrome_feature_list.h b/chrome/browser/flags/android/chrome_feature_list.h
--- a/chrome/browser/flags/android/chrome_feature_list.h
+++ b/chrome/browser/flags/android/chrome_feature_list.h
@@ -91,6 +91,7 @@ BASE_DECLARE_FEATURE(kGridTabSwitcherForTablets);
 BASE_DECLARE_FEATURE(kHandleMediaIntents);
 BASE_DECLARE_FEATURE(kImmersiveUiMode);
 BASE_DECLARE_FEATURE(kIncognitoReauthenticationForAndroid);
+extern const base::Feature kInlineUpdateFlow;
 BASE_DECLARE_FEATURE(kIncognitoScreenshot);
 BASE_DECLARE_FEATURE(kInfobarScrollOptimization);
 BASE_DECLARE_FEATURE(kImprovedA2HS);
diff --git a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
--- a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
+++ b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
@@ -385,6 +385,7 @@ public abstract class ChromeFeatureList {
             "IncognitoReauthenticationForAndroid";
     public static final String INCOGNITO_SCREENSHOT = "IncognitoScreenshot";
     public static final String INFOBAR_SCROLL_OPTIMIZATION = "InfobarScrollOptimization";
+    public static final String INLINE_UPDATE_FLOW = "InlineUpdateFlow";
     public static final String INSTALLABLE_AMBIENT_BADGE_INFOBAR = "InstallableAmbientBadgeInfoBar";
     public static final String INSTALLABLE_AMBIENT_BADGE_MESSAGE = "InstallableAmbientBadgeMessage";
     public static final String INSTANCE_SWITCHER = "InstanceSwitcher";
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings.grd b/chrome/browser/ui/android/strings/android_chrome_strings.grd
--- a/chrome/browser/ui/android/strings/android_chrome_strings.grd
+++ b/chrome/browser/ui/android/strings/android_chrome_strings.grd
@@ -1837,6 +1837,12 @@ Your Google account may have other forms of browsing history like searches and a
       <message name="IDS_DEPRECATION_WARNING" desc="Warning about Chrome updates no longer being supported">
         Chrome updates are no longer supported for this version of Android
       </message>
+      <message name="IDS_ALLOW_INLINE_UPDATE_TITLE" desc="Title for allow inline update preference">
+        Allow checking for updates
+      </message>
+      <message name="IDS_ALLOW_INLINE_UPDATE_SUMMARY" desc="Summary for allow inline update preference">
+        Notify about new releases by periodically checking for their availability
+      </message>
 
       <!-- Account management UI strings. -->
       <message name="IDS_ACCOUNT_MANAGEMENT_TITLE" desc="Header title for the account management screen. [CHAR_LIMIT=32]">
@@ -3453,7 +3459,10 @@ To change this setting, <ph name="BEGIN_LINK">&lt;resetlink&gt;</ph>reset sync<p
 
       <!-- Main menu items -->
       <message name="IDS_MENU_UPDATE" desc="Menu item for updating chrome. [CHAR_LIMIT=24]">
-        Update Chrome
+        Update Bromite
+      </message>
+      <message name="IDS_MENU_UPDATE_SUMMARY_VULNERABLE" desc="Content description for the menu button when it is covered by the update icon that is displayed when the current version of Bromite is vulnerable.">
+        Update not available. Read more
       </message>
       <message name="IDS_MENU_UPDATE_SUMMARY_DEFAULT" desc="Summary string for update menu item explaining that a newer version of Chrome is available. [CHAR_LIMIT=30]">
         Newer version is available
@@ -3464,6 +3473,18 @@ To change this setting, <ph name="BEGIN_LINK">&lt;resetlink&gt;</ph>reset sync<p
       <message name="IDS_MENU_UPDATE_UNSUPPORTED_SUMMARY_DEFAULT" desc="Summary string for update menu item explaining that the Android version on the device is unsupported. [CHAR_LIMIT=30]">
         Android version is unsupported
       </message>
+      <message name="IDS_MENU_INLINE_UPDATE_DOWNLOADING" desc="Menu item for when Chrome is in the process of updating. [CHAR_LIMIT=24]">
+        Downloading…
+      </message>
+      <message name="IDS_MENU_INLINE_UPDATE_FAILED" desc="Menu item for when Chrome failed to download an update. [CHAR_LIMIT=24]">
+        Couldn’t download
+      </message>
+      <message name="IDS_MENU_INLINE_UPDATE_READY" desc="Menu item text that is shown when Chrome has downloaded an update for itself and is ready to be restarted, which will apply the update. [CHAR_LIMIT=24]">
+        Update ready
+      </message>
+      <message name="IDS_MENU_INLINE_UPDATE_READY_SUMMARY" desc="Summary text that is shown when Chrome has downloaded an update for itself and is ready to be restarted, which will apply the update.  This lets the user know the update will restart. [CHAR_LIMIT=24]">
+        Restart Bromite
+      </message>
       <message name="IDS_MENU_NEW_WINDOW" desc="Menu item for opening a new window. [CHAR_LIMIT=27]">
         New window
       </message>
diff --git a/components/endpoint_fetcher/endpoint_fetcher.cc b/components/endpoint_fetcher/endpoint_fetcher.cc
--- a/components/endpoint_fetcher/endpoint_fetcher.cc
+++ b/components/endpoint_fetcher/endpoint_fetcher.cc
@@ -17,6 +17,11 @@
 #include "services/network/public/cpp/simple_url_loader.h"
 #include "services/network/public/mojom/url_response_head.mojom.h"
 
+// used for the Bromite customization
+#include "net/base/load_flags.h"
+#include "net/http/http_status_code.h"
+#include "services/network/public/cpp/resource_request.h"
+
 namespace {
 const char kContentTypeKey[] = "Content-Type";
 const char kDeveloperKey[] = "X-Developer-Key";
@@ -61,6 +66,7 @@ EndpointFetcher::EndpointFetcher(
       http_method_(http_method),
       content_type_(content_type),
       timeout_ms_(timeout_ms),
+      intercept_redirect_(false),
       post_data_(post_data),
       headers_(headers),
       annotation_tag_(annotation_tag),
@@ -78,6 +84,7 @@ EndpointFetcher::EndpointFetcher(
       http_method_("GET"),
       content_type_(std::string()),
       timeout_ms_(0),
+      intercept_redirect_(false),
       post_data_(std::string()),
       annotation_tag_(annotation_tag),
       url_loader_factory_(url_loader_factory),
@@ -101,6 +108,7 @@ EndpointFetcher::EndpointFetcher(
       http_method_(http_method),
       content_type_(content_type),
       timeout_ms_(timeout_ms),
+      intercept_redirect_(false),
       post_data_(post_data),
       annotation_tag_(annotation_tag),
       url_loader_factory_(url_loader_factory),
@@ -127,6 +135,7 @@ EndpointFetcher::EndpointFetcher(
       http_method_(http_method),
       content_type_(content_type),
       timeout_ms_(timeout_ms),
+      intercept_redirect_(false),
       post_data_(post_data),
       headers_(headers),
       cors_exempt_headers_(cors_exempt_headers),
@@ -138,10 +147,28 @@ EndpointFetcher::EndpointFetcher(
 EndpointFetcher::EndpointFetcher(
     const net::NetworkTrafficAnnotationTag& annotation_tag)
     : timeout_ms_(kDefaultTimeOutMs),
+      intercept_redirect_(false),
       annotation_tag_(annotation_tag),
       identity_manager_(nullptr),
       sanitize_response_(true) {}
 
+// constructor used by Bromite
+EndpointFetcher::EndpointFetcher(
+    const scoped_refptr<network::SharedURLLoaderFactory>& url_loader_factory,
+    const GURL& url,
+    const std::string& http_method,
+    int64_t timeout_ms,
+    const bool intercept_redirect,
+    const net::NetworkTrafficAnnotationTag& annotation_tag)
+    : url_(url),
+      http_method_(http_method),
+      timeout_ms_(timeout_ms),
+      intercept_redirect_(intercept_redirect),
+      annotation_tag_(annotation_tag),
+      url_loader_factory_(url_loader_factory),
+      identity_manager_(nullptr),
+      sanitize_response_(false) {}
+
 EndpointFetcher::~EndpointFetcher() = default;
 
 void EndpointFetcher::Fetch(EndpointFetcherCallback endpoint_fetcher_callback) {
@@ -198,6 +225,8 @@ void EndpointFetcher::PerformRequest(
   resource_request->method = http_method_;
   resource_request->url = url_;
   resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
+  resource_request->load_flags = net::LOAD_BYPASS_CACHE | net::LOAD_DISABLE_CACHE
+                                  | net::LOAD_DO_NOT_SAVE_COOKIES;
   if (base::EqualsCaseInsensitiveASCII(http_method_, "POST")) {
     resource_request->headers.SetHeader(kContentTypeKey, content_type_);
   }
@@ -228,25 +257,52 @@ void EndpointFetcher::PerformRequest(
     default:
       break;
   }
+
+  if (intercept_redirect_ == true) {
+    // will need manual mode to capture the landing page URL
+    resource_request->redirect_mode = network::mojom::RedirectMode::kManual; // default is kFollow
+  }
+
   // TODO(crbug.com/997018) Make simple_url_loader_ local variable passed to
   // callback
   simple_url_loader_ = network::SimpleURLLoader::Create(
       std::move(resource_request), annotation_tag_);
+  simple_url_loader_->SetTimeoutDuration(base::Milliseconds(timeout_ms_));
+  simple_url_loader_->SetAllowHttpErrorResults(true);
+
+  if (!response_) {
+    //RFC: what is this for?
+    response_ = std::make_unique<EndpointResponse>();
+  }
+  if (intercept_redirect_ == true) {
+    // use a callback to capture landing page URL
+    simple_url_loader_->SetOnRedirectCallback(base::BindRepeating(
+      &EndpointFetcher::OnSimpleLoaderRedirect, base::Unretained(this)));
+  }
 
   if (base::EqualsCaseInsensitiveASCII(http_method_, "POST")) {
     simple_url_loader_->AttachStringForUpload(post_data_, content_type_);
   }
   simple_url_loader_->SetRetryOptions(kNumRetries,
                                       network::SimpleURLLoader::RETRY_ON_5XX);
-  simple_url_loader_->SetTimeoutDuration(base::Milliseconds(timeout_ms_));
-  simple_url_loader_->SetAllowHttpErrorResults(true);
-  network::SimpleURLLoader::BodyAsStringCallback body_as_string_callback =
+
+  LOG(INFO) << "performing " << http_method_ << " request to " << url_;
+  if (base::EqualsCaseInsensitiveASCII(http_method_, "HEAD")) {
+    endpoint_fetcher_callback_ = std::move(endpoint_fetcher_callback);
+
+    simple_url_loader_->DownloadHeadersOnly(
+        url_loader_factory_.get(),
+        base::BindOnce(&EndpointFetcher::OnURLLoadComplete,
+                     base::Unretained(this)));
+  } else {
+      network::SimpleURLLoader::BodyAsStringCallback body_as_string_callback =
       base::BindOnce(&EndpointFetcher::OnResponseFetched,
                      weak_ptr_factory_.GetWeakPtr(),
                      std::move(endpoint_fetcher_callback));
-  simple_url_loader_->DownloadToString(
-      url_loader_factory_.get(), std::move(body_as_string_callback),
-      network::SimpleURLLoader::kMaxBoundedStringDownloadSize);
+      simple_url_loader_->DownloadToString(
+         url_loader_factory_.get(), std::move(body_as_string_callback),
+         network::SimpleURLLoader::kMaxBoundedStringDownloadSize);
+  }
 }
 
 void EndpointFetcher::OnResponseFetched(
@@ -315,3 +371,37 @@ void EndpointFetcher::OnSanitizationResult(
 std::string EndpointFetcher::GetUrlForTesting() {
   return url_.spec();
 }
+
+void EndpointFetcher::OnSimpleLoaderRedirect(
+    const net::RedirectInfo& redirect_info,
+    const network::mojom::URLResponseHead& response_head,
+    std::vector<std::string>* removed_headers) {
+  url_ = redirect_info.new_url;
+  if (response_->redirect_url.empty()) {
+    response_->redirect_url = url_.spec();
+    response_->response = std::to_string(redirect_info.status_code);
+  } else {
+    LOG(INFO) << "BromiteUpdater: redirect URL is not empty, status code is " << redirect_info.status_code;
+  }
+
+  std::move(endpoint_fetcher_callback_).Run(std::move(response_));
+}
+
+void EndpointFetcher::OnURLLoadComplete(
+    scoped_refptr<net::HttpResponseHeaders> headers) {
+  if (!endpoint_fetcher_callback_)
+    return;
+
+  if (headers) {
+    if (response_->redirect_url.empty()) {
+      std::string location;
+      if (simple_url_loader_->ResponseInfo()->headers->IsRedirect(&location)) {
+        response_->redirect_url = location;
+      }
+    }
+  }
+
+  std::string net_error = net::ErrorToString(simple_url_loader_->NetError());
+ response_->response = net_error;
+  std::move(endpoint_fetcher_callback_).Run(std::move(response_));
+}
diff --git a/components/endpoint_fetcher/endpoint_fetcher.h b/components/endpoint_fetcher/endpoint_fetcher.h
--- a/components/endpoint_fetcher/endpoint_fetcher.h
+++ b/components/endpoint_fetcher/endpoint_fetcher.h
@@ -16,6 +16,8 @@
 #include "net/traffic_annotation/network_traffic_annotation.h"
 #include "services/data_decoder/public/cpp/json_sanitizer.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
+#include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/mojom/url_response_head.mojom.h"
 
 namespace network {
 struct ResourceRequest;
@@ -38,6 +40,8 @@ enum class FetchErrorType {
 
 struct EndpointResponse {
   std::string response;
+  long last_modified;
+  std::string redirect_url;
   int http_status_code{-1};
   absl::optional<FetchErrorType> error_type;
 };
@@ -90,6 +94,14 @@ class EndpointFetcher {
       const GURL& url,
       const net::NetworkTrafficAnnotationTag& annotation_tag);
 
+  // Constructor if no authentication is needed, with timeout
+  EndpointFetcher(const scoped_refptr<network::SharedURLLoaderFactory>& url_loader_factory,
+                  const GURL& url,
+                  const std::string& http_method,
+                  int64_t timeout_ms,
+                  const bool intercept_redirect,
+                  const net::NetworkTrafficAnnotationTag& annotation_tag);
+
   // Used for tests. Can be used if caller constructs their own
   // url_loader_factory and identity_manager.
   EndpointFetcher(
@@ -144,6 +156,10 @@ class EndpointFetcher {
   void OnSanitizationResult(std::unique_ptr<EndpointResponse> response,
                             EndpointFetcherCallback endpoint_fetcher_callback,
                             data_decoder::JsonSanitizer::Result result);
+  void OnURLLoadComplete(scoped_refptr<net::HttpResponseHeaders> headers);
+  void OnSimpleLoaderRedirect(const net::RedirectInfo& redirect_info,
+                              const network::mojom::URLResponseHead& response_head,
+                              std::vector<std::string>* removed_headers);
 
   enum AuthType { CHROME_API_KEY, OAUTH, NO_AUTH };
   AuthType auth_type_;
@@ -151,10 +167,11 @@ class EndpointFetcher {
   // Members set in constructor to be passed to network::ResourceRequest or
   // network::SimpleURLLoader.
   const std::string oauth_consumer_name_;
-  const GURL url_;
+  GURL url_;
   const std::string http_method_;
   const std::string content_type_;
   int64_t timeout_ms_;
+  const bool intercept_redirect_;
   const std::string post_data_;
   const std::vector<std::string> headers_;
   const std::vector<std::string> cors_exempt_headers_;
@@ -172,6 +189,9 @@ class EndpointFetcher {
       access_token_fetcher_;
   std::unique_ptr<network::SimpleURLLoader> simple_url_loader_;
 
+  EndpointFetcherCallback endpoint_fetcher_callback_;
+  std::unique_ptr<EndpointResponse> response_;
+
   base::WeakPtrFactory<EndpointFetcher> weak_ptr_factory_{this};
 };
 
--
2.25.1
