Commit d944f249 authored by Peter Kotwicz's avatar Peter Kotwicz Committed by Commit Bot

[Android WebAPK] Add WebappCoordinator/WebApkCoordinator

This CL:
- Daggerizes:
  - WebappDisclosureSnackbarController
  - WebappActionsNotificationManager
- Moves the management of the following classes out of
  WebappActivity/WebApkActivity and into
  WebappActivityCoordinator/WebApkActivityCoordinator
  - WebappDisclosureSnackbarController
  - WebappActionsNotificationManager
  - WebApkUpdateManager
- Introduces new class - WebappDeferredStartupWithStorageRegistrar -
  for getting notified once the WebappDataStorage has been fetched
  (and potentially created) during deferred startup.

BUG=1042147
TEST=WebApkInitializationTest

Change-Id: I98ea759d4f1186da4e58d899aab6f3aec2caaecc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2005976
Commit-Queue: Peter Kotwicz <pkotwicz@chromium.org>
Reviewed-by: default avatarYaron Friedman <yfriedman@chromium.org>
Reviewed-by: default avatarGlenn Hartmann <hartmanng@chromium.org>
Cr-Commit-Position: refs/heads/master@{#742480}
parent 565cec0b
......@@ -1795,6 +1795,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/webapps/WebApkActivity7.java",
"java/src/org/chromium/chrome/browser/webapps/WebApkActivity8.java",
"java/src/org/chromium/chrome/browser/webapps/WebApkActivity9.java",
"java/src/org/chromium/chrome/browser/webapps/WebApkActivityCoordinator.java",
"java/src/org/chromium/chrome/browser/webapps/WebApkExtras.java",
"java/src/org/chromium/chrome/browser/webapps/WebApkHandlerDelegate.java",
"java/src/org/chromium/chrome/browser/webapps/WebApkInfo.java",
......@@ -1825,10 +1826,12 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/webapps/WebappActivity7.java",
"java/src/org/chromium/chrome/browser/webapps/WebappActivity8.java",
"java/src/org/chromium/chrome/browser/webapps/WebappActivity9.java",
"java/src/org/chromium/chrome/browser/webapps/WebappActivityCoordinator.java",
"java/src/org/chromium/chrome/browser/webapps/WebappActivityTabController.java",
"java/src/org/chromium/chrome/browser/webapps/WebappAuthenticator.java",
"java/src/org/chromium/chrome/browser/webapps/WebappCustomTabTimeSpentLogger.java",
"java/src/org/chromium/chrome/browser/webapps/WebappDataStorage.java",
"java/src/org/chromium/chrome/browser/webapps/WebappDeferredStartupWithStorageHandler.java",
"java/src/org/chromium/chrome/browser/webapps/WebappDirectoryManager.java",
"java/src/org/chromium/chrome/browser/webapps/WebappDisclosureSnackbarController.java",
"java/src/org/chromium/chrome/browser/webapps/WebappExtras.java",
......
......@@ -517,6 +517,7 @@ chrome_test_java_sources = [
"javatests/src/org/chromium/chrome/browser/webapps/ActivityAssignerTest.java",
"javatests/src/org/chromium/chrome/browser/webapps/WebApkActivityTest.java",
"javatests/src/org/chromium/chrome/browser/webapps/WebApkActivityTestRule.java",
"javatests/src/org/chromium/chrome/browser/webapps/WebApkInitializationTest.java",
"javatests/src/org/chromium/chrome/browser/webapps/WebApkIntegrationTest.java",
"javatests/src/org/chromium/chrome/browser/webapps/WebApkUpdateDataFetcherTest.java",
"javatests/src/org/chromium/chrome/browser/webapps/WebApkUpdateManagerTest.java",
......
......@@ -366,7 +366,7 @@ public abstract class ChromeActivity<C extends ChromeActivityComponent>
ChromeActivityCommonsModule commonsModule = overridenCommonsFactory == null
? new ChromeActivityCommonsModule(this, getLifecycleDispatcher())
: overridenCommonsFactory.create(this);
: overridenCommonsFactory.create(this, getLifecycleDispatcher());
return createComponent(commonsModule);
}
......
......@@ -318,6 +318,13 @@ public class BrowserServicesIntentDataProvider {
return false;
}
/**
* @return Whether the Activity is a WebAPK activity.
*/
public boolean isWebApkActivity() {
return false;
}
/**
* @return Whether the Activity should attempt to load a dynamic module.
*
......
......@@ -29,6 +29,7 @@ import org.chromium.chrome.browser.tabmodel.ChromeTabCreator;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorImpl;
import org.chromium.chrome.browser.ui.RootUiCoordinator;
import org.chromium.chrome.browser.webapps.WebappActivityCoordinator;
/**
* Contains functionality which is shared between {@link WebappActivity} and
......@@ -44,6 +45,7 @@ public abstract class BaseCustomTabActivity<C extends BaseCustomTabActivityCompo
protected CustomTabToolbarColorController mToolbarColorController;
protected CustomTabStatusBarColorProvider mStatusBarColorProvider;
protected CustomTabActivityTabFactory mTabFactory;
protected @Nullable WebappActivityCoordinator mWebappActivityCoordinator;
// This is to give the right package name while using the client's resources during an
// overridePendingTransition call.
......@@ -76,6 +78,14 @@ public abstract class BaseCustomTabActivity<C extends BaseCustomTabActivityCompo
component.resolveCompositorContentInitializer();
component.resolveTaskDescriptionHelper();
BrowserServicesIntentDataProvider intentDataProvider = getIntentDataProvider();
if (intentDataProvider.isWebappOrWebApkActivity()) {
mWebappActivityCoordinator = component.resolveWebappActivityCoordinator();
}
if (intentDataProvider.isWebApkActivity()) {
component.resolveWebApkActivityCoordinator();
}
}
/**
......@@ -210,6 +220,14 @@ public abstract class BaseCustomTabActivity<C extends BaseCustomTabActivityCompo
return mStatusBarColorProvider.getBaseStatusBarColor(tab, super.getBaseStatusBarColor(tab));
}
@Override
public void initDeferredStartupForActivity() {
if (mWebappActivityCoordinator != null) {
mWebappActivityCoordinator.initDeferredStartupForActivity();
}
super.initDeferredStartupForActivity();
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
Boolean result = KeyboardShortcuts.dispatchKeyEvent(
......
......@@ -17,6 +17,8 @@ import org.chromium.chrome.browser.customtabs.features.toolbar.CustomTabToolbarC
import org.chromium.chrome.browser.customtabs.features.toolbar.CustomTabToolbarCoordinator;
import org.chromium.chrome.browser.dependency_injection.ChromeActivityComponent;
import org.chromium.chrome.browser.webapps.SplashController;
import org.chromium.chrome.browser.webapps.WebApkActivityCoordinator;
import org.chromium.chrome.browser.webapps.WebappActivityCoordinator;
/**
* Contains accessors which are shared between {@link CustomTabActivityComponent} and
......@@ -35,4 +37,6 @@ public interface BaseCustomTabActivityComponent extends ChromeActivityComponent
SplashController resolveSplashController();
TabObserverRegistrar resolveTabObserverRegistrar();
TwaFinishHandler resolveTwaFinishHandler();
WebappActivityCoordinator resolveWebappActivityCoordinator();
WebApkActivityCoordinator resolveWebApkActivityCoordinator();
}
......@@ -39,7 +39,10 @@ public class ChromeActivityCommonsModule {
private final ActivityLifecycleDispatcher mLifecycleDispatcher;
/** See {@link ModuleFactoryOverrides} */
public interface Factory { ChromeActivityCommonsModule create(ChromeActivity<?> activity); }
public interface Factory {
ChromeActivityCommonsModule create(ChromeActivity<?> activity,
ActivityLifecycleDispatcher activityLifecycleDispatcher);
}
public ChromeActivityCommonsModule(
ChromeActivity<?> activity, ActivityLifecycleDispatcher lifecycleDispatcher) {
......
......@@ -61,11 +61,6 @@ public class WebApkActivity extends WebappActivity {
&& !webApkPackageName.startsWith(WebApkConstants.WEBAPK_PACKAGE_PREFIX);
}
@Override
public String getWebApkPackageName() {
return getWebApkInfo().webApkPackageName();
}
@Override
public void onResume() {
super.onResume();
......@@ -79,40 +74,6 @@ public class WebApkActivity extends WebappActivity {
RecordHistogram.recordTimesHistogram("MobileStartup.IntentToCreationTime.WebApk", timeMs);
}
@Override
protected void onDeferredStartupWithStorage(WebappDataStorage storage) {
super.onDeferredStartupWithStorage(storage);
WebApkInfo info = getWebApkInfo();
WebApkUma.recordShellApkVersion(info.shellApkVersion(), info.distributor());
storage.incrementLaunchCount();
getComponent().resolveWebApkUpdateManager().updateIfNeeded(storage, info);
}
@Override
protected void onDeferredStartupWithNullStorage(
WebappDisclosureSnackbarController disclosureSnackbarController) {
// WebappDataStorage objects are cleared if a user clears Chrome's data. Recreate them
// for WebAPKs since we need to store metadata for updates and disclosure notifications.
WebappRegistry.getInstance().register(
getWebApkInfo().id(), new WebappRegistry.FetchWebappDataStorageCallback() {
@Override
public void onWebappDataStorageRetrieved(WebappDataStorage storage) {
if (isActivityFinishingOrDestroyed()) return;
onDeferredStartupWithStorage(storage);
// Set force == true to indicate that we need to show a privacy
// disclosure for the newly installed unbound WebAPKs which
// have no storage yet. We can't simply default to a showing if the
// storage has a default value as we don't want to show this disclosure
// for pre-existing unbound WebAPKs.
disclosureSnackbarController.maybeShowDisclosure(
WebApkActivity.this, storage, true /* force */);
}
});
}
@Override
public void onPauseWithNative() {
WebApkInfo info = getWebApkInfo();
......
// 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.webapps;
import androidx.annotation.Nullable;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.dependency_injection.ActivityScope;
import org.chromium.chrome.browser.metrics.WebApkUma;
import javax.inject.Inject;
import dagger.Lazy;
/**
* Coordinator for the WebAPK activity component.
* Add methods here if other components need to communicate with the WebAPK activity component.
*/
@ActivityScope
public class WebApkActivityCoordinator {
private final WebApkActivity mActivity;
private final Lazy<WebApkUpdateManager> mWebApkUpdateManager;
@Inject
public WebApkActivityCoordinator(ChromeActivity<?> activity,
WebappDeferredStartupWithStorageHandler deferredStartupWithStorageHandler,
WebappDisclosureSnackbarController disclosureSnackbarController,
Lazy<WebApkUpdateManager> webApkUpdateManager) {
// We don't need to do anything with |disclosureSnackbarController|. We just need to resolve
// it so that it starts working.
mActivity = (WebApkActivity) activity;
mWebApkUpdateManager = webApkUpdateManager;
deferredStartupWithStorageHandler.addTask((storage, didCreateStorage) -> {
if (activity.isActivityFinishingOrDestroyed()) return;
onDeferredStartupWithStorage(storage, didCreateStorage);
});
}
public void onDeferredStartupWithStorage(
@Nullable WebappDataStorage storage, boolean didCreateStorage) {
assert storage != null;
WebApkInfo info = mActivity.getWebApkInfo();
WebApkUma.recordShellApkVersion(info.shellApkVersion(), info.distributor());
storage.incrementLaunchCount();
mWebApkUpdateManager.get().updateIfNeeded(storage, info);
}
}
......@@ -75,6 +75,7 @@ public class WebApkUpdateDataFetcher extends EmptyTabObserver {
* Puts the object in a state where it is safe to be destroyed.
*/
public void destroy() {
if (mTab == null) return;
mTab.removeObserver(this);
WebApkUpdateDataFetcherJni.get().destroy(mNativePointer, WebApkUpdateDataFetcher.this);
mNativePointer = 0;
......
......@@ -10,10 +10,17 @@ import android.content.Context;
import android.content.Intent;
import android.support.v4.app.NotificationCompat;
import androidx.annotation.NonNull;
import org.chromium.base.ContextUtils;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.browserservices.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.customtabs.content.CustomTabActivityTabProvider;
import org.chromium.chrome.browser.dependency_injection.ActivityScope;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.PauseResumeWithNativeObserver;
import org.chromium.chrome.browser.notifications.ChromeNotification;
import org.chromium.chrome.browser.notifications.NotificationBuilderFactory;
import org.chromium.chrome.browser.notifications.NotificationConstants;
......@@ -30,6 +37,8 @@ import org.chromium.ui.base.Clipboard;
import java.lang.ref.WeakReference;
import javax.inject.Inject;
/**
* Manages the notification shown by Chrome when running standalone Web Apps. It accomplishes
* number of goals:
......@@ -37,7 +46,8 @@ import java.lang.ref.WeakReference;
* - Exposes 'Share' and 'Open in Chrome' actions.
* - Messages that Web App runs in Chrome.
*/
class WebappActionsNotificationManager {
@ActivityScope
class WebappActionsNotificationManager implements PauseResumeWithNativeObserver {
private static final String ACTION_SHARE =
"org.chromium.chrome.browser.webapps.NOTIFICATION_ACTION_SHARE";
private static final String ACTION_OPEN_IN_CHROME =
......@@ -45,16 +55,40 @@ class WebappActionsNotificationManager {
private static final String ACTION_FOCUS =
"org.chromium.chrome.browser.webapps.NOTIFICATION_ACTION_FOCUS";
public static void maybeShowNotification(Tab tab, WebappInfo webappInfo) {
if (tab == null) return;
private final CustomTabActivityTabProvider mTabProvider;
private final BrowserServicesIntentDataProvider mIntentDataProvider;
@Inject
public WebappActionsNotificationManager(CustomTabActivityTabProvider tabProvider,
BrowserServicesIntentDataProvider intentDataProvider,
ActivityLifecycleDispatcher lifecycleDispatcher) {
mTabProvider = tabProvider;
mIntentDataProvider = intentDataProvider;
lifecycleDispatcher.register(this);
}
@Override
public void onResumeWithNative() {
maybeShowNotification(mTabProvider.getTab(), mIntentDataProvider);
}
@Override
public void onPauseWithNative() {
cancelNotification();
}
private static void maybeShowNotification(
Tab tab, BrowserServicesIntentDataProvider intentDataProvider) {
WebappExtras webappExtras = intentDataProvider.getWebappExtras();
if (tab == null || webappExtras == null) return;
// All features provided by the notification are also available in the minimal-ui toolbar.
if (webappInfo.displayMode() == WebDisplayMode.MINIMAL_UI) {
if (webappExtras.displayMode == WebDisplayMode.MINIMAL_UI) {
return;
}
Context appContext = ContextUtils.getApplicationContext();
ChromeNotification notification = createNotification(appContext, tab, webappInfo);
ChromeNotification notification = createNotification(appContext, tab, webappExtras);
NotificationManagerProxy nm = new NotificationManagerProxyImpl(appContext);
nm.notify(notification);
......@@ -64,7 +98,7 @@ class WebappActionsNotificationManager {
}
private static ChromeNotification createNotification(
Context appContext, Tab tab, WebappInfo webappInfo) {
Context appContext, Tab tab, @NonNull WebappExtras webappExtras) {
// The pending intents target an activity (instead of a service or a broadcast receiver) so
// that the notification tray closes when a user taps the one of the notification action
// links.
......@@ -83,7 +117,7 @@ class WebappActionsNotificationManager {
ChannelDefinitions.ChannelId.WEBAPP_ACTIONS,
null /* remoteAppPackageName */, metadata)
.setSmallIcon(R.drawable.ic_chrome)
.setContentTitle(webappInfo.shortName())
.setContentTitle(webappExtras.shortName)
.setContentText(appContext.getString(R.string.webapp_tap_to_copy_url))
.setShowWhen(false)
.setAutoCancel(false)
......
......@@ -18,7 +18,6 @@ import android.text.TextUtils;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ActivityState;
......@@ -87,8 +86,6 @@ public class WebappActivity extends BaseCustomTabActivity<WebappActivityComponen
private TabObserverRegistrar mTabObserverRegistrar;
private CustomTabDelegateFactory mDelegateFactory;
private WebappDisclosureSnackbarController mDisclosureSnackbarController;
private boolean mIsInitialized;
private Integer mBrandColor;
......@@ -140,7 +137,6 @@ public class WebappActivity extends BaseCustomTabActivity<WebappActivityComponen
*/
public WebappActivity() {
mWebappInfo = createWebappInfo(null);
mDisclosureSnackbarController = new WebappDisclosureSnackbarController();
}
@Override
......@@ -394,36 +390,6 @@ public class WebappActivity extends BaseCustomTabActivity<WebappActivityComponen
super.onResume();
}
@Override
public void onResumeWithNative() {
super.onResumeWithNative();
WebappActionsNotificationManager.maybeShowNotification(getActivityTab(), mWebappInfo);
WebappDataStorage storage =
WebappRegistry.getInstance().getWebappDataStorage(mWebappInfo.id());
if (storage != null) {
mDisclosureSnackbarController.maybeShowDisclosure(this, storage, false /* force */);
}
}
@Override
public void onPauseWithNative() {
WebappActionsNotificationManager.cancelNotification();
super.onPauseWithNative();
}
@Override
protected void initDeferredStartupForActivity() {
super.initDeferredStartupForActivity();
WebappDataStorage storage =
WebappRegistry.getInstance().getWebappDataStorage(mWebappInfo.id());
if (storage != null) {
onDeferredStartupWithStorage(storage);
} else {
onDeferredStartupWithNullStorage(mDisclosureSnackbarController);
}
}
@Override
protected void recordIntentToCreationTime(long timeMs) {
super.recordIntentToCreationTime(timeMs);
......@@ -431,15 +397,6 @@ public class WebappActivity extends BaseCustomTabActivity<WebappActivityComponen
RecordHistogram.recordTimesHistogram("MobileStartup.IntentToCreationTime.WebApp", timeMs);
}
protected void onDeferredStartupWithStorage(WebappDataStorage storage) {
updateStorage(storage);
}
protected void onDeferredStartupWithNullStorage(
WebappDisclosureSnackbarController disclosureSnackbarController) {
// Overridden in WebApkActivity
}
@Override
public AppMenuPropertiesDelegate createAppMenuPropertiesDelegate() {
return new CustomTabAppMenuPropertiesDelegate(this, getActivityTabProvider(),
......@@ -479,25 +436,6 @@ public class WebappActivity extends BaseCustomTabActivity<WebappActivityComponen
return Holder.sWebappInfoMap.remove(id);
}
protected void updateStorage(WebappDataStorage storage) {
// The information in the WebappDataStorage may have been purged by the
// user clearing their history or not launching the web app recently.
// Restore the data if necessary.
storage.updateFromWebappInfo(mWebappInfo);
// A recent last used time is the indicator that the web app is still
// present on the home screen, and enables sources such as notifications to
// launch web apps. Thus, we do not update the last used time when the web
// app is not directly launched from the home screen, as this interferes
// with the heuristic.
if (mWebappInfo.isLaunchedFromHomescreen()) {
// TODO(yusufo): WebappRegistry#unregisterOldWebapps uses this information to delete
// WebappDataStorage objects for legacy webapps which haven't been used in a while.
// That will need to be updated to not delete anything for a TWA which remains installed
storage.updateLastUsedTime();
}
}
protected CustomTabTabObserver createTabObserver() {
return new CustomTabTabObserver() {
@Override
......@@ -564,14 +502,6 @@ public class WebappActivity extends BaseCustomTabActivity<WebappActivityComponen
return WebappScopePolicy.Type.LEGACY;
}
/**
* @return The package name if this Activity is associated with an APK. Null if there is no
* associated Android native client.
*/
public @Nullable String getWebApkPackageName() {
return null;
}
@Override
public boolean onMenuOrKeyboardAction(int id, boolean fromMenu) {
// Disable creating bookmark.
......
// 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.webapps;
import androidx.annotation.NonNull;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.browserservices.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.dependency_injection.ActivityScope;
import javax.inject.Inject;
/**
* Coordinator shared between webapp activity and WebAPK activity components.
* Add methods here if other components need to communicate with either of these components.
*/
@ActivityScope
public class WebappActivityCoordinator {
private final WebappActivity mActivity;
private final BrowserServicesIntentDataProvider mIntentDataProvider;
private final WebappDeferredStartupWithStorageHandler mDeferredStartupWithStorageHandler;
@Inject
public WebappActivityCoordinator(ChromeActivity<?> activity,
BrowserServicesIntentDataProvider intentDataProvider,
WebappDeferredStartupWithStorageHandler deferredStartupWithStorageHandler,
WebappActionsNotificationManager actionsNotificationManager) {
// We don't need to do anything with |actionsNotificationManager|. We just need to resolve
// it so that it starts working.
mActivity = (WebappActivity) activity;
mIntentDataProvider = intentDataProvider;
mDeferredStartupWithStorageHandler = deferredStartupWithStorageHandler;
mDeferredStartupWithStorageHandler.addTask((storage, didCreateStorage) -> {
if (activity.isActivityFinishingOrDestroyed()) return;
if (storage != null) {
updateStorage(storage);
}
});
}
/**
* Invoked to add deferred startup tasks to queue.
*/
public void initDeferredStartupForActivity() {
mDeferredStartupWithStorageHandler.initDeferredStartupForActivity();
}
private void updateStorage(@NonNull WebappDataStorage storage) {
// The information in the WebappDataStorage may have been purged by the
// user clearing their history or not launching the web app recently.
// Restore the data if necessary.
WebappInfo webappInfo = mActivity.getWebappInfo();
storage.updateFromWebappInfo(webappInfo);
// A recent last used time is the indicator that the web app is still
// present on the home screen, and enables sources such as notifications to
// launch web apps. Thus, we do not update the last used time when the web
// app is not directly launched from the home screen, as this interferes
// with the heuristic.
if (webappInfo.isLaunchedFromHomescreen()) {
// TODO(yusufo): WebappRegistry#unregisterOldWebapps uses this information to delete
// WebappDataStorage objects for legacy webapps which haven't been used in a while.
storage.updateLastUsedTime();
}
}
}
// 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.webapps;
import androidx.annotation.Nullable;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.DeferredStartupHandler;
import org.chromium.chrome.browser.browserservices.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.dependency_injection.ActivityScope;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
/**
* Requests {@link WebappDataStorage} during deferred startup. For WebAPKs only, creates
* {@link WebappDataStorage} if the WebAPK is not registered. Runs tasks once the
* {@link WebappDataStorage} has been fetched (and perhaps also created).
*/
@ActivityScope
public class WebappDeferredStartupWithStorageHandler {
interface Task {
/**
* Called to run task.
* @param storage Null if there is no {@link WebappDataStorage} registered for the webapp
* and a new entry was not created.
* @param didCreateStorage Whether a new {@link WebappDataStorage} entry was created.
*/
void run(@Nullable WebappDataStorage storage, boolean didCreateStorage);
}
private final ChromeActivity<?> mActivity;
private final @Nullable String mWebApkId;
private final List<Task> mDeferredWithStorageTasks = new ArrayList<>();
@Inject
public WebappDeferredStartupWithStorageHandler(
ChromeActivity<?> activity, BrowserServicesIntentDataProvider intentDataProvider) {
mActivity = activity;
WebappExtras webappExtras = intentDataProvider.getWebappExtras();
mWebApkId = (webappExtras != null && intentDataProvider.isWebApkActivity())
? webappExtras.id
: null;
}
/**
* Invoked to add deferred startup task to queue.
*/
public void initDeferredStartupForActivity() {
DeferredStartupHandler.getInstance().addDeferredTask(() -> { runDeferredTask(); });
}
public void addTask(Task task) {
mDeferredWithStorageTasks.add(task);
}
private void runDeferredTask() {
if (mActivity.isActivityFinishingOrDestroyed()) return;
WebappDataStorage storage = WebappRegistry.getInstance().getWebappDataStorage(mWebApkId);
if (storage != null || mWebApkId == null) {
runTasks(storage, false /* didCreateStorage */);
}
WebappRegistry.getInstance().register(
mWebApkId, new WebappRegistry.FetchWebappDataStorageCallback() {
@Override
public void onWebappDataStorageRetrieved(WebappDataStorage storage) {
runTasks(storage, true /* didCreateStorage */);
}
});
}
public void runTasks(@Nullable WebappDataStorage storage, boolean didCreateStorage) {
for (Task task : mDeferredWithStorageTasks) {
task.run(storage, didCreateStorage);
}
mDeferredWithStorageTasks.clear();
}
}
......@@ -4,11 +4,20 @@
package org.chromium.chrome.browser.webapps;
import androidx.annotation.Nullable;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.browserservices.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.dependency_injection.ActivityScope;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.PauseResumeWithNativeObserver;
import org.chromium.chrome.browser.ui.messages.snackbar.Snackbar;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.webapk.lib.common.WebApkConstants;
import javax.inject.Inject;
/**
* Unbound WebAPKs are part of Chrome. They have access to cookies and report metrics the same way
* as the rest of Chrome. However, there is no UI indicating they are running in Chrome. For privacy
......@@ -18,7 +27,53 @@ import org.chromium.webapk.lib.common.WebApkConstants;
* as long as the app is open. It should remain active even across pause/resume and should show the
* next time the app is opened if it hasn't been acknowledged.
*/
public class WebappDisclosureSnackbarController implements SnackbarManager.SnackbarController {
@ActivityScope
public class WebappDisclosureSnackbarController
implements SnackbarManager.SnackbarController, PauseResumeWithNativeObserver {
private final ChromeActivity mActivity;
private final BrowserServicesIntentDataProvider mIntentDataProvider;
@Inject
public WebappDisclosureSnackbarController(ChromeActivity<?> activity,
BrowserServicesIntentDataProvider intentDataProvider,
WebappDeferredStartupWithStorageHandler deferredStartupWithStorageHandler,
ActivityLifecycleDispatcher lifecycleDispatcher) {
mActivity = activity;
mIntentDataProvider = intentDataProvider;
lifecycleDispatcher.register(this);
deferredStartupWithStorageHandler.addTask((storage, didCreateStorage) -> {
if (activity.isActivityFinishingOrDestroyed()) return;
onDeferredStartupWithStorage(storage, didCreateStorage);
});
}
public void onDeferredStartupWithStorage(
@Nullable WebappDataStorage storage, boolean didCreateStorage) {
if (didCreateStorage) {
// Set force == true to indicate that we need to show a privacy disclosure for the newly
// installed unbound WebAPKs which have no storage yet. We can't simply default to a
// showing if the storage has a default value as we don't want to show this disclosure
// for pre-existing unbound WebAPKs.
maybeShowDisclosure(storage, true /* force */);
}
}
@Override
public void onResumeWithNative() {
WebappExtras webappExtras = mIntentDataProvider.getWebappExtras();
WebappDataStorage storage = WebappRegistry.getInstance().getWebappDataStorage(
mIntentDataProvider.getWebappExtras().id);
if (storage != null) {
maybeShowDisclosure(storage, false /* force */);
}
}
@Override
public void onPauseWithNative() {}
/**
* @param actionData an instance of WebappInfo
*/
......@@ -37,45 +92,41 @@ public class WebappDisclosureSnackbarController implements SnackbarManager.Snack
/**
* Shows the disclosure informing the user the Webapp is running in Chrome.
* @param activity Webapp activity to show disclosure for.
* @param storage Storage for the Webapp.
* @param force Whether to force showing the Snackbar (if no storage is available on start).
*/
public void maybeShowDisclosure(
WebappActivity activity, WebappDataStorage storage, boolean force) {
private void maybeShowDisclosure(WebappDataStorage storage, boolean force) {
if (storage == null) return;
// If forced we set the bit to show the disclosure. This persists to future instances.
if (force) storage.setShowDisclosure();
if (shouldShowDisclosure(activity, storage)) {
activity.getSnackbarManager().showSnackbar(
Snackbar.make(activity.getResources().getString(
if (shouldShowDisclosure(storage)) {
mActivity.getSnackbarManager().showSnackbar(
Snackbar.make(mActivity.getResources().getString(
R.string.app_running_in_chrome_disclosure),
this, Snackbar.TYPE_PERSISTENT,
Snackbar.UMA_WEBAPK_PRIVACY_DISCLOSURE)
.setAction(
activity.getResources().getString(R.string.ok), storage)
.setAction(mActivity.getResources().getString(R.string.ok), storage)
.setSingleLine(false));
}
}
/**
* Restricts showing to TWAs and unbound WebAPKs that haven't dismissed the disclosure.
* @param activity Webapp activity.
* Restricts showing to unbound WebAPKs that haven't dismissed the disclosure.
* @param storage Storage for the Webapp.
* @return boolean indicating whether to show the privacy disclosure.
*/
private boolean shouldShowDisclosure(WebappActivity activity, WebappDataStorage storage) {
private boolean shouldShowDisclosure(WebappDataStorage storage) {
// Show only if the correct flag is set.
if (!storage.shouldShowDisclosure()) {
return false;
}
// This will be null for Webapps or bound WebAPKs.
String packageName = activity.getWebApkPackageName();
// Show for unbound WebAPKs.
return packageName != null
&& !packageName.startsWith(WebApkConstants.WEBAPK_PACKAGE_PREFIX);
WebApkExtras webApkExtras = mIntentDataProvider.getWebApkExtras();
return webApkExtras != null && webApkExtras.webApkPackageName != null
&& !webApkExtras.webApkPackageName.startsWith(
WebApkConstants.WEBAPK_PACKAGE_PREFIX);
}
}
......@@ -67,6 +67,11 @@ public class WebappIntentDataProvider extends BrowserServicesIntentDataProvider
return true;
}
@Override
public boolean isWebApkActivity() {
return mWebApkExtras != null;
}
@Override
@Nullable
public WebappExtras getWebappExtras() {
......
// 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.webapps;
import static org.junit.Assert.assertTrue;
import android.support.test.filters.LargeTest;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Feature;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.dependency_injection.ChromeActivityCommonsModule;
import org.chromium.chrome.browser.dependency_injection.ModuleOverridesRule;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.LifecycleObserver;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.util.browser.webapps.WebApkInfoBuilder;
import org.chromium.net.test.EmbeddedTestServer;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeoutException;
/**
* Tests that the expected classes are constructed when a {@link WebApkActivity} is launched.
*/
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class WebApkInitializationTest {
/**
* {@link ActivityLifecycleDispatcher} wrapper which tracks {@link LifecycleObserver}
* registrations.
*/
private static class TrackingActivityLifecycleDispatcher
implements ActivityLifecycleDispatcher {
private ActivityLifecycleDispatcher mRealActivityLifecycleDispatcher;
private Set<String> mRegisteredObserverClassNames = new HashSet<>();
public void init(ActivityLifecycleDispatcher realActivityLifecycleDispatcher) {
mRealActivityLifecycleDispatcher = realActivityLifecycleDispatcher;
}
/**
* Returns set of all the {@link LifecycleObserver} subclasses which have registered with
* the {@link ActivityLifecycleDispatcher}.
*/
public Set<String> getRegisteredObserverClassNames() {
return mRegisteredObserverClassNames;
}
@Override
public void register(LifecycleObserver observer) {
mRegisteredObserverClassNames.add(observer.getClass().getName());
mRealActivityLifecycleDispatcher.register(observer);
}
@Override
public void unregister(LifecycleObserver observer) {
mRealActivityLifecycleDispatcher.unregister(observer);
}
@Override
public @ActivityState int getCurrentActivityState() {
return mRealActivityLifecycleDispatcher.getCurrentActivityState();
}
@Override
public boolean isNativeInitializationFinished() {
return true;
}
}
private final TrackingActivityLifecycleDispatcher mTrackingActivityLifecycleDispatcher =
new TrackingActivityLifecycleDispatcher();
private final TestRule mModuleOverridesRule = new ModuleOverridesRule().setOverride(
ChromeActivityCommonsModule.Factory.class, (activity, lifecycleDispatcher) -> {
mTrackingActivityLifecycleDispatcher.init(lifecycleDispatcher);
return new ChromeActivityCommonsModule(
activity, mTrackingActivityLifecycleDispatcher);
});
private final WebApkActivityTestRule mActivityRule = new WebApkActivityTestRule();
@Rule
public final TestRule mRuleChain =
RuleChain.outerRule(mModuleOverridesRule).around(mActivityRule);
/**
* Test that {@link WebappActionsNotificationManager} and
* {@link WebappDisclosureSnackbarController} are constructed when a {@link WebApkActivity} is
* launched.
*/
@Test
@LargeTest
@Feature({"WebApk"})
public void testInitialization() throws TimeoutException {
EmbeddedTestServer embeddedTestServer =
mActivityRule.getEmbeddedTestServerRule().getServer();
WebApkInfoBuilder webApkInfoBuilder = new WebApkInfoBuilder(
"org.chromium.webapk.for.testing",
embeddedTestServer.getURL("/chrome/test/data/banners/manifest_test_page.html"));
mActivityRule.startWebApkActivity(webApkInfoBuilder.build());
Set<String> registeredObserverClassNames =
mTrackingActivityLifecycleDispatcher.getRegisteredObserverClassNames();
assertTrue(registeredObserverClassNames.contains(
WebappActionsNotificationManager.class.getName()));
assertTrue(registeredObserverClassNames.contains(
WebappDisclosureSnackbarController.class.getName()));
}
}
......@@ -9,6 +9,7 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
......@@ -19,12 +20,17 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.android.util.concurrent.RoboExecutorService;
import org.robolectric.annotation.Config;
import org.chromium.base.task.PostTask;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Feature;
import org.chromium.chrome.browser.browserservices.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.ui.messages.snackbar.Snackbar;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.test.util.browser.webapps.WebApkInfoBuilder;
import org.chromium.webapk.lib.common.WebApkConstants;
/**
......@@ -43,23 +49,41 @@ public class WebappDisclosureSnackbarControllerTest {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
// Run AsyncTasks synchronously.
PostTask.setPrenativeThreadPoolExecutorForTesting(new RoboExecutorService());
doReturn("test text").when(mResources).getString(anyInt());
doReturn(mManager).when(mActivity).getSnackbarManager();
doReturn(mResources).when(mActivity).getResources();
}
private WebappDisclosureSnackbarController buildControllerForWebApk(String webApkPackageName) {
BrowserServicesIntentDataProvider intentDataProvider =
new WebApkInfoBuilder(webApkPackageName, "https://pwa.rocks/")
.build()
.getProvider();
return new WebappDisclosureSnackbarController(mActivity, intentDataProvider,
mock(WebappDeferredStartupWithStorageHandler.class),
mock(ActivityLifecycleDispatcher.class));
}
private WebappDataStorage registerStorageForWebApk(String packageName) {
String id = WebappRegistry.webApkIdForPackage(packageName);
WebappRegistry.getInstance().register(id, (storage) -> {});
return WebappRegistry.getInstance().getWebappDataStorage(id);
}
public void verifyShownThenDismissedOnNewCreateStorage(String packageName) {
WebappDisclosureSnackbarController controller = new WebappDisclosureSnackbarController();
WebappDataStorage storage = WebappDataStorage.open(packageName);
WebappDisclosureSnackbarController controller = buildControllerForWebApk(packageName);
WebappDataStorage storage = registerStorageForWebApk(packageName);
// Simulates the case that shows the disclosure when creating a new storage.
controller.maybeShowDisclosure(mActivity, storage, true);
controller.onDeferredStartupWithStorage(storage, true /* didCreateStorage */);
verify(mManager, times(1)).showSnackbar(any(Snackbar.class));
assertTrue(storage.shouldShowDisclosure());
// Simulate a restart or a resume (has storage so `force = false`).
controller.maybeShowDisclosure(mActivity, storage, false);
// Simulate a restart or a resume.
controller.onResumeWithNative();
verify(mManager, times(2)).showSnackbar(any(Snackbar.class));
assertTrue(storage.shouldShowDisclosure());
......@@ -68,36 +92,34 @@ public class WebappDisclosureSnackbarControllerTest {
// Simulate resuming or starting again this time no disclosure should show.
assertFalse(storage.shouldShowDisclosure());
controller.maybeShowDisclosure(mActivity, storage, false);
controller.onResumeWithNative();
verify(mManager, times(2)).showSnackbar(any(Snackbar.class));
storage.delete();
}
public void verifyNotShownOnExistingStorageWithoutShouldShowDisclosure(String packageName) {
WebappDisclosureSnackbarController controller = new WebappDisclosureSnackbarController();
WebappDataStorage storage = WebappDataStorage.open(packageName);
WebappDisclosureSnackbarController controller = buildControllerForWebApk(packageName);
WebappDataStorage storage = registerStorageForWebApk(packageName);
// Simulate that starting with existing storage will not cause the disclosure to show.
assertFalse(storage.shouldShowDisclosure());
controller.maybeShowDisclosure(mActivity, storage, false);
controller.onDeferredStartupWithStorage(storage, false /* didCreateStorage */);
verify(mManager, times(0)).showSnackbar(any(Snackbar.class));
storage.delete();
}
public void verifyNeverShown(String packageName) {
WebappDisclosureSnackbarController controller = new WebappDisclosureSnackbarController();
WebappDataStorage storage = WebappDataStorage.open(packageName);
WebappDisclosureSnackbarController controller = buildControllerForWebApk(packageName);
WebappDataStorage storage = registerStorageForWebApk(packageName);
// Try to show the disclosure the first time (fake having no storage on startup by setting
// `force = true`) this shouldn't work as the app was installed via Chrome.
controller.maybeShowDisclosure(mActivity, storage, true);
// Try to show the disclosure the first time.
controller.onDeferredStartupWithStorage(storage, true /* didCreateStorage */);
verify(mManager, times(0)).showSnackbar(any(Snackbar.class));
// Try to the disclosure again this time emulating a restart or a resume (fake having
// storage `force = false`) again this shouldn't work.
controller.maybeShowDisclosure(mActivity, storage, false);
// Try to the disclosure again this time emulating a restart or a resume.
controller.onResumeWithNative();
verify(mManager, times(0)).showSnackbar(any(Snackbar.class));
storage.delete();
......@@ -107,8 +129,6 @@ public class WebappDisclosureSnackbarControllerTest {
@Feature({"Webapps"})
public void testUnboundWebApkShowDisclosure() {
String packageName = "unbound";
doReturn(packageName).when(mActivity).getWebApkPackageName();
verifyShownThenDismissedOnNewCreateStorage(packageName);
}
......@@ -122,17 +142,6 @@ public class WebappDisclosureSnackbarControllerTest {
@Feature({"Webapps"})
public void testBoundWebApkNoDisclosure() {
String packageName = WebApkConstants.WEBAPK_PACKAGE_PREFIX + ".bound";
doReturn(packageName).when(mActivity).getWebApkPackageName();
verifyNeverShown(packageName);
}
@Test
@Feature({"Webapps"})
public void testWebappNoDisclosure() {
String packageName = "webapp";
// Don't set a client package name, it should be null for Webapps.
verifyNeverShown(packageName);
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment