Commit 710de2f6 authored by Patrick Noland's avatar Patrick Noland Committed by Commit Bot

[Chromeshine] Add suspended tab view

When a site is suspended, SuspendedTab is shown in place of the actual
contents of the page. Showing a SuspendedTab stops loading on the
associated tab. Logic for showing a SuspendedTab flows through
PageViewObserver, which already has the knowledge of which domains are
being navigated to and shown. The SuspendedTab will eagerly remove
itself on a new page load, which means every page load on a suspended
site will create a new SuspendedTab. The alternative ended up being much
more complex to code.

We don't actively reload a page when a
site is un-suspended, although this would be fairly easy to add in a
follow-up.


Bug: 902568
Change-Id: Ibc7099eafbab65f583d5989436c8831366a9b040
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1435623
Commit-Queue: Patrick Noland <pnoland@chromium.org>
Reviewed-by: default avatarTheresa <twellington@chromium.org>
Cr-Commit-Position: refs/heads/master@{#637935}
parent c1e30636
<?xml version="1.0" encoding="utf-8"?>
<!-- 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. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:targetApi="21"
android:width="36dp"
android:height="60dp"
android:viewportWidth="36.0"
android:viewportHeight="60.0">
<path
android:pathData="M0.03,18L12,30 0,42v18h36l-0.03,-17.97L36,42 24,30l12,-12V0H0l0.03,18zM30,6v9.51L19.77,25.74 18,27.51l-1.74,-1.74L6.03,15.51 6,6h24z"
android:fillColor="@color/default_icon_color"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:background="@color/modern_primary_color"
android:fillViewport="true"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:paddingTop="24dp"
android:paddingBottom="24dp"
android:orientation="vertical"
android:layout_gravity="center_horizontal"
android:gravity="start" >
<ImageView
android:id="@+id/suspended_tab_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingTop="40dp"
android:paddingBottom="40dp"
app:srcCompat="@drawable/ic_site_timer"
android:importantForAccessibility="no"
android:layout_gravity="start" />
<TextView
android:id="@+id/suspended_tab_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
android:textAppearance="@style/TextAppearance.BlackHeadline"
android:layout_gravity="start"
android:text="@string/usage_stats_site_paused" />
<TextView
android:id="@+id/suspended_tab_explanation"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"
android:paddingBottom="16dp"
android:textAppearance="@style/TextAppearance.BlackBody"
android:layout_gravity="start" />
<org.chromium.ui.widget.ButtonCompat
android:id="@+id/suspended_tab_settings_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:gravity="center_horizontal"
android:text="@string/preferences"
style="@style/TextButton" />
</LinearLayout>
</ScrollView>
\ No newline at end of file
......@@ -37,20 +37,25 @@ public class PageViewObserver {
private final TabObserver mTabObserver;
private final EventTracker mEventTracker;
private final TokenTracker mTokenTracker;
private final SuspensionTracker mSuspensionTracker;
private Tab mCurrentTab;
private String mLastFqdn;
public PageViewObserver(Activity activity, TabModelSelector tabModelSelector,
EventTracker eventTracker, TokenTracker tokenTracker) {
EventTracker eventTracker, TokenTracker tokenTracker,
SuspensionTracker suspensionTracker) {
mActivity = activity;
mTabModelSelector = tabModelSelector;
mEventTracker = eventTracker;
mTokenTracker = tokenTracker;
mSuspensionTracker = suspensionTracker;
mTabObserver = new EmptyTabObserver() {
@Override
public void onShown(Tab tab, @TabSelectionType int type) {
updateUrl(tab.getUrl());
if (!tab.isLoading() && !tab.isBeingRestored()) {
updateUrl(tab.getUrl());
}
}
@Override
......@@ -102,6 +107,13 @@ public class PageViewObserver {
private void updateUrl(String newUrl) {
String newFqdn = newUrl == null ? "" : Uri.parse(newUrl).getHost();
boolean didSuspend = false;
if (newFqdn != null && mSuspensionTracker.isWebsiteSuspended(newFqdn)) {
SuspendedTab.create(mCurrentTab).show();
didSuspend = true;
}
if (mLastFqdn != null && mLastFqdn.equals(newFqdn)) return;
if (mLastFqdn != null) {
......@@ -111,7 +123,7 @@ public class PageViewObserver {
mLastFqdn = null;
}
if (!URLUtil.isHttpUrl(newUrl) && !URLUtil.isHttpsUrl(newUrl)) return;
if (!URLUtil.isHttpUrl(newUrl) && !URLUtil.isHttpsUrl(newUrl) || didSuspend) return;
mLastFqdn = newFqdn;
mEventTracker.addWebsiteEvent(new WebsiteEvent(
......
// 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.usage_stats;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
import android.widget.TextView;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.content_public.browser.LoadUrlParams;
/**
* Represents the suspension page presented when a user tries to visit a site whose fully-qualified
* domain name (FQDN) has been suspended via Digital Wellbeing.
*/
public class SuspendedTab extends EmptyTabObserver {
private static final String DIGITAL_WELLBEING_DASHBOARD_ACTION =
"com.google.android.apps.wellbeing.action.APP_USAGE_DASHBOARD";
private final Tab mTab;
private View mView;
public static SuspendedTab create(Tab tab) {
return new SuspendedTab(tab);
}
private SuspendedTab(Tab tab) {
mTab = tab;
mTab.addObserver(this);
}
/**
* Show the suspended tab UI within the root view of the associated tab. This will stop loading
* of mTab so that the page is not also rendered.
*/
public void show() {
if (mTab.getWebContents() == null) return;
mTab.stopLoading();
attachView();
}
private View createView() {
Context context = mTab.getContext();
LayoutInflater inflater = LayoutInflater.from(context);
String fqdn = Uri.parse(mTab.getUrl()).getHost();
View suspendedTabView = inflater.inflate(R.layout.suspended_tab, null);
TextView explanationText =
(TextView) suspendedTabView.findViewById(R.id.suspended_tab_explanation);
explanationText.setText(
context.getString(R.string.usage_stats_site_paused_explanation, fqdn));
View settingsLink = suspendedTabView.findViewById(R.id.suspended_tab_settings_button);
settingsLink.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(DIGITAL_WELLBEING_DASHBOARD_ACTION);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
});
return suspendedTabView;
}
private void attachView() {
assert mView == null;
ViewGroup parent = mTab.getContentView();
mView = createView();
parent.addView(mView,
new LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
}
private void removeIfPresent() {
removeViewIfPresent();
mTab.removeObserver(this);
mView = null;
}
private void removeViewIfPresent() {
if (isShowing()) {
mTab.getContentView().removeView(mView);
}
}
private boolean isShowing() {
return mView != null && mView.getParent() == mTab.getContentView();
}
// TabObserver implementation.
@Override
public void onLoadUrl(Tab tab, LoadUrlParams params, int loadType) {
removeIfPresent();
}
@Override
public void onPageLoadStarted(Tab tab, String url) {
removeIfPresent();
}
@Override
public void onDestroyed(Tab tab) {
removeIfPresent();
}
// TODO(pnoland): Add integration tests for SuspendedTab that exercise this multi-window logic.
@Override
public void onActivityAttachmentChanged(Tab tab, boolean isAttached) {
if (!isAttached) {
removeViewIfPresent();
} else {
attachView();
}
}
}
......@@ -56,7 +56,8 @@ public class UsageStatsService {
public PageViewObserver createPageViewObserver(
TabModelSelector tabModelSelector, Activity activity) {
ThreadUtils.assertOnUiThread();
return new PageViewObserver(activity, tabModelSelector, mEventTracker, mTokenTracker);
return new PageViewObserver(
activity, tabModelSelector, mEventTracker, mTokenTracker, mSuspensionTracker);
}
/** @return Whether the user has authorized DW to access usage stats data. */
......
......@@ -4040,6 +4040,9 @@ To change this setting, <ph name="BEGIN_LINK">&lt;resetlink&gt;</ph>reset sync<p
<message name="IDS_AUTOFILL_ASSISTANT_3RD_PARTY_TERMS_REVIEW" desc="Message that indicates that the user wants to review the terms and conditions of a 3rd party's domain, e.g., 'odeon.co.uk'.">
Read and agree to the terms &amp; conditions on <ph name="BEGIN_BOLD">&lt;b&gt;</ph><ph name="DOMAIN">%1$s<ex>google.com</ex></ph><ph name="END_BOLD">&lt;/b&gt;</ph> later
</message>
<message name="IDS_AUTOFILL_ASSISTANT_3RD_PARTY_PRIVACY_NOTICE" desc="Privacy notice telling users that autofill assistant will send personal data to a third party’s website.">
Chrome will send personal data you selected to <ph name="BEGIN_BOLD">&lt;b&gt;</ph><ph name="DOMAIN">%1$s<ex>google.com</ex></ph><ph name="END_BOLD">&lt;/b&gt;</ph>
</message>
<!-- Usage Stats strings -->
<message name="IDS_USAGE_STATS_CONSENT_TITLE" desc="Title for activity authorizing Digital Wellbeing to access Chrome usage data">
Connect Chrome to Digital Wellbeing?
......@@ -4050,8 +4053,11 @@ To change this setting, <ph name="BEGIN_LINK">&lt;resetlink&gt;</ph>reset sync<p
<message name="IDS_USAGE_STATS_SETTING_TITLE" desc="Title for setting toggling Digital Wellbeing to access Chrome usage data">
Connect to Digital Wellbeing
</message>
<message name="IDS_AUTOFILL_ASSISTANT_3RD_PARTY_PRIVACY_NOTICE" desc="Privacy notice telling users that autofill assistant will send personal data to a third party’s website.">
Chrome will send personal data you selected to <ph name="BEGIN_BOLD">&lt;b&gt;</ph><ph name="DOMAIN">%1$s<ex>google.com</ex></ph><ph name="END_BOLD">&lt;/b&gt;</ph>
<message name="IDS_USAGE_STATS_SITE_PAUSED" desc="Message when a website is suspended due to exceeding a user-defined limit">
Site paused
</message>
<message name="IDS_USAGE_STATS_SITE_PAUSED_EXPLANATION" desc="Message when a website is suspended due to exceeding a user-defined limit">
Your <ph name="FQDN">%1$s</ph> timer ran out. It'll start again tomorrow.
</message>
<!-- Bottom Tab Grid strings -->
......
2a3ebe6e93a81a36dd04d7d2be1faf0bef1f140c
\ No newline at end of file
2a3ebe6e93a81a36dd04d7d2be1faf0bef1f140c
\ No newline at end of file
......@@ -1665,6 +1665,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/upgrade/UpgradeIntentService.java",
"java/src/org/chromium/chrome/browser/usage_stats/EventTracker.java",
"java/src/org/chromium/chrome/browser/usage_stats/PageViewObserver.java",
"java/src/org/chromium/chrome/browser/usage_stats/SuspendedTab.java",
"java/src/org/chromium/chrome/browser/usage_stats/SuspensionTracker.java",
"java/src/org/chromium/chrome/browser/usage_stats/TokenGenerator.java",
"java/src/org/chromium/chrome/browser/usage_stats/TokenTracker.java",
......
......@@ -4,6 +4,7 @@
package org.chromium.chrome.browser.usage_stats;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doReturn;
......@@ -57,6 +58,8 @@ public final class PageViewObserverTest {
private EventTracker mEventTracker;
@Mock
private TokenTracker mTokenTracker;
@Mock
private SuspensionTracker mSuspensionTracker;
@Captor
private ArgumentCaptor<TabObserver> mTabObserverCaptor;
@Captor
......@@ -194,9 +197,36 @@ public final class PageViewObserverTest {
verify(mEventTracker, times(0)).addWebsiteEvent(argThat(isStopEvent(DIFFERENT_FQDN)));
}
@Test
public void navigationToSuspendedDomain_suspendedTabShown() {
PageViewObserver observer = createPageViewObserver();
onUpdateUrl(mTab, STARTING_URL);
doReturn(true).when(mSuspensionTracker).isWebsiteSuspended(DIFFERENT_FQDN);
onUpdateUrl(mTab, DIFFERENT_URL);
verify(mTab, times(2)).addObserver(mTabObserverCaptor.capture());
assertTrue(mTabObserverCaptor.getValue() instanceof SuspendedTab);
}
@Test
public void navigationToUnsuspendedDomain_suspendedTabRemoved() {
PageViewObserver observer = createPageViewObserver();
onUpdateUrl(mTab, STARTING_URL);
doReturn(true).when(mSuspensionTracker).isWebsiteSuspended(DIFFERENT_FQDN);
onUpdateUrl(mTab, DIFFERENT_URL);
verify(mTab, times(2)).addObserver(mTabObserverCaptor.capture());
SuspendedTab suspendedTab = (SuspendedTab) mTabObserverCaptor.getValue();
suspendedTab.onPageLoadStarted(mTab, STARTING_URL);
verify(mTab, times(1)).removeObserver(suspendedTab);
}
private PageViewObserver createPageViewObserver() {
PageViewObserver observer =
new PageViewObserver(mActivity, mTabModelSelector, mEventTracker, mTokenTracker);
PageViewObserver observer = new PageViewObserver(
mActivity, mTabModelSelector, mEventTracker, mTokenTracker, mSuspensionTracker);
verify(mTabModel, times(1)).addObserver(mTabModelObserverCaptor.capture());
if (mTabModelSelector.getCurrentTab() != null) {
verify(mTabModelSelector.getCurrentTab(), times(1))
......
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