From: uazo 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 | 272 ++++++++++++++++++ .../omaha/inline/InlineUpdateController.java | 51 ++++ .../inline/InlineUpdateControllerFactory.java | 21 ++ chrome/browser/endpoint_fetcher/BUILD.gn | 2 + .../endpoint_fetcher_android.cc | 50 ++++ .../endpoint_fetcher/EndpointFetcher.java | 26 +- .../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 | 103 ++++++- .../endpoint_fetcher/endpoint_fetcher.h | 23 +- 23 files changed, 933 insertions(+), 50 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 @@ -87,6 +87,8 @@ public class BuildConfig { public static MAYBE_FINAL boolean IS_FOR_TEST MAYBE_FALSE; #endif + public static MAYBE_FINAL String BUILD_TARGET_CPU = _BUILD_TARGET_CPU; + #if defined(_WRITE_CLANG_PROFILING_DATA) public static MAYBE_FINAL boolean WRITE_CLANG_PROFILING_DATA = true; #else 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 @@ -1894,6 +1894,9 @@ if (enable_java_templates && is_android) { sources = [ "//build/android/java/templates/BuildConfig.template" ] defines = [] + # add arch to org.chromium.build.BuildConfig + defines += [ "_BUILD_TARGET_CPU=\"${target_cpu}\"" ] + # Set these even when !use_final_fields so that they have correct default # values within robolectric_binary(), which ignores jar_excluded_patterns. if ((defined(invoker.assertions_implicitly_enabled) && 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 @@ -872,6 +872,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. + 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"; private static final int UNKNOWN_DATE = -2; @@ -157,7 +162,8 @@ public class OmahaBase { } static boolean isDisabled() { - return sDisabledForTesting; + // do not enable version control via Omaha Update Server + return true; } /** @@ -567,6 +573,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); @@ -637,4 +647,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; @@ -36,10 +37,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. */ @@ -49,6 +52,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; } /** @@ -123,6 +132,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. @@ -137,4 +153,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 @@ -12,6 +12,7 @@ import android.view.Choreographer; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.StringRes; import org.chromium.base.BuildInfo; import org.chromium.base.Callback; @@ -135,8 +136,21 @@ public class UpdateMenuItemHelper { Log.e(TAG, "Failed to launch Activity for: %s", mStatus.updateUrl); } 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; } @@ -182,7 +196,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( @@ -237,6 +251,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; @@ -15,7 +16,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.ObserverList; @@ -25,6 +30,10 @@ import org.chromium.base.task.AsyncTask; import org.chromium.base.task.AsyncTask.Status; import org.chromium.base.task.PostTask; import org.chromium.base.task.TaskTraits; +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; @@ -34,30 +43,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. */ @@ -91,6 +107,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) { @@ -99,11 +121,13 @@ public class UpdateStatusProvider { latestVersion = other.latestVersion; latestUnsupportedVersion = other.latestUnsupportedVersion; mIsSimulated = other.mIsSimulated; + mIsInlineSimulated = other.mIsInlineSimulated; } } private final ObserverList> mObservers = new ObserverList<>(); + private final InlineUpdateController mInlineController; private final UpdateQuery mOmahaQuery; private final UpdateSuccessMetrics mMetrics; private @Nullable UpdateStatus mStatus; @@ -171,6 +195,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. @@ -178,12 +226,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); @@ -194,9 +241,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() { @@ -204,19 +271,36 @@ 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) { - mMetrics.analyzeFirstStatus(); mRecordedInitialStatus = true; } @@ -228,6 +312,7 @@ public class UpdateStatusProvider { } private static final class UpdateQuery extends AsyncTask { + static final String TAG = "UpdateStatusProvider"; private final Runnable mCallback; private @Nullable UpdateStatus mStatus; @@ -244,7 +329,7 @@ public class UpdateStatusProvider { protected UpdateStatus doInBackground() { UpdateStatus testStatus = getTestStatus(); if (testStatus != null) return testStatus; - return getRealStatus(); + return getActualStatus(); } @Override @@ -263,6 +348,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: @@ -279,27 +366,33 @@ public class UpdateStatusProvider { return status; } - private UpdateStatus getRealStatus() { + private UpdateStatus getActualStatus() { UpdateStatus status = new UpdateStatus(); - if (VersionNumberGetter.isNewerVersionAvailable()) { - status.updateUrl = MarketURLGetter.getMarketUrl(); - status.latestVersion = VersionNumberGetter.getInstance().getLatestKnownVersion(); - - boolean allowedToUpdate = checkForSufficientStorage() - && PackageUtils.isPackageInstalled( - GooglePlayServicesUtil.GOOGLE_PLAY_STORE_PACKAGE); - 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 @@ -50,7 +50,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,272 @@ +// 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 . + +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.base.task.TaskTraits; +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.ui.base.PageTransition; +import org.chromium.net.NetworkTrafficAnnotationTag; + +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 static final NetworkTrafficAnnotationTag TRAFFIC_ANNOTATION = + NetworkTrafficAnnotationTag.createComplete("bromite_inline_update_controller", + "semantics {" + + " sender: 'Bromite Inline Update (Android)'" + + " description:" + + " 'Check for update'" + + " trigger: 'This request is made once, on first run'" + + " data: 'None.'" + + " destination: OTHER" + + " internal {" + + " contacts {" + + " email: 'uazo@users.noreply.github.com'" + + " }" + + " contacts {" + + " email: 'uazo@users.noreply.github.com'" + + " }" + + " }" + + " user_data {" + + " type: NONE" + + " }" + + " last_reviewed: '2023-01-01'" + + "}" + + "policy {" + + " cookies_allowed: NO" + + " setting: 'Can be disabled in Settings.'" + + " policy_exception_justification: 'Not implemented.'" + + "}"); + + 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 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, TRAFFIC_ANNOTATION); + } + + private void checkLatestUpstreamVersion(final Callback 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, TRAFFIC_ANNOTATION); + } + + private void postStatus(@UpdateStatusProvider.UpdateState int status, String updateUrl) { + mUpdateState = status; + mUpdateUrl = updateUrl; + PostTask.postTask(TaskTraits.UI_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 @@ -19,6 +19,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", ] } @@ -26,5 +27,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" @@ -31,6 +32,24 @@ static void OnEndpointFetcherComplete( base::android::AttachCurrentThread(), std::move(endpoint_response->response)))); } + +static void OnEndpointFetcherHeadComplete( + const base::android::JavaRef& 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 endpoint_fetcher, + std::unique_ptr 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 @@ -113,6 +132,7 @@ static void JNI_EndpointFetcher_NativeFetchWithNoAuth( JNIEnv* env, const base::android::JavaParamRef& jprofile, const base::android::JavaParamRef& jurl, + jlong jtimeout, jboolean intercept_redirect, jint jannotation_hash_code, const base::android::JavaParamRef& jcallback) { auto endpoint_fetcher = std::make_unique( @@ -120,6 +140,9 @@ static void JNI_EndpointFetcher_NativeFetchWithNoAuth( ->GetDefaultStoragePartition() ->GetURLLoaderFactoryForBrowserProcess(), GURL(base::android::ConvertJavaStringToUTF8(env, jurl)), + "GET", + jtimeout, + intercept_redirect, net::NetworkTrafficAnnotationTag::FromJavaAnnotation( jannotation_hash_code)); auto* const endpoint_fetcher_ptr = endpoint_fetcher.get(); @@ -131,3 +154,30 @@ static void JNI_EndpointFetcher_NativeFetchWithNoAuth( std::move(endpoint_fetcher)), nullptr); } + +static void JNI_EndpointFetcher_NativeHeadWithNoAuth( + JNIEnv* env, + const base::android::JavaParamRef& jprofile, + const base::android::JavaParamRef& jurl, + jlong jtimeout, jboolean intercept_redirect, + jint jannotation_hash_code, + const base::android::JavaParamRef& jcallback) { + auto endpoint_fetcher = std::make_unique( + ProfileAndroid::FromProfileAndroid(jprofile) + ->GetDefaultStoragePartition() + ->GetURLLoaderFactoryForBrowserProcess(), + GURL(base::android::ConvertJavaStringToUTF8(env, jurl)), + "HEAD", + jtimeout, + intercept_redirect, + net::NetworkTrafficAnnotationTag::FromJavaAnnotation( + jannotation_hash_code)); + auto* const endpoint_fetcher_ptr = endpoint_fetcher.get(); + endpoint_fetcher_ptr->PerformRequest( + base::BindOnce(&OnEndpointFetcherHeadComplete, + base::android::ScopedJavaGlobalRef(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,24 @@ public final class EndpointFetcher { postData, timeout, headers, annotation.getHashCode(), callback); } + @MainThread + public static void nativeHeadWithNoAuth( + Callback callback, Profile profile, + String url, long timeout, boolean allow_redirect, + NetworkTrafficAnnotationTag annotation) { + EndpointFetcherJni.get().nativeHeadWithNoAuth( + profile, url, timeout, allow_redirect, annotation.getHashCode(), callback); + } + + @MainThread + public static void nativeFetchWithNoAuth( + Callback callback, Profile profile, + String url, long timeout, boolean allow_redirect, + NetworkTrafficAnnotationTag annotation) { + EndpointFetcherJni.get().nativeFetchWithNoAuth( + profile, url, timeout, allow_redirect, annotation.getHashCode(), callback); + } + @NativeMethods public interface Natives { void nativeFetchOAuth(Profile profile, String oathConsumerName, String url, @@ -78,7 +96,13 @@ public final class EndpointFetcher { void nativeFetchChromeAPIKey(Profile profile, String url, String httpsMethod, String contentType, String postData, long timeout, String[] headers, int annotationHashCode, Callback callback); - void nativeFetchWithNoAuth(Profile profile, String url, int annotationHashCode, + void nativeFetchWithNoAuth( + Profile profile, String url, long timeout, boolean allow_redirect, + int annotationHashCode, Callback callback); + void nativeHeadWithNoAuth( + Profile profile, String url, long timeout, boolean allow_redirect, + int annotationHashCode, + Callback 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 @@ -271,6 +271,7 @@ const base::Feature* const kFeaturesExposedToJava[] = { &kNotificationPermissionBottomSheet, &kPageAnnotationsService, &kPreconnectOnTabCreation, + &kInlineUpdateFlow, &kBookmarksImprovedSaveFlow, &kBookmarksRefresh, &kOmahaMinSdkVersionAndroid, @@ -722,6 +723,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 @@ -118,6 +118,7 @@ BASE_DECLARE_FEATURE(kGridTabSwitcherLandscapeAspectRatioPhones); BASE_DECLARE_FEATURE(kHideTabOnTabSwitcher); BASE_DECLARE_FEATURE(kImprovedIncognitoScreenshot); BASE_DECLARE_FEATURE(kIncognitoReauthenticationForAndroid); +BASE_DECLARE_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 @@ -305,6 +305,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 @@ -2216,6 +2216,12 @@ Your Google account may have other forms of browsing history like searches and a Chrome updates are no longer supported for this version of Android + + Allow checking for updates + + + Notify about new releases by periodically checking for their availability + @@ -3840,7 +3846,10 @@ To change this setting, <resetlink>reset sync

- Update Chrome + Update Bromite + + + Update not available. Read more Newer version is available @@ -3851,6 +3860,18 @@ To change this setting, <resetlink>reset sync

Android version is unsupported + + Downloading… + + + Couldn’t download + + + Update ready + + + Restart Bromite + New window 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 @@ -18,6 +18,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"; @@ -64,6 +69,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), @@ -82,6 +88,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), @@ -107,6 +114,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), @@ -134,6 +142,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), @@ -146,11 +155,29 @@ EndpointFetcher::EndpointFetcher( EndpointFetcher::EndpointFetcher( const net::NetworkTrafficAnnotationTag& annotation_tag) : timeout_ms_(kDefaultTimeOutMs), + intercept_redirect_(false), annotation_tag_(annotation_tag), identity_manager_(nullptr), consent_level_(absl::nullopt), sanitize_response_(true) {} +// constructor used by Bromite +EndpointFetcher::EndpointFetcher( + const scoped_refptr& 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) { @@ -209,6 +236,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_); } @@ -239,25 +268,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(); + } + 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( @@ -338,3 +394,38 @@ void EndpointFetcher::OnSanitizationResult( std::string EndpointFetcher::GetUrlForTesting() { return url_.spec(); } + +void EndpointFetcher::OnSimpleLoaderRedirect( + const GURL& url_before_redirect, + const net::RedirectInfo& redirect_info, + const network::mojom::URLResponseHead& response_head, + std::vector* 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 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 @@ -17,6 +17,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; @@ -39,6 +41,8 @@ enum class FetchErrorType { struct EndpointResponse { std::string response; + long last_modified; + std::string redirect_url; int http_status_code{-1}; absl::optional error_type; }; @@ -92,6 +96,14 @@ class EndpointFetcher { const GURL& url, const net::NetworkTrafficAnnotationTag& annotation_tag); + // Constructor if no authentication is needed, with timeout + EndpointFetcher(const scoped_refptr& 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( @@ -147,6 +159,11 @@ class EndpointFetcher { void OnSanitizationResult(std::unique_ptr response, EndpointFetcherCallback endpoint_fetcher_callback, data_decoder::JsonSanitizer::Result result); + void OnURLLoadComplete(scoped_refptr headers); + void OnSimpleLoaderRedirect(const GURL& url_before_redirect, + const net::RedirectInfo& redirect_info, + const network::mojom::URLResponseHead& response_head, + std::vector* removed_headers); enum AuthType { CHROME_API_KEY, OAUTH, NO_AUTH }; AuthType auth_type_; @@ -154,10 +171,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 headers_; const std::vector cors_exempt_headers_; @@ -181,6 +199,9 @@ class EndpointFetcher { access_token_fetcher_; std::unique_ptr simple_url_loader_; + EndpointFetcherCallback endpoint_fetcher_callback_; + std::unique_ptr response_; + base::WeakPtrFactory weak_ptr_factory_{this}; }; -- 2.25.1