Add migration from old to new NTP Promo pref structure.

The structure of the pref that stores NTP Promos changed when support
was added for storing multiple NTP Promos. This CL adds code to handle
migrating the data from the old structure to the new structure, so that
there is no data loss when users upgrade.

BUG=620554

Review-Url: https://codereview.chromium.org/2094523002
Cr-Commit-Position: refs/heads/master@{#402356}
diff --git a/ios/chrome/browser/notification_promo.cc b/ios/chrome/browser/notification_promo.cc
index 11119b2..e5f5d34 100644
--- a/ios/chrome/browser/notification_promo.cc
+++ b/ios/chrome/browser/notification_promo.cc
@@ -25,6 +25,7 @@
 const char kPrefPromoObject[] = "ios.ntppromo";
 
 // Keys in the kPrefPromoObject dictionary; used only here.
+const char kPrefPromoID[] = "id";
 const char kPrefPromoFirstViewTime[] = "first_view_time";
 const char kPrefPromoViews[] = "views";
 const char kPrefPromoClosed[] = "closed";
@@ -135,14 +136,21 @@
 }
 
 void NotificationPromo::WritePrefs() {
+  WritePrefs(promo_id_, first_view_time_, views_, closed_);
+}
+
+void NotificationPromo::WritePrefs(int promo_id,
+                                   double first_view_time,
+                                   int views,
+                                   bool closed) {
   base::DictionaryValue* ntp_promo = new base::DictionaryValue;
-  ntp_promo->SetDouble(kPrefPromoFirstViewTime, first_view_time_);
-  ntp_promo->SetInteger(kPrefPromoViews, views_);
-  ntp_promo->SetBoolean(kPrefPromoClosed, closed_);
+  ntp_promo->SetDouble(kPrefPromoFirstViewTime, first_view_time);
+  ntp_promo->SetInteger(kPrefPromoViews, views);
+  ntp_promo->SetBoolean(kPrefPromoClosed, closed);
 
   base::DictionaryValue promo_dict;
   promo_dict.MergeDictionary(local_state_->GetDictionary(kPrefPromoObject));
-  promo_dict.Set(base::IntToString(promo_id_), ntp_promo);
+  promo_dict.Set(base::IntToString(promo_id), ntp_promo);
   local_state_->Set(kPrefPromoObject, promo_dict);
   DVLOG(1) << "WritePrefs " << promo_dict;
 }
@@ -150,6 +158,10 @@
 void NotificationPromo::InitFromPrefs(PromoType promo_type) {
   promo_type_ = promo_type;
 
+  // Check if data is stored in the old prefs structure, and migrate it before
+  // reading from prefs.
+  MigrateOldPrefs();
+
   // If |promo_id_| is not set, do nothing.
   if (promo_id_ == -1)
     return;
@@ -169,6 +181,48 @@
   ntp_promo->GetBoolean(kPrefPromoClosed, &closed_);
 }
 
+void NotificationPromo::MigrateOldPrefs() {
+  const base::DictionaryValue* promo_dict =
+      local_state_->GetDictionary(kPrefPromoObject);
+  if (!promo_dict)
+    return;
+
+  const base::ListValue* promo_list = NULL;
+  promo_dict->GetList("mobile_ntp_whats_new_promo", &promo_list);
+  if (!promo_list)
+    return;
+
+  const base::DictionaryValue* ntp_promo = NULL;
+  promo_list->GetDictionary(0, &ntp_promo);
+  if (!ntp_promo) {
+    // If the list is saved but there is no promo dictionary, clear prefs to
+    // delete the empty list.
+    NotificationPromo::MigrateUserPrefs(local_state_);
+    return;
+  }
+
+  int promo_id = -1;
+  ntp_promo->GetInteger(kPrefPromoID, &promo_id);
+  if (promo_id == -1) {
+    // If there is no promo id saved in prefs, then data is corrupt and the
+    // prefs can be discarded.
+    NotificationPromo::MigrateUserPrefs(local_state_);
+    return;
+  }
+
+  double first_view_time = 0;
+  ntp_promo->GetDouble(kPrefPromoFirstViewTime, &first_view_time);
+  int views = 0;
+  ntp_promo->GetInteger(kPrefPromoViews, &views);
+  bool closed = false;
+  ntp_promo->GetBoolean(kPrefPromoClosed, &closed);
+
+  // Clear prefs to discard the old structure before saving the data in the new
+  // structure.
+  NotificationPromo::MigrateUserPrefs(local_state_);
+  WritePrefs(promo_id, first_view_time, views, closed);
+}
+
 bool NotificationPromo::CanShow() const {
   return !closed_ && !promo_text_.empty() && !ExceedsMaxViews() &&
          !ExceedsMaxSeconds() &&
diff --git a/ios/chrome/browser/notification_promo.h b/ios/chrome/browser/notification_promo.h
index c144121..d05a6998 100644
--- a/ios/chrome/browser/notification_promo.h
+++ b/ios/chrome/browser/notification_promo.h
@@ -87,9 +87,12 @@
   // For testing.
   friend class NotificationPromoTest;
 
-  // Flush data members to prefs for storage.
+  // Flush data from instance variables to prefs for storage.
   void WritePrefs();
 
+  // Flush given parameters to prefs for storage.
+  void WritePrefs(int promo_id, double first_view_time, int views, bool closed);
+
   // Tests views_ against max_views_.
   // When max_views_ is 0, we don't cap the number of views.
   bool ExceedsMaxViews() const;
@@ -102,6 +105,12 @@
   // payload.
   bool IsPayloadParam(const std::string& param_name) const;
 
+  // Transition data saved in old prefs structure to new structure that supports
+  // storing multiple promos.
+  // TODO(crbug.com/623726) Remove this method when migration is no longer
+  // needed as most users have been upgraded to the new pref structure.
+  void MigrateOldPrefs();
+
   PrefService* local_state_;
 
   PromoType promo_type_;
diff --git a/ios/chrome/browser/notification_promo_unittest.cc b/ios/chrome/browser/notification_promo_unittest.cc
index 642836a..f2697966 100644
--- a/ios/chrome/browser/notification_promo_unittest.cc
+++ b/ios/chrome/browser/notification_promo_unittest.cc
@@ -288,6 +288,54 @@
     notification_promo_.WritePrefs();
   }
 
