Sending focused annotation update to the host

Adding a new method FFI_OnFocusChange() in FPDF_FORMFILLINFO with
version = 2 which will send the focused annotation whenever it changes.
This will be used to bring the focused annotation into view via
scrolling.

Bug: chromium:989027
Change-Id: Ie0d1523f14234a6738ad1f390dd1deb09b03834d
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/59270
Reviewed-by: Lei Zhang <thestig@chromium.org>
Reviewed-by: Tom Sepez <tsepez@chromium.org>
Commit-Queue: Aayush Dhir <aadhir@microsoft.com>
diff --git a/fpdfsdk/cpdfsdk_formfillenvironment.cpp b/fpdfsdk/cpdfsdk_formfillenvironment.cpp
index feeb2b4..243528e 100644
--- a/fpdfsdk/cpdfsdk_formfillenvironment.cpp
+++ b/fpdfsdk/cpdfsdk_formfillenvironment.cpp
@@ -10,6 +10,7 @@
 #include <utility>
 #include <vector>
 
+#include "core/fpdfapi/page/cpdf_annotcontext.h"
 #include "core/fpdfapi/parser/cpdf_array.h"
 #include "core/fpdfapi/parser/cpdf_dictionary.h"
 #include "core/fpdfdoc/cpdf_nametree.h"
@@ -706,6 +707,10 @@
     return false;
 
   m_pFocusAnnot.Reset(pAnnot->Get());
+
+  // If we are not able to inform the client about the focus change, it
+  // shouldn't be considered as failure.
+  SendOnFocusChange(pAnnot);
   return true;
 }
 
@@ -747,3 +752,31 @@
 bool CPDFSDK_FormFillEnvironment::HasPermissions(uint32_t flags) const {
   return !!(m_pCPDFDoc->GetUserPermissions() & flags);
 }
+
+void CPDFSDK_FormFillEnvironment::SendOnFocusChange(
+    ObservedPtr<CPDFSDK_Annot>* pAnnot) {
+  if (!m_pInfo || m_pInfo->version < 2 || !m_pInfo->FFI_OnFocusChange)
+    return;
+
+  if ((*pAnnot)->AsXFAWidget()) {
+    // TODO(crbug.com/pdfium/1482): Handle XFA case.
+    return;
+  }
+
+  CPDFSDK_PageView* pPageView = (*pAnnot)->GetPageView();
+  if (!pPageView || !pPageView->IsValid())
+    return;
+
+  CPDF_Page* cpdf_page = (*pAnnot)->GetPDFPage();
+  if (!cpdf_page)
+    return;
+
+  CPDF_Dictionary* annot_dict = (*pAnnot)->GetPDFAnnot()->GetAnnotDict();
+
+  auto focused_annot =
+      pdfium::MakeUnique<CPDF_AnnotContext>(annot_dict, cpdf_page);
+  FPDF_ANNOTATION fpdf_annot =
+      FPDFAnnotationFromCPDFAnnotContext(focused_annot.get());
+
+  m_pInfo->FFI_OnFocusChange(m_pInfo, fpdf_annot, pPageView->GetPageIndex());
+}
\ No newline at end of file
diff --git a/fpdfsdk/cpdfsdk_formfillenvironment.h b/fpdfsdk/cpdfsdk_formfillenvironment.h
index 71a4fa8..d7ccc23 100644
--- a/fpdfsdk/cpdfsdk_formfillenvironment.h
+++ b/fpdfsdk/cpdfsdk_formfillenvironment.h
@@ -209,6 +209,7 @@
 
  private:
   IPDF_Page* GetPage(int nIndex);
+  void SendOnFocusChange(ObservedPtr<CPDFSDK_Annot>* pAnnot);
 
   FPDF_FORMFILLINFO* const m_pInfo;
   std::unique_ptr<CPDFSDK_ActionHandler> m_pActionHandler;
