Make Android Clipboard Keep Track of Last Modified Time
And expose it the C++ code.
To do in a later changelist: add lightweight code to store the hash and update time in prefs so that Chrome can have a better chance of using the current clipboard content on startup. (If the clipboard content hasn't changed since the last time Chrome ran, the last modified date is likely correct.)
BUG=682446
Review-Url: https://codereview.chromium.org/2766623003
Cr-Commit-Position: refs/heads/master@{#461234}
diff --git a/base/android/java/src/org/chromium/base/metrics/RecordUserAction.java b/base/android/java/src/org/chromium/base/metrics/RecordUserAction.java
index 06004d69..5334746 100644
--- a/base/android/java/src/org/chromium/base/metrics/RecordUserAction.java
+++ b/base/android/java/src/org/chromium/base/metrics/RecordUserAction.java
@@ -5,6 +5,7 @@
package org.chromium.base.metrics;
import org.chromium.base.ThreadUtils;
+import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.JNINamespace;
/**
@@ -18,7 +19,23 @@
*/
@JNINamespace("base::android")
public class RecordUserAction {
+ private static Throwable sDisabledBy;
+
+ /**
+ * Tests may not have native initialized, so they may need to disable metrics. The value should
+ * be reset after the test done, to avoid carrying over state to unrelated tests.
+ */
+ @VisibleForTesting
+ public static void setDisabledForTests(boolean disabled) {
+ if (disabled && sDisabledBy != null) {
+ throw new IllegalStateException("UserActions are already disabled.", sDisabledBy);
+ }
+ sDisabledBy = disabled ? new Throwable() : null;
+ }
+
public static void record(final String action) {
+ if (sDisabledBy != null) return;
+
if (ThreadUtils.runningOnUiThread()) {
nativeRecordUserAction(action);
return;
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/DisableHistogramsRule.java b/chrome/android/junit/src/org/chromium/chrome/browser/DisableHistogramsRule.java
index 506da77..0859ae5 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/DisableHistogramsRule.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/DisableHistogramsRule.java
@@ -22,4 +22,4 @@
protected void after() {
RecordHistogram.setDisabledForTests(false);
}
-}
\ No newline at end of file
+}
diff --git a/tools/metrics/actions/actions.xml b/tools/metrics/actions/actions.xml
index 70e2e726..63d9443 100644
--- a/tools/metrics/actions/actions.xml
+++ b/tools/metrics/actions/actions.xml
@@ -10103,10 +10103,14 @@
<action name="MobileOmniboxClipboardChanged">
<owner>jif@chromium.org</owner>
<description>
- Emitted when Chrome detects that the clipboard contains a new URL. This
- occurs either when Chrome enters the foreground and notices that the content
- of the clipboard changed, or when the clipboard changes while Chrome is in
- the foreground.
+ Emitted when Chrome detects that the clipboard contains a new URL.
+
+ On iOS: this occurs either when Chrome enters the foreground and notices
+ that the content of the clipboard changed, or when the clipboard changes
+ while Chrome is in the foreground.
+
+ On Android: this occurs when Chrome starts up or when the clipboard changes
+ while Chrome is running (in the foreground or not).
</description>
</action>
diff --git a/tools/metrics/histograms/histograms.xml b/tools/metrics/histograms/histograms.xml
index e572aa68..a416caa 100644
--- a/tools/metrics/histograms/histograms.xml
+++ b/tools/metrics/histograms/histograms.xml
@@ -7466,6 +7466,15 @@
</summary>
</histogram>
+<histogram name="Clipboard.ConstructedHasher" enum="BooleanSuccess">
+ <owner>mpearson@chromium.org</owner>
+ <summary>
+ Whether Android's Clipboard.java successfully constructed a hasher to hash
+ clipboard entries. Recorded on construction of the class, which happens
+ only on startup.
+ </summary>
+</histogram>
+
<histogram name="Clipboard.IncognitoUseCase" enum="ClipboardAction">
<obsolete>
Deprecated as of 4/2013, experiment confirmed correctness of our patch.
diff --git a/ui/android/BUILD.gn b/ui/android/BUILD.gn
index b401083..97e17e1 100644
--- a/ui/android/BUILD.gn
+++ b/ui/android/BUILD.gn
@@ -249,12 +249,14 @@
junit_binary("ui_junit_tests") {
java_files = [
+ "junit/src/org/chromium/ui/base/ClipboardTest.java",
"junit/src/org/chromium/ui/base/SelectFileDialogTest.java",
"junit/src/org/chromium/ui/text/SpanApplierTest.java",
]
deps = [
":ui_java",
"//base:base_java",
+ "//base:base_java_test_support",
]
}
diff --git a/ui/android/java/src/org/chromium/ui/base/Clipboard.java b/ui/android/java/src/org/chromium/ui/base/Clipboard.java
index 4554352a..5755f18 100644
--- a/ui/android/java/src/org/chromium/ui/base/Clipboard.java
+++ b/ui/android/java/src/org/chromium/ui/base/Clipboard.java
@@ -9,25 +9,45 @@
import android.content.Context;
import org.chromium.base.ContextUtils;
+import org.chromium.base.Log;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.SuppressFBWarnings;
+import org.chromium.base.metrics.RecordHistogram;
+import org.chromium.base.metrics.RecordUserAction;
import org.chromium.ui.R;
import org.chromium.ui.widget.Toast;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
/**
* Simple proxy that provides C++ code with an access pathway to the Android clipboard.
*/
@JNINamespace("ui")
-public class Clipboard {
+public class Clipboard implements ClipboardManager.OnPrimaryClipChangedListener {
private static Clipboard sInstance;
+ private static final String TAG = "Clipboard";
+
// Necessary for coercing clipboard contents to text if they require
// access to network resources, etceteras (e.g., URI in clipboard)
private final Context mContext;
private final ClipboardManager mClipboardManager;
+ // A message hasher that's used to hash clipboard contents so we can tell
+ // when a clipboard changes without storing the full contents.
+ private MessageDigest mMd5Hasher;
+ // The hash of the current clipboard.
+ // TODO(mpearson): unsuppress this warning once saving and restoring
+ // the hash from prefs is added.
+ @SuppressFBWarnings("URF_UNREAD_FIELD")
+ private byte[] mClipboardMd5;
+ // The time when the clipboard was last updated. Set to 0 if unknown.
+ private long mClipboardChangeTime;
+
/**
* Get the singleton Clipboard instance (creating it if needed).
*/
@@ -44,6 +64,20 @@
mClipboardManager =
(ClipboardManager) ContextUtils.getApplicationContext().getSystemService(
Context.CLIPBOARD_SERVICE);
+ mClipboardManager.addPrimaryClipChangedListener(this);
+ try {
+ mMd5Hasher = MessageDigest.getInstance("MD5");
+ mClipboardMd5 = weakMd5Hash();
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(TAG,
+ "Unable to construct MD5 MessageDigest: %s; assume "
+ + "clipboard last update time is start of epoch.",
+ e);
+ mMd5Hasher = null;
+ mClipboardMd5 = new byte[] {};
+ }
+ RecordHistogram.recordBooleanHistogram("Clipboard.ConstructedHasher", mMd5Hasher != null);
+ mClipboardChangeTime = 0;
}
/**
@@ -95,6 +129,21 @@
}
/**
+ * Gets the time the clipboard content last changed.
+ *
+ * This is calculated according to the device's clock. E.g., it continues
+ * increasing when the device is suspended. Likewise, it can be in the
+ * future if the user's clock updated after this information was recorded.
+ *
+ * @return a Java long recording the last changed time in milliseconds since
+ * epoch, or 0 if the time could not be determined.
+ */
+ @CalledByNative
+ public long getClipboardContentChangeTimeInMillis() {
+ return mClipboardChangeTime;
+ }
+
+ /**
* Emulates the behavior of the now-deprecated
* {@link android.text.ClipboardManager#setText(CharSequence)}, setting the
* clipboard's current primary clip to a plain-text clip that consists of
@@ -139,4 +188,37 @@
Toast.makeText(mContext, text, Toast.LENGTH_SHORT).show();
}
}
+
+ /**
+ * Updates mClipboardMd5 and mClipboardChangeTime when the clipboard updates.
+ *
+ * Implements OnPrimaryClipChangedListener to listen for clipboard updates.
+ */
+ @Override
+ public void onPrimaryClipChanged() {
+ if (mMd5Hasher == null) return;
+ RecordUserAction.record("MobileOmniboxClipboardChanged");
+ mClipboardMd5 = weakMd5Hash();
+ // Always update the clipboard change time even if the clipboard
+ // content hasn't changed. This is because if the user put something
+ // in the clipboard recently (even if it was not necessary because it
+ // was already there), that content should be considered recent.
+ mClipboardChangeTime = System.currentTimeMillis();
+ }
+
+ /**
+ * Returns a weak hash of getCoercedText().
+ *
+ * @return a Java byte[] with the weak hash.
+ */
+ private byte[] weakMd5Hash() {
+ if (getCoercedText() == null) {
+ return new byte[] {};
+ }
+ // Compute a hash consisting of the first 4 bytes of the MD5 hash of
+ // getCoercedText(). This value is used to detect clipboard content
+ // change. Keeping only 4 bytes is a privacy requirement to introduce
+ // collision and allow deniability of having copied a given string.
+ return Arrays.copyOfRange(mMd5Hasher.digest(getCoercedText().getBytes()), 0, 4);
+ }
}
diff --git a/ui/android/junit/src/org/chromium/ui/base/ClipboardTest.java b/ui/android/junit/src/org/chromium/ui/base/ClipboardTest.java
new file mode 100644
index 0000000..a4bd966
--- /dev/null
+++ b/ui/android/junit/src/org/chromium/ui/base/ClipboardTest.java
@@ -0,0 +1,50 @@
+// Copyright 2017 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.ui.base;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import org.chromium.base.ContextUtils;
+import org.chromium.base.metrics.RecordHistogram;
+import org.chromium.base.metrics.RecordUserAction;
+import org.chromium.testing.local.LocalRobolectricTestRunner;
+
+/**
+ * Tests logic in the Clipboard class.
+ */
+@RunWith(LocalRobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class ClipboardTest {
+ @BeforeClass
+ public static void beforeClass() {
+ RecordHistogram.setDisabledForTests(true);
+ RecordUserAction.setDisabledForTests(true);
+ }
+
+ @AfterClass
+ public static void afterClass() {
+ RecordHistogram.setDisabledForTests(false);
+ RecordUserAction.setDisabledForTests(false);
+ }
+
+ @Test
+ public void testGetClipboardContentChangeTimeInMillis() {
+ ContextUtils.initApplicationContext(RuntimeEnvironment.application);
+ // Upon launch, the clipboard should be at start of epoch, i.e., ancient.
+ Clipboard clipboard = Clipboard.getInstance();
+ assertEquals(0, clipboard.getClipboardContentChangeTimeInMillis());
+ // After updating the clipboard, it should have a new time.
+ clipboard.onPrimaryClipChanged();
+ assertTrue(clipboard.getClipboardContentChangeTimeInMillis() > 0);
+ }
+}
diff --git a/ui/base/clipboard/clipboard.cc b/ui/base/clipboard/clipboard.cc
index 9ab641c..6c98121 100644
--- a/ui/base/clipboard/clipboard.cc
+++ b/ui/base/clipboard/clipboard.cc
@@ -86,6 +86,10 @@
clipboard_map->erase(it);
}
+base::Time Clipboard::GetClipboardLastModifiedTime() const {
+ return base::Time();
+}
+
void Clipboard::DispatchObject(ObjectType type, const ObjectMapParams& params) {
// Ignore writes with empty parameters.
for (const auto& param : params) {
diff --git a/ui/base/clipboard/clipboard.h b/ui/base/clipboard/clipboard.h
index 8ccb873c..5d27f2c 100644
--- a/ui/base/clipboard/clipboard.h
+++ b/ui/base/clipboard/clipboard.h
@@ -21,6 +21,7 @@
#include "base/synchronization/lock.h"
#include "base/threading/platform_thread.h"
#include "base/threading/thread_checker.h"
+#include "base/time/time.h"
#include "build/build_config.h"
#include "ui/base/clipboard/clipboard_types.h"
#include "ui/base/ui_base_export.h"
@@ -204,6 +205,10 @@
virtual void ReadData(const FormatType& format,
std::string* result) const = 0;
+ // Returns an estimate of the time the clipboard was last updated. If the
+ // time is unknown, returns Time::Time().
+ virtual base::Time GetClipboardLastModifiedTime() const;
+
// Gets the FormatType corresponding to an arbitrary format string,
// registering it with the system if needed. Due to Windows/Linux
// limitiations, |format_string| must never be controlled by the user.
diff --git a/ui/base/clipboard/clipboard_android.cc b/ui/base/clipboard/clipboard_android.cc
index 2da7c540..8f35282 100644
--- a/ui/base/clipboard/clipboard_android.cc
+++ b/ui/base/clipboard/clipboard_android.cc
@@ -4,6 +4,9 @@
#include "ui/base/clipboard/clipboard_android.h"
+#include <algorithm>
+
+#include "base/android/context_utils.h"
#include "base/android/jni_string.h"
#include "base/lazy_instance.h"
#include "base/stl_util.h"
@@ -47,6 +50,7 @@
public:
ClipboardMap();
std::string Get(const std::string& format);
+ int64_t GetLastClipboardChangeTimeInMillis();
bool HasFormat(const std::string& format);
void Set(const std::string& format, const std::string& data);
void CommitToAndroidClipboard();
@@ -57,6 +61,8 @@
std::map<std::string, std::string> map_;
base::Lock lock_;
+ int64_t last_clipboard_change_time_ms_;
+
// Java class and methods for the Android ClipboardManager.
ScopedJavaGlobalRef<jobject> clipboard_manager_;
};
@@ -74,6 +80,12 @@
return it == map_.end() ? std::string() : it->second;
}
+int64_t ClipboardMap::GetLastClipboardChangeTimeInMillis() {
+ base::AutoLock lock(lock_);
+ UpdateFromAndroidClipboard();
+ return last_clipboard_change_time_ms_;
+}
+
bool ClipboardMap::HasFormat(const std::string& format) {
base::AutoLock lock(lock_);
UpdateFromAndroidClipboard();
@@ -158,6 +170,9 @@
AddMapEntry(env, &android_clipboard_state, kPlainTextFormat, jtext);
AddMapEntry(env, &android_clipboard_state, kHTMLFormat, jhtml);
+ last_clipboard_change_time_ms_ =
+ Java_Clipboard_getClipboardContentChangeTimeInMillis(env,
+ clipboard_manager_);
if (!MapIsSubset(android_clipboard_state, map_))
android_clipboard_state.swap(map_);
@@ -399,6 +414,12 @@
*result = g_map.Get().Get(format.ToString());
}
+base::Time ClipboardAndroid::GetClipboardLastModifiedTime() const {
+ DCHECK(CalledOnValidThread());
+ return base::Time::FromJavaTime(
+ g_map.Get().GetLastClipboardChangeTimeInMillis());
+}
+
// Main entry point used to write several values in the clipboard.
void ClipboardAndroid::WriteObjects(ClipboardType type,
const ObjectMap& objects) {
diff --git a/ui/base/clipboard/clipboard_android.h b/ui/base/clipboard/clipboard_android.h
index b86d275..a41550a2 100644
--- a/ui/base/clipboard/clipboard_android.h
+++ b/ui/base/clipboard/clipboard_android.h
@@ -45,6 +45,7 @@
base::string16* result) const override;
void ReadBookmark(base::string16* title, std::string* url) const override;
void ReadData(const FormatType& format, std::string* result) const override;
+ base::Time GetClipboardLastModifiedTime() const override;
void WriteObjects(ClipboardType type, const ObjectMap& objects) override;
void WriteText(const char* text_data, size_t text_len) override;
void WriteHTML(const char* markup_data,