+  // Tests that if data is saved in the old pref structure, it is successfully
+  // migrated to the new structure that supports saving multiple promos.
+  // TODO(crbug.com/623726) Remove this method when migration is no longer
+  // needed as most users have been upgraded to the new pref structure.
+  void TestMigrationOfOldPrefs() {
+    NotificationPromo promo(&local_state_);
+    promo.InitFromVariations();
+
+    // Pick values for each variable that is saved into old prefs structure.
+    double first_view_time = 2.0;
+    int views = max_views_ + 1;
+    bool closed = true;
+
+    // Save data into old prefs structure.
+    base::DictionaryValue* ntp_promo = new base::DictionaryValue;
+    ntp_promo->SetInteger("id", promo.promo_id_);
+    ntp_promo->SetDouble("first_view_time", first_view_time);
+    ntp_promo->SetInteger("views", views);
+    ntp_promo->SetBoolean("closed", true);
+
+    base::ListValue* promo_list = new base::ListValue;
+    promo_list->Set(0, ntp_promo);
+
+    std::string promo_list_key = "mobile_ntp_whats_new_promo";
+    std::string promo_dict_key = "ios.ntppromo";
+
+    base::DictionaryValue promo_dict;
+    promo_dict.Set(promo_list_key, promo_list);
+    local_state_.Set(promo_dict_key, promo_dict);
+
+    // Initialize promo and verify that its instance variables match the data
+    // saved in the old structure.
+    promo.InitFromPrefs(promo_type_);
+    EXPECT_EQ(first_view_time, promo.first_view_time_);
+    EXPECT_EQ(views, promo.views_);
+    EXPECT_EQ(closed, promo.closed_);
+    EXPECT_FALSE(promo.CanShow());
+
+    // Verify that old pref structure was cleared.
+    const base::DictionaryValue* current_promo_dict =
+        local_state_.GetDictionary(promo_dict_key);
+    // Do not continue further if no dictionary was found at the key in prefs.
+    ASSERT_TRUE(current_promo_dict);
+    const base::ListValue* current_promo_list = NULL;
+    current_promo_dict->GetList(promo_list_key, &current_promo_list);
+    EXPECT_FALSE(current_promo_list);
+  }
+
   const NotificationPromo& promo() const { return notification_promo_; }
 
  protected:
@@ -375,4 +423,28 @@
   TestFirstViewTimeRecorded();
 }
 
+// TODO(crbug.com/623726) Remove this test case when migration is no longer
+// needed as most users have been upgraded to the new pref structure.
+TEST_F(NotificationPromoTest, NotificationPromoPrefMigrationTest) {
+  Init(
+      "{"
+      "  \"start\":\"3 Aug 1999 9:26:06 GMT\","
+      "  \"end\":\"$1\","
+      "  \"promo_text\":\"What do you think of Chrome?\","
+      "  \"payload\":"
+      "    {"
+      "      \"days_active\":7,"
+      "      \"install_age_days\":21"
+      "    },"
+      "  \"max_views\":30,"
+      "  \"max_seconds\":30,"
+      "  \"promo_id\":0"
+      "}",
+      "What do you think of Chrome?",
+      933672366,  // unix epoch for 3 Aug 1999 9:26:06 GMT.
+      0, 30, 30);
+  InitPromoFromVariations();
+  TestMigrationOfOldPrefs();
+}
+
 }  // namespace ios