diff --git a/fpdfsdk/fpdf_formfill.cpp b/fpdfsdk/fpdf_formfill.cpp
index a3f3022..085fec3 100644
--- a/fpdfsdk/fpdf_formfill.cpp
+++ b/fpdfsdk/fpdf_formfill.cpp
@@ -226,6 +226,20 @@
 #endif
 }
 
+// Returns true if formfill version is correctly set. See |version| in
+// FPDF_FORMFILLINFO for details regarding correct version.
+bool CheckFormfillVersion(FPDF_FORMFILLINFO* formInfo) {
+  if (!formInfo || formInfo->version < 1 || formInfo->version > 2)
+    return false;
+
+#ifdef PDF_ENABLE_XFA
+  if (formInfo->version != 2)
+    return false;
+#endif  // PDF_ENABLE_XFA
+
+  return true;
+}
+
 }  // namespace
 
 FPDF_EXPORT int FPDF_CALLCONV
@@ -285,12 +299,7 @@
 FPDF_EXPORT FPDF_FORMHANDLE FPDF_CALLCONV
 FPDFDOC_InitFormFillEnvironment(FPDF_DOCUMENT document,
                                 FPDF_FORMFILLINFO* formInfo) {
-#ifdef PDF_ENABLE_XFA
-  constexpr int kRequiredVersion = 2;
-#else   // PDF_ENABLE_XFA
-  constexpr int kRequiredVersion = 1;
-#endif  // PDF_ENABLE_XFA
-  if (!formInfo || formInfo->version != kRequiredVersion)
+  if (!CheckFormfillVersion(formInfo))
     return nullptr;
 
   auto* pDocument = CPDFDocumentFromFPDFDocument(document);
diff --git a/fpdfsdk/fpdf_formfill_embeddertest.cpp b/fpdfsdk/fpdf_formfill_embeddertest.cpp
index 3ae272b..e186f00 100644
--- a/fpdfsdk/fpdf_formfill_embeddertest.cpp
+++ b/fpdfsdk/fpdf_formfill_embeddertest.cpp
@@ -523,12 +523,21 @@
   static constexpr float kSingleFormLastSelectedYSecondVisibleOption = 108.0;
 };
 
+class FPDFFormFillTextFormEmbedderTestVersion2
+    : public FPDFFormFillTextFormEmbedderTest {
+  void SetUp() override {
+    SetFormFillInfoVersion(2);
+    FPDFFormFillInteractiveEmbedderTest::SetUp();
+  }
+};
+
 TEST_F(FPDFFormFillEmbedderTest, FirstTest) {
   EmbedderTestMockDelegate mock;
   EXPECT_CALL(mock, Alert(_, _, _, _)).Times(0);
   EXPECT_CALL(mock, UnsupportedHandler(_)).Times(0);
   EXPECT_CALL(mock, SetTimer(_, _)).Times(0);
   EXPECT_CALL(mock, KillTimer(_)).Times(0);
+  EXPECT_CALL(mock, OnFocusChange(_, _, _)).Times(0);
   SetDelegate(&mock);
 
   EXPECT_TRUE(OpenDocument("hello_world.pdf"));
@@ -2056,6 +2065,30 @@
   CheckSelection(L"Hello World");
 }
 
