1 // Copyright 2019 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.chrome.browser.tasks; 6 7 import android.content.Context; 8 import android.content.SharedPreferences; 9 import android.os.Handler; 10 11 import androidx.annotation.NonNull; 12 import androidx.annotation.VisibleForTesting; 13 14 import org.chromium.base.ContextUtils; 15 import org.chromium.base.metrics.RecordHistogram; 16 import org.chromium.base.task.AsyncTask; 17 import org.chromium.chrome.browser.compositor.layouts.EmptyOverviewModeObserver; 18 import org.chromium.chrome.browser.compositor.layouts.OverviewModeBehavior; 19 import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher; 20 import org.chromium.chrome.browser.lifecycle.Destroyable; 21 import org.chromium.chrome.browser.tab.Tab; 22 import org.chromium.chrome.browser.tab.TabHidingType; 23 import org.chromium.chrome.browser.tab.TabObserver; 24 import org.chromium.chrome.browser.tab.TabSelectionType; 25 import org.chromium.chrome.browser.tabmodel.TabModelObserver; 26 import org.chromium.chrome.browser.tabmodel.TabModelSelector; 27 import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabModelObserver; 28 import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabObserver; 29 import org.chromium.chrome.browser.version.ChromeVersionInfo; 30 import org.chromium.content_public.browser.LoadUrlParams; 31 import org.chromium.content_public.browser.NavigationHandle; 32 import org.chromium.ui.base.PageTransition; 33 34 import java.util.HashMap; 35 import java.util.Map; 36 import java.util.concurrent.TimeUnit; 37 38 /** 39 * Manages Journey related signals, specifically those related to tab engagement. 40 */ 41 public class JourneyManager implements Destroyable { 42 @VisibleForTesting 43 static final String PREFS_FILE = "last_engagement_for_tab_id_pref"; 44 45 @VisibleForTesting 46 static final String TAB_REVISIT_METRIC = "Tabs.TimeSinceLastView.OnTabView"; 47 48 @VisibleForTesting 49 static final String TAB_CLOSE_METRIC = "Tabs.TimeSinceLastView.OnTabClose"; 50 51 @VisibleForTesting 52 static final String TAB_CLOBBER_METRIC = "Tabs.TimeSinceLastView.OnTabClobber"; 53 54 private static final long INVALID_TIME = -1; 55 56 // We track this in seconds because UMA can only handle 32-bit signed integers, which 45 days 57 // will overflow. 58 private static final int MAX_ENGAGEMENT_TIME_S = (int) TimeUnit.DAYS.toSeconds(45); 59 60 private final TabModelSelectorTabObserver mTabModelSelectorTabObserver; 61 private final TabModelSelectorTabModelObserver mTabModelSelectorTabModelObserver; 62 private final OverviewModeBehavior.OverviewModeObserver mOverviewModeObserver; 63 private final ActivityLifecycleDispatcher mLifecycleDispatcher; 64 private final OverviewModeBehavior mOverviewModeBehavior; 65 private final EngagementTimeUtil mEngagementTimeUtil; 66 67 private Map<Integer, Boolean> mDidFirstPaintPerTab = new HashMap<>(); 68 private Map<Integer, Runnable> mPendingRevisits = new HashMap<>(); 69 private final Handler mHandler = new Handler(); 70 private Tab mCurrentTab; 71 JourneyManager(TabModelSelector selector, @NonNull ActivityLifecycleDispatcher dispatcher, @NonNull OverviewModeBehavior overviewModeBehavior, EngagementTimeUtil engagementTimeUtil)72 public JourneyManager(TabModelSelector selector, 73 @NonNull ActivityLifecycleDispatcher dispatcher, 74 @NonNull OverviewModeBehavior overviewModeBehavior, 75 EngagementTimeUtil engagementTimeUtil) { 76 if (!ChromeVersionInfo.isLocalBuild() && !ChromeVersionInfo.isCanaryBuild() 77 && !ChromeVersionInfo.isDevBuild()) { 78 // We do not want this in beta/stable until it's no longer backed by SharedPreferences. 79 mTabModelSelectorTabObserver = null; 80 mTabModelSelectorTabModelObserver = null; 81 mOverviewModeObserver = null; 82 mLifecycleDispatcher = null; 83 mEngagementTimeUtil = null; 84 mOverviewModeBehavior = null; 85 return; 86 } 87 88 mTabModelSelectorTabObserver = new TabModelSelectorTabObserver(selector) { 89 @Override 90 public void onShown(Tab tab, @TabSelectionType int type) { 91 if (type != TabSelectionType.FROM_USER) return; 92 93 mCurrentTab = tab; 94 95 recordDeferredEngagementMetric(tab); 96 } 97 98 @Override 99 public void onHidden(Tab tab, @TabHidingType int reason) { 100 handleTabEngagementStopped(tab); 101 } 102 103 @Override 104 public void onClosingStateChanged(Tab tab, boolean closing) { 105 if (!closing) return; 106 107 mCurrentTab = null; 108 109 recordTabCloseMetric(tab); 110 } 111 112 @Override 113 public void onDidStartNavigation(Tab tab, NavigationHandle navigationHandle) { 114 if (!navigationHandle.isInMainFrame() || navigationHandle.isSameDocument()) return; 115 116 mDidFirstPaintPerTab.put(tab.getId(), false); 117 } 118 119 @Override 120 public void didFirstVisuallyNonEmptyPaint(Tab tab) { 121 if (!mDidFirstPaintPerTab.containsKey(tab.getId())) { 122 // If this is the first paint of this Tab in the current app lifetime, record. 123 // e.g. First load of the tab after cold start. 124 recordDeferredEngagementMetric(tab); 125 } 126 127 mCurrentTab = tab; 128 129 mDidFirstPaintPerTab.put(tab.getId(), true); 130 131 handleTabEngagementStarted(tab); 132 } 133 134 @Override 135 public void onLoadUrl(Tab tab, LoadUrlParams params, int loadType) { 136 // The transition source (e.g. FROM_ADDRESS_BAR = 0x02000000) is bitwise OR'ed with 137 // the transition method (e.g. TYPED = 0x01) and we are only interested in whether 138 // a navigation happened from the address bar. 139 if ((params.getTransitionType() & PageTransition.FROM_ADDRESS_BAR) == 0) return; 140 141 int tabId = tab.getId(); 142 143 if (!mPendingRevisits.containsKey(tabId)) { 144 return; 145 } 146 147 mHandler.removeCallbacks(mPendingRevisits.get(tabId)); 148 mPendingRevisits.remove(tabId); 149 150 recordTabClobberMetric(tab, params.getInputStartTimestamp()); 151 } 152 }; 153 154 mTabModelSelectorTabModelObserver = new TabModelSelectorTabModelObserver(selector) { 155 @Override 156 public void tabClosureCommitted(Tab tab) { 157 getPrefs().edit().remove(String.valueOf(tab.getId())).apply(); 158 } 159 }; 160 161 mOverviewModeBehavior = overviewModeBehavior; 162 mOverviewModeObserver = new EmptyOverviewModeObserver() { 163 @Override 164 public void onOverviewModeStartedShowing(boolean showToolbar) { 165 handleTabEngagementStopped(mCurrentTab); 166 } 167 }; 168 mOverviewModeBehavior.addOverviewModeObserver(mOverviewModeObserver); 169 170 mLifecycleDispatcher = dispatcher; 171 mLifecycleDispatcher.register(this); 172 173 mEngagementTimeUtil = engagementTimeUtil; 174 } 175 176 @Override destroy()177 public void destroy() { 178 mTabModelSelectorTabObserver.destroy(); 179 mTabModelSelectorTabModelObserver.destroy(); 180 mOverviewModeBehavior.removeOverviewModeObserver(mOverviewModeObserver); 181 mLifecycleDispatcher.unregister(this); 182 } 183 handleTabEngagementStarted(Tab tab)184 private void handleTabEngagementStarted(Tab tab) { 185 if (tab == null) return; 186 187 Boolean didFirstPaint = mDidFirstPaintPerTab.get(tab.getId()); 188 if (didFirstPaint == null || !didFirstPaint) return; 189 190 storeLastEngagement(tab.getId(), mEngagementTimeUtil.currentTime()); 191 } 192 handleTabEngagementStopped(Tab tab)193 private void handleTabEngagementStopped(Tab tab) { 194 if (tab == null) return; 195 196 if (mPendingRevisits.containsKey(tab.getId())) { 197 mHandler.removeCallbacks(mPendingRevisits.get(tab.getId())); 198 mPendingRevisits.remove(tab.getId()); 199 } 200 201 long lastEngagementMs = mEngagementTimeUtil.currentTime(); 202 203 Boolean didFirstPaint = mDidFirstPaintPerTab.get(tab.getId()); 204 if (didFirstPaint == null || !didFirstPaint) { 205 return; 206 } 207 208 storeLastEngagement(tab.getId(), lastEngagementMs); 209 } 210 storeLastEngagement(int tabId, long lastEngagementTimestampMs)211 private void storeLastEngagement(int tabId, long lastEngagementTimestampMs) { 212 new AsyncTask<Void>() { 213 @Override 214 protected Void doInBackground() { 215 getPrefs().edit().putLong(String.valueOf(tabId), lastEngagementTimestampMs).apply(); 216 return null; 217 } 218 219 @Override 220 protected void onPostExecute(Void result) {} 221 }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); 222 } 223 getPrefs()224 private SharedPreferences getPrefs() { 225 // TODO(mattsimmons): Add a native counterpart to this class and don't write directly to 226 // shared prefs. 227 return ContextUtils.getApplicationContext().getSharedPreferences( 228 PREFS_FILE, Context.MODE_PRIVATE); 229 } 230 getLastEngagementTimestamp(Tab tab)231 private long getLastEngagementTimestamp(Tab tab) { 232 return getPrefs().getLong(String.valueOf(tab.getId()), INVALID_TIME); 233 } 234 recordDeferredEngagementMetric(Tab tab)235 private void recordDeferredEngagementMetric(Tab tab) { 236 final long currentEngagementTimeMs = mEngagementTimeUtil.currentTime(); 237 238 assert (!mPendingRevisits.containsKey(tab.getId())); 239 240 Runnable viewMetricTask = () -> recordViewMetric(tab, currentEngagementTimeMs); 241 mPendingRevisits.put(tab.getId(), viewMetricTask); 242 mHandler.postDelayed(viewMetricTask, mEngagementTimeUtil.tabClobberThresholdMillis()); 243 } 244 recordViewMetric(Tab tab, long viewTimeMs)245 private void recordViewMetric(Tab tab, long viewTimeMs) { 246 mPendingRevisits.remove(tab.getId()); 247 248 long lastEngagement = getLastEngagementTimestamp(tab); 249 250 if (lastEngagement == INVALID_TIME) return; 251 252 long elapsedMs = mEngagementTimeUtil.timeSinceLastEngagement(lastEngagement, viewTimeMs); 253 254 recordEngagementMetric(TAB_REVISIT_METRIC, elapsedMs); 255 256 handleTabEngagementStarted(tab); 257 } 258 recordTabCloseMetric(Tab tab)259 private void recordTabCloseMetric(Tab tab) { 260 long lastEngagement = getLastEngagementTimestamp(tab); 261 262 if (lastEngagement == INVALID_TIME) return; 263 264 long elapsedMs = mEngagementTimeUtil.timeSinceLastEngagement(lastEngagement); 265 266 recordEngagementMetric(TAB_CLOSE_METRIC, elapsedMs); 267 } 268 recordTabClobberMetric(Tab tab, long inputStartTimeTicksMs)269 private void recordTabClobberMetric(Tab tab, long inputStartTimeTicksMs) { 270 long lastEngagement = getLastEngagementTimestamp(tab); 271 272 if (lastEngagement == INVALID_TIME) return; 273 274 long elapsedMs = mEngagementTimeUtil.timeSinceLastEngagementFromTimeTicksMs( 275 lastEngagement, inputStartTimeTicksMs); 276 277 recordEngagementMetric(TAB_CLOBBER_METRIC, elapsedMs); 278 } 279 recordEngagementMetric(String name, long elapsedMs)280 private void recordEngagementMetric(String name, long elapsedMs) { 281 if (elapsedMs == INVALID_TIME) return; 282 283 int elapsedSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(elapsedMs); 284 285 RecordHistogram.recordCustomCountHistogram( 286 name, elapsedSeconds, 1, MAX_ENGAGEMENT_TIME_S, 50); 287 } 288 289 @VisibleForTesting getTabModelSelectorTabObserver()290 public TabObserver getTabModelSelectorTabObserver() { 291 return mTabModelSelectorTabObserver; 292 } 293 294 @VisibleForTesting getTabModelSelectorTabModelObserver()295 public TabModelObserver getTabModelSelectorTabModelObserver() { 296 return mTabModelSelectorTabModelObserver; 297 } 298 299 @VisibleForTesting getOverviewModeObserver()300 public OverviewModeBehavior.OverviewModeObserver getOverviewModeObserver() { 301 return mOverviewModeObserver; 302 } 303 } 304