0

Add default browser promo dialogs

Show default browser promo dialogs on launch to encourage users
to set Chrome as their default browser.

Bug: 1090103
Change-Id: I9ebbcdb30dbe68b84035a0e324205219b5ccf267
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2228191
Commit-Queue: Lijin Shen <lazzzis@google.com>
Reviewed-by: Yaron Friedman <yfriedman@chromium.org>
Reviewed-by: Theresa  <twellington@chromium.org>
Reviewed-by: Pavel Yatsuk <pavely@chromium.org>
Cr-Commit-Position: refs/heads/master@{#777507}
This commit is contained in:
Lijin Shen
2020-06-11 21:53:22 +00:00
committed by Commit Bot
parent 20a8611e24
commit 0ebcc795a2
34 changed files with 914 additions and 7 deletions

@ -184,6 +184,7 @@ android_resources("chrome_app_java_resources") {
"//chrome/android/webapk/libs/common:splash_resources",
"//chrome/app:java_strings_grd",
"//chrome/browser/ui/android/appmenu:java_resources",
"//chrome/browser/ui/android/default_browser_promo:java_resources",
"//chrome/browser/ui/android/favicon:java_resources",
"//chrome/browser/ui/android/strings:ui_strings_grd",
"//chrome/browser/ui/messages/android:java_resources",
@ -318,6 +319,7 @@ android_library("chrome_java") {
"//chrome/browser/ui:infobar_android_enums_java",
"//chrome/browser/ui/android/appmenu:factory_java",
"//chrome/browser/ui/android/appmenu:java",
"//chrome/browser/ui/android/default_browser_promo:java",
"//chrome/browser/ui/android/favicon:java",
"//chrome/browser/ui/android/native_page:java",
"//chrome/browser/ui/messages/android:java",
@ -744,6 +746,8 @@ junit_binary("chrome_junit_tests") {
"//chrome/browser/thumbnail:java",
"//chrome/browser/ui/android/appmenu:java",
"//chrome/browser/ui/android/appmenu/internal:junit",
"//chrome/browser/ui/android/default_browser_promo:java",
"//chrome/browser/ui/android/default_browser_promo:junit",
"//chrome/browser/ui/android/favicon:java",
"//chrome/browser/ui/android/native_page:java",
"//chrome/browser/ui/messages/android:java",
@ -920,6 +924,8 @@ android_library("chrome_test_java") {
"//chrome/browser/thumbnail:javatests",
"//chrome/browser/ui/android/appmenu:java",
"//chrome/browser/ui/android/appmenu/test:test_support_java",
"//chrome/browser/ui/android/default_browser_promo:java",
"//chrome/browser/ui/android/default_browser_promo:javatests",
"//chrome/browser/ui/android/favicon:java",
"//chrome/browser/ui/android/native_page:java",
"//chrome/browser/ui/messages/android:java",

@ -9,6 +9,7 @@ include_rules = [
"+chrome/browser/thumbnail/generator/android/java",
"+chrome/browser/ui/android/appmenu",
"-chrome/browser/ui/android/appmenu/internal",
"+chrome/browser/ui/android/default_browser_promo",
"+chrome/browser/ui/messages/android/java",
"+chrome/browser/download/android/java",
"+chrome/browser/image_fetcher/android/java",

@ -13,6 +13,7 @@ import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
import org.chromium.chrome.browser.ui.default_browser_promo.DefaultBrowserPromoUtils;
/**
* Records the behavior metrics after an ACTION_MAIN intent is received.
@ -105,6 +106,8 @@ public class MainIntentBehaviorMetrics {
RecordHistogram.recordEnumeratedHistogram("MobileStartup.LaunchType",
isLaunchFromIcon ? LAUNCH_FROM_ICON : LAUNCH_NOT_FROM_ICON, LAUNCH_BOUNDARY);
DefaultBrowserPromoUtils.incrementSessionCount();
ThreadUtils.getUiThreadHandler().removeCallbacks(mLogLaunchRunnable);
}
}

@ -45,6 +45,7 @@ import org.chromium.chrome.browser.toolbar.ToolbarButtonInProductHelpController;
import org.chromium.chrome.browser.toolbar.bottom.BottomToolbarConfiguration;
import org.chromium.chrome.browser.ui.RootUiCoordinator;
import org.chromium.chrome.browser.ui.appmenu.AppMenuHandler;
import org.chromium.chrome.browser.ui.default_browser_promo.DefaultBrowserPromoUtils;
import org.chromium.chrome.browser.ui.tablet.emptybackground.EmptyBackgroundViewWrapper;
import org.chromium.chrome.browser.vr.VrModuleProvider;
import org.chromium.content_public.browser.UiThreadTaskTraits;
@ -348,6 +349,9 @@ public class TabbedRootUiCoordinator extends RootUiCoordinator implements Native
mActivity, mActivity.getTabModelSelector().getCurrentModel().isIncognito())) {
return true;
}
if (DefaultBrowserPromoUtils.prepareLaunchPromoIfNeeded(mActivity)) {
return true;
}
return LanguageAskPrompt.maybeShowLanguageAskPrompt(mActivity);
}

@ -5596,6 +5596,11 @@ const FeatureEntry kFeatureEntries[] = {
#endif // !defined(OS_ANDROID)
#if defined(OS_ANDROID)
{"android-default-browser-promo",
flag_descriptions::kAndroidDefaultBrowserPromoName,
flag_descriptions::kAndroidDefaultBrowserPromoDescription, kOsAndroid,
FEATURE_VALUE_TYPE(chrome::android::kAndroidDefaultBrowserPromo)},
{"android-multiple-display", flag_descriptions::kAndroidMultipleDisplayName,
flag_descriptions::kAndroidMultipleDisplayDescription, kOsAndroid,
FEATURE_VALUE_TYPE(chrome::android::kAndroidMultipleDisplay)},

@ -93,6 +93,11 @@
"owners": [ "kdillon@chromium.org" ],
"expiry_milestone": 88
},
{
"name": "android-default-browser-promo",
"owners": [ "lazzzis@google.com", "twellington" ],
"expiry_milestone": 91
},
{
"name": "android-files-in-files-app",
"owners": [ "fukino" ],

@ -2333,6 +2333,10 @@ const char kAndroidAutofillAccessibilityName[] = "Autofill Accessibility";
const char kAndroidAutofillAccessibilityDescription[] =
"Enable accessibility for autofill popup.";
const char kAndroidDefaultBrowserPromoName[] = "Default Browser Promo";
const char kAndroidDefaultBrowserPromoDescription[] =
"Shows a promo dialog to set Chrome as the default browser";
const char kAndroidMultipleDisplayName[] = "Multiple Display";
const char kAndroidMultipleDisplayDescription[] =
"When enabled, tabs can be moved to the secondary display.";

@ -1358,6 +1358,9 @@ extern const char kEnableVulkanDescription[];
extern const char kAndroidAutofillAccessibilityName[];
extern const char kAndroidAutofillAccessibilityDescription[];
extern const char kAndroidDefaultBrowserPromoName[];
extern const char kAndroidDefaultBrowserPromoDescription[];
extern const char kAndroidMultipleDisplayName[];
extern const char kAndroidMultipleDisplayDescription[];

@ -102,6 +102,7 @@ const base::Feature* kFeaturesExposedToJava[] = {
&kAllowNewIncognitoTabIntents,
&kAllowRemoteContextForNotifications,
&kAndroidBlockIntentNonSafelistedHeaders,
&kAndroidDefaultBrowserPromo,
&kAndroidMultipleDisplay,
&kAndroidNightModeTabReparenting,
&kAndroidPartnerCustomizationPhenotype,
@ -273,6 +274,9 @@ const base::Feature kAdjustWebApkInstallationSpace = {
const base::Feature kAndroidBlockIntentNonSafelistedHeaders{
"AndroidBlockIntentNonSafelistedHeaders", base::FEATURE_ENABLED_BY_DEFAULT};
const base::Feature kAndroidDefaultBrowserPromo{
"AndroidDefaultBrowserPromo", base::FEATURE_DISABLED_BY_DEFAULT};
const base::Feature kAndroidMultipleDisplay{"AndroidMultipleDisplay",
base::FEATURE_ENABLED_BY_DEFAULT};

@ -16,6 +16,7 @@ extern const base::Feature kAdjustWebApkInstallationSpace;
extern const base::Feature kAllowNewIncognitoTabIntents;
extern const base::Feature kAllowRemoteContextForNotifications;
extern const base::Feature kAndroidBlockIntentNonSafelistedHeaders;
extern const base::Feature kAndroidDefaultBrowserPromo;
extern const base::Feature kAndroidMultipleDisplay;
extern const base::Feature kAndroidNightModeTabReparenting;
extern const base::Feature kAndroidPartnerCustomizationPhenotype;

@ -205,6 +205,7 @@ public abstract class ChromeFeatureList {
public static final String ADJUST_WEBAPK_INSTALLATION_SPACE = "AdjustWebApkInstallationSpace";
public static final String ANDROID_BLOCK_INTENT_NON_SAFELISTED_HEADERS =
"AndroidBlockIntentNonSafelistedHeaders";
public static final String ANDROID_DEFAULT_BROWSER_PROMO = "AndroidDefaultBrowserPromo";
public static final String ANDROID_MULTIPLE_DISPLAY = "AndroidMultipleDisplay";
public static final String ANDROID_NIGHT_MODE_TAB_REPARENTING =
"AndroidNightModeTabReparenting";

@ -279,6 +279,14 @@ public final class ChromePreferenceKeys {
public static final String DATA_REDUCTION_SITE_BREAKDOWN_ALLOWED_DATE =
"data_reduction_site_breakdown_allowed_date";
/**
* Keys used to save whether it is ready to promo.
*/
public static final String DEFAULT_BROWSER_PROMO_SESSION_COUNT =
"Chrome.DefaultBrowserPromo.SessionCount";
public static final String DEFAULT_BROWSER_PROMO_IS_PROMOED =
"Chrome.DefaultBrowserPromo.IsPromoed";
public static final String DOWNLOAD_AUTO_RESUMPTION_ATTEMPT_LEFT = "ResumptionAttemptLeft";
public static final String DOWNLOAD_FOREGROUND_SERVICE_OBSERVERS = "ForegroundServiceObservers";
public static final String DOWNLOAD_IS_DOWNLOAD_HOME_ENABLED =
@ -761,6 +769,8 @@ public final class ChromePreferenceKeys {
CONTEXT_MENU_OPEN_IN_EPHEMERAL_TAB_CLICKED,
CONTEXT_MENU_SEARCH_WITH_GOOGLE_LENS_CLICKED,
CRYPTID_LAST_RENDER_TIMESTAMP,
DEFAULT_BROWSER_PROMO_IS_PROMOED,
DEFAULT_BROWSER_PROMO_SESSION_COUNT,
EXPLORE_OFFLINE_CONTENT_AVAILABILITY_STATUS,
FLAGS_CACHED.pattern(),
FLAGS_CACHED_DUET_TABSTRIP_INTEGRATION_ANDROID_ENABLED,

@ -0,0 +1,73 @@
# Copyright 2020 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.
import("//build/config/android/rules.gni")
android_library("java") {
sources = [
"java/src/org/chromium/chrome/browser/ui/default_browser_promo/DefaultBrowserPromoDialog.java",
"java/src/org/chromium/chrome/browser/ui/default_browser_promo/DefaultBrowserPromoManager.java",
"java/src/org/chromium/chrome/browser/ui/default_browser_promo/DefaultBrowserPromoUtils.java",
]
deps = [
":java_resources",
"//base:base_java",
"//chrome/browser/flags:java",
"//chrome/browser/preferences:java",
"//chrome/browser/profiles/android:java",
"//components/browser_ui/widget/android:java",
]
}
android_resources("java_resources") {
custom_package = "org.chromium.chrome.browser.ui.default_browser_promo"
sources = [
"java/res/drawable/default_browser_promo_illustration.xml",
"java/res/drawable/ic_illustration_aroundlogo.xml",
]
deps = [
"//chrome/browser/ui/android/strings:ui_strings_grd",
"//components/browser_ui/widget/android:java_resources",
"//ui/android:ui_java_resources",
]
}
java_library("junit") {
# Skip platform checks since Robolectric depends on requires_android targets.
bypass_platform_checks = true
testonly = true
sources = [ "java/src/org/chromium/chrome/browser/ui/default_browser_promo/DefaultBrowserPromoUtilsTest.java" ]
deps = [
":java",
"//base:base_java",
"//base:base_junit_test_support",
"//third_party/android_deps:robolectric_all_java",
"//third_party/junit",
"//third_party/mockito:mockito_java",
]
}
android_library("javatests") {
testonly = true
sources = [ "java/src/org/chromium/chrome/browser/ui/default_browser_promo/DefaultBrowserPromoManagerTest.java" ]
deps = [
":java",
"//base:base_java",
"//base:base_java_test_support",
"//chrome/android:chrome_java",
"//chrome/test/android:chrome_java_test_support",
"//components/browser_ui/widget/android:java",
"//content/public/test/android:content_java_test_support",
"//third_party/android_deps:android_support_v7_appcompat_java",
"//third_party/android_deps:androidx_preference_preference_java",
"//third_party/android_deps:espresso_java",
"//third_party/android_support_test_runner:rules_java",
"//third_party/android_support_test_runner:runner_java",
"//third_party/hamcrest:hamcrest_java",
"//third_party/junit:junit",
"//ui/android:ui_java_test_support",
]
}

@ -0,0 +1,12 @@
noparent = True
include_rules = [
"+base/android",
"+base/test/android",
"+content/public/test/android",
"+chrome/browser/flags",
"+chrome/browser/profiles/android",
"+chrome/browser/preferences",
"+chrome/test/android",
"+components/browser_ui/widget/android",
"+ui/android",
]

@ -0,0 +1,6 @@
lazzzis@google.com
twellington@chromium.org
# TEAM: chrome-android-app@chromium.org
# COMPONENT: UI>Browser>Mobile>Messages
# OS: Android

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 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. -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:height="122dp"
android:width="127dp"
android:drawable="@drawable/ic_illustration_aroundlogo" />
<item
android:top="27dp"
android:left="27dp"
android:height="65dp"
android:width="65dp"
android:drawable="@mipmap/app_icon"/>
</layer-list>

File diff suppressed because one or more lines are too long

@ -0,0 +1,123 @@
// Copyright 2020 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.ui.default_browser_promo;
import android.app.Activity;
import android.content.DialogInterface;
import android.view.View;
import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.BuildInfo;
import org.chromium.components.browser_ui.widget.PromoDialog;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* The promo dialog guiding how to set Chrome as the default browser.
*/
public class DefaultBrowserPromoDialog extends PromoDialog {
@IntDef({DialogStyle.ROLE_MANAGER, DialogStyle.DISAMBIGUATION_SHEET,
DialogStyle.SYSTEM_SETTINGS})
@Retention(RetentionPolicy.SOURCE)
public @interface DialogStyle {
int ROLE_MANAGER = 0;
int DISAMBIGUATION_SHEET = 1;
int SYSTEM_SETTINGS = 2;
}
private final int mDialogStyle;
private final Runnable mOnOK;
private Runnable mOnCancel;
/**
* Building a {@link DefaultBrowserPromoDialog}.
* @param activity The activity to display dialog.
* @param dialogStyle The type of dialog.
* @param onOK The {@link Runnable} on user's agreeing to change default.
* @param onCancel The {@link Runnable} on user's refusing or dismissing the dialog.
* @return
*/
public static DefaultBrowserPromoDialog createDialog(
Activity activity, @DialogStyle int dialogStyle, Runnable onOK, Runnable onCancel) {
return new DefaultBrowserPromoDialog(activity, dialogStyle, onOK, onCancel);
}
private DefaultBrowserPromoDialog(
Activity activity, @DialogStyle int style, Runnable onOK, Runnable onCancel) {
super(activity);
mDialogStyle = style;
mOnOK = onOK;
mOnCancel = onCancel;
setOnDismissListener(this);
}
@Override
@VisibleForTesting
public DialogParams getDialogParams() {
DialogParams params = new DialogParams();
Activity activity = getOwnerActivity();
assert activity != null;
String appName = BuildInfo.getInstance().hostPackageLabel;
params.vectorDrawableResource = R.drawable.default_browser_promo_illustration;
params.headerCharSequence =
activity.getString(R.string.default_browser_promo_dialog_title, appName);
String desc =
activity.getString(R.string.default_browser_promo_dialog_desc, appName) + "\n\n";
String steps;
String primaryButtonText;
if (mDialogStyle == DialogStyle.ROLE_MANAGER) {
steps = activity.getString(
R.string.default_browser_promo_dialog_role_manager_steps, appName);
primaryButtonText = activity.getString(
R.string.default_browser_promo_dialog_choose_chrome_button, appName);
} else if (mDialogStyle == DialogStyle.DISAMBIGUATION_SHEET) {
steps = activity.getString(
R.string.default_browser_promo_dialog_disambiguation_sheet_steps, appName);
primaryButtonText = activity.getString(
R.string.default_browser_promo_dialog_choose_chrome_button, appName);
} else {
assert mDialogStyle == DialogStyle.SYSTEM_SETTINGS;
steps = activity.getString(
R.string.default_browser_promo_dialog_system_settings_steps, appName);
primaryButtonText =
activity.getString(R.string.default_browser_promo_dialog_go_to_settings_button);
}
params.subheaderCharSequence = desc + steps;
params.primaryButtonCharSequence = primaryButtonText;
params.secondaryButtonStringResource = R.string.no_thanks;
return params;
}
@Override
public void onDismiss(DialogInterface dialog) {
// Can be dismissed by pressing the back button.
if (mOnCancel != null) mOnCancel.run();
}
@Override
public void onClick(View view) {
super.onClick(view);
int id = view.getId();
if (id == R.id.button_primary) {
mOnCancel = null;
mOnOK.run();
dismiss();
} else if (id == R.id.button_secondary) {
if (mOnCancel != null) mOnCancel.run();
mOnCancel = null;
dismiss();
}
}
@VisibleForTesting
public int getDialogStyleForTesting() {
return mDialogStyle;
}
}

@ -0,0 +1,103 @@
// Copyright 2020 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.ui.default_browser_promo;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.role.RoleManager;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.provider.Settings;
import androidx.annotation.VisibleForTesting;
/**
* Manage all types of default browser promo dialogs and listen to the activity state change to
* trigger dialogs.
*/
public class DefaultBrowserPromoManager {
private final Activity mActivity;
private DefaultBrowserPromoDialog mDialog;
/**
* @param activity Activity to show promo dialogs.
* @return A {@link DefaultBrowserPromoManager} displaying dialogs based on android version and
* current default browser state in system.
*/
public static DefaultBrowserPromoManager create(Activity activity) {
return new DefaultBrowserPromoManager(activity);
}
private DefaultBrowserPromoManager(Activity activity) {
mActivity = activity;
}
/**
* @param state The current {@link DefaultBrowserPromoUtils.DefaultBrowserState} in system.
*/
public void promo(@DefaultBrowserPromoUtils.DefaultBrowserState int state) {
promoInternal(state, Build.VERSION.SDK_INT);
}
private void promoInternal(
@DefaultBrowserPromoUtils.DefaultBrowserState int state, int sdkInt) {
if (sdkInt >= Build.VERSION_CODES.Q) {
promoByRoleManager();
} else if (state == DefaultBrowserPromoUtils.DefaultBrowserState.NO_DEFAULT) {
promoByDisambiguationSheet();
} else if (sdkInt >= Build.VERSION_CODES.M) {
promoBySystemSettings();
}
}
@SuppressLint({"WrongConstant", "NewApi"})
private void promoByRoleManager() {
showDialog(DefaultBrowserPromoDialog.DialogStyle.ROLE_MANAGER, () -> {
RoleManager roleManager =
(RoleManager) mActivity.getSystemService(Context.ROLE_SERVICE);
boolean isRoleAvailable = roleManager.isRoleAvailable(RoleManager.ROLE_BROWSER);
boolean isRoleHeld = roleManager.isRoleHeld(RoleManager.ROLE_BROWSER);
// TODO(crbug.com/1090103): check the condition before deciding
// to show promo and remove the assertion.
assert isRoleAvailable && !isRoleHeld;
Intent intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_BROWSER);
mActivity.startActivityForResult(intent, 0);
});
}
private void promoBySystemSettings() {
showDialog(DefaultBrowserPromoDialog.DialogStyle.SYSTEM_SETTINGS, () -> {
Intent intent = new Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS);
mActivity.startActivity(intent);
});
}
private void promoByDisambiguationSheet() {
showDialog(DefaultBrowserPromoDialog.DialogStyle.DISAMBIGUATION_SHEET, () -> {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_BROWSABLE);
mActivity.startActivity(intent);
});
}
private void showDialog(@DefaultBrowserPromoDialog.DialogStyle int style, Runnable okCallback) {
mDialog = DefaultBrowserPromoDialog.createDialog(mActivity, style, okCallback, null);
mDialog.show();
}
@VisibleForTesting
public DefaultBrowserPromoDialog getDialogForTesting() {
return mDialog;
}
@VisibleForTesting
public void promoForTesting(
@DefaultBrowserPromoUtils.DefaultBrowserState int state, int sdkInt) {
promoInternal(state, sdkInt);
}
}

@ -0,0 +1,174 @@
// Copyright 2020 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.ui.default_browser_promo;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import android.app.Activity;
import android.os.Build;
import android.support.test.rule.ActivityTestRule;
import androidx.test.filters.MediumTest;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.BuildInfo;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.components.browser_ui.widget.PromoDialog;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.ui.test.util.DummyUiActivity;
/**
* Instrument test for {@link DefaultBrowserPromoManager}.
*/
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class DefaultBrowserPromoManagerTest {
@Rule
public ActivityTestRule<DummyUiActivity> mRule = new ActivityTestRule<>(DummyUiActivity.class);
private DefaultBrowserPromoManager mManager;
private Activity mActivity;
private String mAppName;
@Before
public void setUp() {
mActivity = mRule.getActivity();
mManager = DefaultBrowserPromoManager.create(mActivity);
mAppName = BuildInfo.getInstance().hostPackageLabel;
}
@Test
@MediumTest
public void testPromoByRoleManager() {
TestThreadUtils.runOnUiThreadBlocking(() -> {
mManager.promoForTesting(
DefaultBrowserPromoUtils.DefaultBrowserState.NO_DEFAULT, Build.VERSION_CODES.Q);
});
DefaultBrowserPromoDialog dialog = mManager.getDialogForTesting();
Assert.assertEquals("Dialog should be of role manager style on Q+",
dialog.getDialogStyleForTesting(),
DefaultBrowserPromoDialog.DialogStyle.ROLE_MANAGER);
// test role manager style
PromoDialog.DialogParams params = dialog.getDialogParams();
Assert.assertEquals(
mActivity.getString(R.string.default_browser_promo_dialog_title, mAppName),
params.headerCharSequence);
Assert.assertEquals(
mActivity.getString(R.string.default_browser_promo_dialog_desc, mAppName) + "\n\n"
+ mActivity.getString(
R.string.default_browser_promo_dialog_role_manager_steps, mAppName),
params.subheaderCharSequence);
Assert.assertEquals(
mActivity.getString(
R.string.default_browser_promo_dialog_choose_chrome_button, mAppName),
params.primaryButtonCharSequence);
checkDialogVisibility();
}
@Test
@MediumTest
public void testPromoBySystemSettingsOnL() {
TestThreadUtils.runOnUiThreadBlocking(() -> {
mManager.promoForTesting(DefaultBrowserPromoUtils.DefaultBrowserState.OTHER_DEFAULT,
Build.VERSION_CODES.LOLLIPOP);
});
DefaultBrowserPromoDialog dialog = mManager.getDialogForTesting();
Assert.assertNull("Dialog of system settings style should not be displayed on L", dialog);
}
@Test
@MediumTest
public void testPromoBySystemSettingsOnP() {
TestThreadUtils.runOnUiThreadBlocking(() -> {
mManager.promoForTesting(DefaultBrowserPromoUtils.DefaultBrowserState.OTHER_DEFAULT,
Build.VERSION_CODES.P);
});
DefaultBrowserPromoDialog dialog = mManager.getDialogForTesting();
Assert.assertEquals(
"Dialog should be of system settings style on P-, when there is another default in system",
dialog.getDialogStyleForTesting(),
DefaultBrowserPromoDialog.DialogStyle.SYSTEM_SETTINGS);
// test role manager style
PromoDialog.DialogParams params = dialog.getDialogParams();
Assert.assertEquals(
mActivity.getString(R.string.default_browser_promo_dialog_title, mAppName),
params.headerCharSequence);
Assert.assertEquals(
mActivity.getString(R.string.default_browser_promo_dialog_desc, mAppName) + "\n\n"
+ mActivity.getString(
R.string.default_browser_promo_dialog_system_settings_steps,
mAppName),
params.subheaderCharSequence);
Assert.assertEquals(
mActivity.getString(R.string.default_browser_promo_dialog_go_to_settings_button),
params.primaryButtonCharSequence);
checkDialogVisibility();
}
@Test
@MediumTest
public void testPromoByDisambiguationSheet() {
TestThreadUtils.runOnUiThreadBlocking(() -> {
mManager.promoForTesting(
DefaultBrowserPromoUtils.DefaultBrowserState.NO_DEFAULT, Build.VERSION_CODES.P);
});
DefaultBrowserPromoDialog dialog = mManager.getDialogForTesting();
Assert.assertEquals(
"Dialog should be of disambiguation sheet style on P-, when there is no default in system",
dialog.getDialogStyleForTesting(),
DefaultBrowserPromoDialog.DialogStyle.DISAMBIGUATION_SHEET);
// test role manager style
PromoDialog.DialogParams params = dialog.getDialogParams();
Assert.assertEquals(
mActivity.getString(R.string.default_browser_promo_dialog_title, mAppName),
params.headerCharSequence);
Assert.assertEquals(
mActivity.getString(R.string.default_browser_promo_dialog_desc, mAppName) + "\n\n"
+ mActivity.getString(
R.string.default_browser_promo_dialog_disambiguation_sheet_steps,
mAppName),
params.subheaderCharSequence);
Assert.assertEquals(
mActivity.getString(
R.string.default_browser_promo_dialog_choose_chrome_button, mAppName),
params.primaryButtonCharSequence);
checkDialogVisibility();
}
private void checkDialogVisibility() {
onView(withId(R.id.promo_dialog_layout)).check(matches(isDisplayed()));
// dismiss the dialog
onView(withId(R.id.button_secondary)).perform(click());
onView(withId(R.id.promo_dialog_layout)).check((v, noMatchingViewException) -> {
Assert.assertNotNull("Promo dialog should be dismissed by clicking on secondary button",
noMatchingViewException);
});
}
}

@ -0,0 +1,153 @@
// Copyright 2020 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.ui.default_browser_promo;
import android.app.Activity;
import android.content.pm.ResolveInfo;
import android.text.TextUtils;
import androidx.annotation.IntDef;
import org.chromium.base.ContextUtils;
import org.chromium.base.PackageManagerUtils;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* A utility class providing information regarding states of default browser.
*/
public class DefaultBrowserPromoUtils {
@IntDef({DefaultBrowserState.CHROME_DEFAULT, DefaultBrowserState.NO_DEFAULT,
DefaultBrowserState.OTHER_DEFAULT})
@Retention(RetentionPolicy.SOURCE)
public @interface DefaultBrowserState {
int NO_DEFAULT = 0;
int OTHER_DEFAULT = 1;
/**
* CHROME_DEFAULT means the currently running Chrome as opposed to
* #isCurrentDefaultBrowserChrome() which looks for any Chrome.
*/
int CHROME_DEFAULT = 2;
}
private static final int MIN_TRIGGER_SESSION_COUNT = 3;
private static final String SESSION_COUNT_PARAM = "min_trigger_session_count";
private static final String CHROME_STABLE_PACKAGE_NAME = "com.android.chrome";
// TODO(crbug.com/1090103): move to some util class for reuse.
private static final String[] CHROME_PACKAGE_NAMES = {CHROME_STABLE_PACKAGE_NAME,
"org.chromium.chrome", "com.chrome.canary", "com.chrome.beta", "com.chrome.dev"};
/**
* Determine whether a promo dialog should be displayed or not. And prepare related logic to
* launch promo if a promo dialog has been decided to display.
* Return false if any of following criteria is met:
* 1. A promo dialog has been displayed before.
* 2. Not enough sessions have been started before.
* 3. Any chrome, including pre-stable, has been set as default.
* 4. On Chrome stable while no default browser is set and multiple chrome channels
* are installed.
*
* @param activity The context.
* @return True if promo dialog will be displayed.
*/
public static boolean prepareLaunchPromoIfNeeded(Activity activity) {
if (!ChromeFeatureList.isEnabled(ChromeFeatureList.ANDROID_DEFAULT_BROWSER_PROMO)) {
return false;
}
// Criteria 1
// TODO(crbug.com/1090103): change to int if dialog will be re-promo.
if (SharedPreferencesManager.getInstance().readBoolean(
ChromePreferenceKeys.DEFAULT_BROWSER_PROMO_IS_PROMOED, false)) {
return false;
}
// Criteria 2
int minSessionCount = ChromeFeatureList.getFieldTrialParamByFeatureAsInt(
ChromeFeatureList.ANDROID_DEFAULT_BROWSER_PROMO, SESSION_COUNT_PARAM,
MIN_TRIGGER_SESSION_COUNT);
if (SharedPreferencesManager.getInstance().readInt(
ChromePreferenceKeys.DEFAULT_BROWSER_PROMO_SESSION_COUNT, 0)
< minSessionCount) {
return false;
}
ResolveInfo info = PackageManagerUtils.resolveDefaultWebBrowserActivity();
int state = getCurrentDefaultBrowserState(info);
// Already default
if (state == DefaultBrowserState.CHROME_DEFAULT) return false;
// Criteria 3
if (state == DefaultBrowserState.OTHER_DEFAULT && isCurrentDefaultBrowserChrome(info)) {
return false;
}
// Criteria 4
if (ContextUtils.getApplicationContext().getPackageName().equals(CHROME_STABLE_PACKAGE_NAME)
&& isChromePreStableInstalled()
&& state == DefaultBrowserState.NO_DEFAULT) {
return false;
}
SharedPreferencesManager.getInstance().writeBoolean(
ChromePreferenceKeys.DEFAULT_BROWSER_PROMO_IS_PROMOED, true);
DefaultBrowserPromoManager.create(activity).promo(state);
return true;
}
/**
* Increment session count for triggering feature in the future.
*/
public static void incrementSessionCount() {
if (!ChromeFeatureList.isEnabled(ChromeFeatureList.ANDROID_DEFAULT_BROWSER_PROMO)) return;
SharedPreferencesManager.getInstance().incrementInt(
ChromePreferenceKeys.DEFAULT_BROWSER_PROMO_SESSION_COUNT);
}
private static boolean isChromePreStableInstalled() {
for (ResolveInfo info : PackageManagerUtils.queryAllWebBrowsersInfo()) {
for (String name : CHROME_PACKAGE_NAMES) {
if (name.equals(CHROME_STABLE_PACKAGE_NAME)) continue;
if (name.equals(info.activityInfo.packageName)) return true;
}
}
return false;
}
private static boolean isCurrentDefaultBrowserChrome(ResolveInfo info) {
String packageName = info.activityInfo.packageName;
for (String name : CHROME_PACKAGE_NAMES) {
if (name.equals(packageName)) return true;
}
return false;
}
@DefaultBrowserState
private static int getCurrentDefaultBrowserState(ResolveInfo info) {
if (info.match == 0) return DefaultBrowserState.NO_DEFAULT; // no default
if (TextUtils.equals(ContextUtils.getApplicationContext().getPackageName(),
info.activityInfo.packageName)) {
return DefaultBrowserState.CHROME_DEFAULT; // Already default
}
return DefaultBrowserState.OTHER_DEFAULT;
}
/**
* The current {@link DefaultBrowserState} in the system.
*/
@DefaultBrowserState
public static int getCurrentDefaultBrowserState() {
ResolveInfo info = PackageManagerUtils.resolveDefaultWebBrowserActivity();
return getCurrentDefaultBrowserState(info);
}
}

@ -0,0 +1,71 @@
// Copyright 2020 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.ui.default_browser_promo;
import android.content.pm.ActivityInfo;
import android.content.pm.ResolveInfo;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowPackageManager;
import org.chromium.base.ContextUtils;
import org.chromium.base.PackageManagerUtils;
import org.chromium.base.test.BaseRobolectricTestRunner;
/**
* Unit test for {@link DefaultBrowserPromoUtils}.
*/
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class DefaultBrowserPromoUtilsTest {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
// TODO(crbug.com/1090103): Add test for No Default case and other helper methods in
// DefaultBrowserPromoUtils.
// ResolveInfo#match is changed when intent is resolved, even if we mock it to 0 here.
@Test
public void testGetCurrentDefaultStateForOtherDefault() {
ResolveInfo resolveInfo = new ResolveInfo();
ActivityInfo activityInfo = new ActivityInfo();
resolveInfo.match = 1;
activityInfo.packageName = "android";
resolveInfo.activityInfo = activityInfo;
ShadowPackageManager packageManager =
Shadows.shadowOf(RuntimeEnvironment.application.getPackageManager());
packageManager.addResolveInfoForIntent(
PackageManagerUtils.getQueryInstalledBrowsersIntent(), resolveInfo);
Assert.assertEquals("Should be other default when resolve info matches another browser.",
DefaultBrowserPromoUtils.DefaultBrowserState.OTHER_DEFAULT,
DefaultBrowserPromoUtils.getCurrentDefaultBrowserState());
}
@Test
public void testGetCurrentDefaultStateForChromeDefault() {
ResolveInfo resolveInfo = new ResolveInfo();
ActivityInfo activityInfo = new ActivityInfo();
resolveInfo.match = 1;
activityInfo.packageName = ContextUtils.getApplicationContext().getPackageName();
resolveInfo.activityInfo = activityInfo;
ShadowPackageManager packageManager =
Shadows.shadowOf(RuntimeEnvironment.application.getPackageManager());
packageManager.addResolveInfoForIntent(
PackageManagerUtils.getQueryInstalledBrowsersIntent(), resolveInfo);
Assert.assertEquals(
"Should be chrome default when resolve info matches current package name.",
DefaultBrowserPromoUtils.DefaultBrowserState.CHROME_DEFAULT,
DefaultBrowserPromoUtils.getCurrentDefaultBrowserState());
}
}

@ -3727,6 +3727,31 @@ To change this setting, <ph name="BEGIN_LINK">&lt;resetlink&gt;</ph>reset sync<p
<message name="IDS_PAINT_PREVIEW_DEMO_PLAYBACK_FAILURE" desc="Toast message displayed when there is a failure in playing back a paint preview for the demo. Used in paint preview demo mode." translateable="false">
Paint Preview playback failed.
</message>
<!-- Default Browser Promo Strings-->
<message name="IDS_DEFAULT_BROWSER_PROMO_DIALOG_TITLE" desc="Title of the default browser promo dialog">
Set <ph name="APP_NAME">%1$s<ex>Chrome</ex></ph> as your default?
</message>
<message name="IDS_DEFAULT_BROWSER_PROMO_DIALOG_DESC" desc="Description in the default browser promo dialog, which is concatenated with one of the following steps strings">
<ph name="APP_NAME">%1$s<ex>Chrome</ex></ph> has the smarts and speed you need to safely do, create, and explore online
</message>
<message name="IDS_DEFAULT_BROWSER_PROMO_DIALOG_DISAMBIGUATION_SHEET_STEPS" desc="Description of the steps to set the default browser throught disambiguation sheet. 'Always' should match TC ID 4089537686339013930.">
1. Choose <ph name="APP_NAME">%1$s<ex>Chrome</ex></ph>\n2. Tap “Always”
</message>
<message name="IDS_DEFAULT_BROWSER_PROMO_DIALOG_SYSTEM_SETTINGS_STEPS"
desc="Description of the steps to set the default browser throught system settings.
'Go to Settings' should match TC ID 4431460803004659888. 'Browser App' should match TC ID 6222206565850006894.">
1. Go to Settings\n2. Tap “Browser App”\n3. Choose <ph name="APP_NAME">%1$s<ex>Chrome</ex></ph>
</message>
<message name="IDS_DEFAULT_BROWSER_PROMO_DIALOG_ROLE_MANAGER_STEPS" desc="Description of the steps to set the default browser throught role manager. 'Set as default' should match TC ID 5706081295230541240.">
1. Choose <ph name="APP_NAME">%1$s<ex>Chrome</ex></ph>\n2. Tap “Set as default”
</message>
<message name="IDS_DEFAULT_BROWSER_PROMO_DIALOG_CHOOSE_CHROME_BUTTON" desc="A button in the default browser promo dialog to choose chrome as default">
Choose <ph name="APP_NAME">%1$s<ex>Chrome</ex></ph>
</message>
<message name="IDS_DEFAULT_BROWSER_PROMO_DIALOG_GO_TO_SETTINGS_BUTTON" desc="A button in the default browser promo to navigate users to systen settings. 'Go to Settings' should match TC ID 4431460803004659888.">
Go to Settings
</message>
</messages>
</release>
</grit>

@ -16,7 +16,7 @@
<!-- Dialogs -->
<dimen name="dialog_max_width">600dp</dimen>
<dimen name="dialog_header_margin">14dp</dimen>
<dimen name="promo_dialog_illustration_margin">24dp</dimen>
<dimen name="promo_dialog_illustration_margin">8dp</dimen>
<dimen name="promo_dialog_illustration_width">150dp</dimen>
<dimen name="promo_dialog_padding">16dp</dimen>
<dimen name="promo_dialog_stacked_margin">16dp</dimen>

@ -49,6 +49,12 @@ public abstract class PromoDialog extends AlwaysDismissedDialog
/** Resource ID of the String to show as the promo title. */
public int headerStringResource;
/**
* Optional: CharSequence to show as promo title.
* This parameter takes precedence over {@link #headerStringResoruce}
*/
public CharSequence headerCharSequence;
/**
* Optional: CharSequence to show as descriptive text.
* This parameter takes precedence over {@link #subheaderStringResoruce}
@ -67,6 +73,12 @@ public abstract class PromoDialog extends AlwaysDismissedDialog
/** Optional: Resource ID of the String to show on the primary/ok button. */
public int primaryButtonStringResource;
/**
* Optional: CharSequence to show on the primary/ok button.
* This parameter takes precedence over {@link #primaryButtonStringResource}
*/
public CharSequence primaryButtonCharSequence;
/** Optional: Resource ID of the String to show on the secondary/cancel button. */
public int secondaryButtonStringResource;
}
@ -74,7 +86,7 @@ public abstract class PromoDialog extends AlwaysDismissedDialog
private static final int[] CLICKABLE_BUTTON_IDS = {R.id.button_primary, R.id.button_secondary};
private final FrameLayout mScrimView;
private final PromoDialogLayout mDialogLayout;
private PromoDialogLayout mDialogLayout;
protected PromoDialog(Activity activity) {
super(activity, R.style.PromoDialog);
@ -85,7 +97,6 @@ public abstract class PromoDialog extends AlwaysDismissedDialog
LayoutInflater.from(activity).inflate(R.layout.promo_dialog_layout, mScrimView, true);
mDialogLayout = (PromoDialogLayout) mScrimView.findViewById(R.id.promo_dialog_layout);
mDialogLayout.initialize(getDialogParams());
}
/**
@ -112,6 +123,8 @@ public abstract class PromoDialog extends AlwaysDismissedDialog
super.onCreate(savedInstanceState);
setContentView(mScrimView);
mDialogLayout.initialize(getDialogParams());
// Force the window to allow the dialog contents be as wide as necessary.
getWindow().setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);

@ -81,8 +81,8 @@ public final class PromoDialogLayout extends BoundedLinearLayout {
/** Initializes the dialog contents using the given params. Should only be called once. */
void initialize(DialogParams params) {
assert mParams == null && params != null;
assert params.headerStringResource != 0;
assert params.primaryButtonStringResource != 0;
assert params.headerStringResource != 0 || params.headerCharSequence != null;
assert params.primaryButtonStringResource != 0 || params.primaryButtonCharSequence != null;
mParams = params;
if (mParams.drawableInstance != null) {
@ -99,7 +99,11 @@ public final class PromoDialogLayout extends BoundedLinearLayout {
}
// Create the header.
mHeaderView.setText(mParams.headerStringResource);
if (mParams.headerCharSequence != null) {
mHeaderView.setText(mParams.headerCharSequence);
} else {
mHeaderView.setText(mParams.headerStringResource);
}
// Set up the subheader text.
if (mParams.subheaderCharSequence != null) {
@ -124,7 +128,9 @@ public final class PromoDialogLayout extends BoundedLinearLayout {
// Create the buttons.
DualControlLayout buttonBar = (DualControlLayout) findViewById(R.id.button_bar);
String primaryString = getResources().getString(mParams.primaryButtonStringResource);
String primaryString = mParams.primaryButtonCharSequence != null
? mParams.primaryButtonCharSequence.toString()
: getResources().getString(mParams.primaryButtonStringResource);
buttonBar.addView(
DualControlLayout.createButtonForLayout(getContext(), true, primaryString, null));

@ -38909,6 +38909,7 @@ from previous Chrome versions.
<int value="-1859095876" label="Previews:disabled"/>
<int value="-1858284725" label="TabGroupsFeedback:enabled"/>
<int value="-1856902397" label="LoadingWithMojo:enabled"/>
<int value="-1856718338" label="AndroidDefaultBrowserPromo:disabled"/>
<int value="-1855347512" label="FormControlsRefresh:disabled"/>
<int value="-1854432127" label="ChromeHomePullToRefreshIphAtTop:disabled"/>
<int value="-1854372227" label="VrBrowsingExperimentalFeatures:enabled"/>
@ -39242,6 +39243,7 @@ from previous Chrome versions.
<int value="-1468126425" label="ResourceLoadScheduler:disabled"/>
<int value="-1467642274" label="KeyboardShortcutViewer:disabled"/>
<int value="-1467332609" label="tab-management-experiment-type-anise"/>
<int value="-1467062925" label="AndroidDefaultBrowserPromo:enabled"/>
<int value="-1466990325" label="CrosCompUpdates:enabled"/>
<int value="-1466862366" label="TouchToFillAndroid:enabled"/>
<int value="-1466759286" label="TabModalJsDialog:disabled"/>