+TEST_F(FPDFFormFillTextFormEmbedderTest, FocusAnnotationUpdateToEmbedder) {
+  testing::NiceMock<EmbedderTestMockDelegate> mock;
+  SetDelegate(&mock);
+  CheckFocusedFieldText(L"");
+
+#ifdef PDF_ENABLE_XFA
+  EXPECT_CALL(mock, OnFocusChange(_, _, 0)).Times(1);
+#else   // PDF_ENABLE_XFA
+  EXPECT_CALL(mock, OnFocusChange(_, _, 0)).Times(0);
+#endif  // PDF_ENABLE_XFA
+
+  ClickOnFormFieldAtPoint(RegularFormBegin());
+}
+
+TEST_F(FPDFFormFillTextFormEmbedderTestVersion2,
+       FocusAnnotationUpdateToEmbedder) {
+  testing::NiceMock<EmbedderTestMockDelegate> mock;
+  SetDelegate(&mock);
+  CheckFocusedFieldText(L"");
+
+  EXPECT_CALL(mock, OnFocusChange(_, _, 0)).Times(1);
+  ClickOnFormFieldAtPoint(RegularFormBegin());
+}
+
 TEST_F(FPDFFormFillTextFormEmbedderTest, FocusChanges) {
   static const CFX_PointF kNonFormPoint(1, 1);
   CheckFocusedFieldText(L"");
diff --git a/public/fpdf_formfill.h b/public/fpdf_formfill.h
index 95c24b4..c383212 100644
--- a/public/fpdf_formfill.h
+++ b/public/fpdf_formfill.h
@@ -374,8 +374,17 @@
 
 typedef struct _FPDF_FORMFILLINFO {
   /*
-   * Version number of the interface. Currently must be 1 (when PDFium is built
-   *  without the XFA module) or must be 2 (when built with the XFA module).
+   * Version number of the interface.
+   * Version 1 contains stable interfaces. Version 2 has additional
+   * experimental interfaces.
+   * When PDFium is built without the XFA module, version can be 1 or 2.
+   * With version 1, only stable interfaces are called. With version 2,
+   * additional experimental interfaces are also called.
+   * When PDFium is built with the XFA module, version must be 2.
+   * All the XFA related interfaces are experimental. If PDFium is built with
+   * the XFA module and version 1 then none of the XFA related interfaces
+   * would be called. When PDFium is built with XFA module then the version
+   * must be 2.
    */
   int version;
 
@@ -1069,6 +1078,30 @@
                                    FPDF_WIDESTRING wsURL,
                                    FPDF_WIDESTRING wsData,
                                    FPDF_WIDESTRING wsEncode);
+
+  /*
+   * Method: FFI_OnFocusChange
+   *     Called when the focused annotation is updated.
+   * Interface Version:
+   *     Ignored if |version| < 2.
+   * Implementation Required:
+   *     No
+   * Parameters:
+   *     param           -   Pointer to the interface structure itself.
+   *     annot           -   The focused annotation.
+   *     page_index      -   Index number of the page which contains the
+   *                         focused annotation. 0 for the first page.
+   * Return value:
+   *     None.
+   * Comments:
+   *     This callback function is useful for implementing any view based
+   *     action such as scrolling the annotation rect into view. The
+   *     embedder should not copy and store the annot as its scope is
+   *     limited to this call only.
+   */
+  void (*FFI_OnFocusChange)(struct _FPDF_FORMFILLINFO* param,
+                            FPDF_ANNOTATION annot,
+                            int page_index);
 } FPDF_FORMFILLINFO;
 
 /*
diff --git a/testing/embedder_test.cpp b/testing/embedder_test.cpp
index 9bf4a18..291b8d7 100644
--- a/testing/embedder_test.cpp
+++ b/testing/embedder_test.cpp
@@ -247,15 +247,12 @@
 
   FPDF_FORMFILLINFO* formfillinfo = static_cast<FPDF_FORMFILLINFO*>(this);
   memset(formfillinfo, 0, sizeof(FPDF_FORMFILLINFO));
-#ifdef PDF_ENABLE_XFA
-  formfillinfo->version = 2;
-#else   // PDF_ENABLE_XFA
-  formfillinfo->version = 1;
-#endif  // PDF_ENABLE_XFA
+  formfillinfo->version = form_fill_info_version_;
   formfillinfo->FFI_SetTimer = SetTimerTrampoline;
   formfillinfo->FFI_KillTimer = KillTimerTrampoline;
   formfillinfo->FFI_GetPage = GetPageTrampoline;
   formfillinfo->FFI_DoURIAction = DoURIActionTrampoline;
+  formfillinfo->FFI_OnFocusChange = OnFocusChangeTrampoline;
 
   if (javascript_option == JavaScriptOption::kEnableJavaScript)
     formfillinfo->m_pJsPlatform = platform;
@@ -609,6 +606,14 @@
 }
 
 // static
+void EmbedderTest::OnFocusChangeTrampoline(FPDF_FORMFILLINFO* info,
+                                           FPDF_ANNOTATION annot,
+                                           int page_index) {
+  EmbedderTest* test = static_cast<EmbedderTest*>(info);
+  return test->delegate_->OnFocusChange(info, annot, page_index);
+}
+
+// static
 std::string EmbedderTest::HashBitmap(FPDF_BITMAP bitmap) {
   int stride = FPDFBitmap_GetStride(bitmap);
   int usable_bytes_per_row =
diff --git a/testing/embedder_test.h b/testing/embedder_test.h
index 662333c..5d42442 100644
--- a/testing/embedder_test.h
+++ b/testing/embedder_test.h
@@ -64,6 +64,11 @@
 
     // Equivalent to FPDF_FORMFILLINFO::FFI_DoURIAction().
     virtual void DoURIAction(FPDF_BYTESTRING uri) {}
+
+    // Equivalent to FPDF_FORMFILLINFO::FFI_OnFocusChange().
+    virtual void OnFocusChange(FPDF_FORMFILLINFO* info,
+                               FPDF_ANNOTATION annot,
+                               int page_index) {}
   };
 
   EmbedderTest();
@@ -81,6 +86,10 @@
     delegate_ = delegate ? delegate : default_delegate_.get();
   }
 
+  void SetFormFillInfoVersion(int form_fill_info_version) {
+    form_fill_info_version_ = form_fill_info_version;
+  }
+
   FPDF_DOCUMENT document() const { return document_; }
   FPDF_FORMHANDLE form_handle() const { return form_handle_; }
 
@@ -248,6 +257,12 @@
   std::unique_ptr<Delegate> default_delegate_;
   Delegate* delegate_;
 
+#ifdef PDF_ENABLE_XFA
+  int form_fill_info_version_ = 2;
+#else   // PDF_ENABLE_XFA
+  int form_fill_info_version_ = 1;
+#endif  // PDF_ENABLE_XFA
+
   FPDF_DOCUMENT document_ = nullptr;
   FPDF_FORMHANDLE form_handle_ = nullptr;
   FPDF_AVAIL avail_ = nullptr;
@@ -284,6 +299,9 @@
                                      int page_index);
   static void DoURIActionTrampoline(FPDF_FORMFILLINFO* info,
                                     FPDF_BYTESTRING uri);
+  static void OnFocusChangeTrampoline(FPDF_FORMFILLINFO* info,
+                                      FPDF_ANNOTATION annot,
+                                      int page_index);
   static int WriteBlockCallback(FPDF_FILEWRITE* pFileWrite,
                                 const void* data,
                                 unsigned long size);
diff --git a/testing/embedder_test_mock_delegate.h b/testing/embedder_test_mock_delegate.h
index c3f2820..e9bd70e 100644
--- a/testing/embedder_test_mock_delegate.h
+++ b/testing/embedder_test_mock_delegate.h
@@ -8,7 +8,7 @@
 #include "testing/embedder_test.h"
 #include "testing/gmock/include/gmock/gmock.h"
 
-class EmbedderTestMockDelegate final : public EmbedderTest::Delegate {
+class EmbedderTestMockDelegate : public EmbedderTest::Delegate {
  public:
   MOCK_METHOD1(UnsupportedHandler, void(int type));
   MOCK_METHOD4(
@@ -16,6 +16,10 @@
       int(FPDF_WIDESTRING message, FPDF_WIDESTRING title, int type, int icon));
   MOCK_METHOD2(SetTimer, int(int msecs, TimerCallback fn));
   MOCK_METHOD1(KillTimer, void(int msecs));
+  MOCK_METHOD3(OnFocusChange,
+               void(FPDF_FORMFILLINFO* info,
+                    FPDF_ANNOTATION annot,
+                    int page_index));
 };
 
 #endif  // TESTING_EMBEDDER_TEST_MOCK_DELEGATE_H_