Refresh user settings on AVD edit

Cache user settings in AvdInfo.

Fixes: 336324982
Test: manual
Change-Id: I8726053326ecd5dfda1c71b701ce0fad47d30228
diff --git a/device-provisioner/src/main/com/android/sdklib/deviceprovisioner/LocalEmulatorProvisionerPlugin.kt b/device-provisioner/src/main/com/android/sdklib/deviceprovisioner/LocalEmulatorProvisionerPlugin.kt
index aaf608e..2575c88 100644
--- a/device-provisioner/src/main/com/android/sdklib/deviceprovisioner/LocalEmulatorProvisionerPlugin.kt
+++ b/device-provisioner/src/main/com/android/sdklib/deviceprovisioner/LocalEmulatorProvisionerPlugin.kt
@@ -35,7 +35,7 @@
 import com.android.sdklib.devices.Abi
 import com.android.sdklib.internal.avd.AvdInfo
 import com.android.sdklib.internal.avd.AvdInfo.AvdStatus
-import com.android.sdklib.internal.avd.AvdManager
+import com.android.sdklib.internal.avd.AvdManager.USER_SETTINGS_INI_PREFERRED_ABI
 import com.android.sdklib.internal.avd.HardwareProperties
 import com.google.wireless.android.sdk.stats.DeviceInfo
 import com.intellij.icons.AllIcons
@@ -297,6 +297,10 @@
             logger.debug { "${logName()} Processing: $message" }
             when (message) {
               is AvdInfoUpdate -> {
+                if (!activeAvdInfo.isSameMetadata(message.avdInfo)) {
+                  activeAvdInfo = activeAvdInfo.copyMetadata(message.avdInfo)
+                  properties = properties.toBuilder().apply { setAvdInfo(activeAvdInfo) }.build()
+                }
                 if (connectedDevice == null) {
                   if (activeAvdInfo != message.avdInfo) {
                     activeAvdInfo = message.avdInfo
@@ -794,8 +798,7 @@
       density = avdInfo.density
       resolution = avdInfo.resolution
       isDebuggable = !avdInfo.hasPlayStore()
-      preferredAbi =
-        avdInfo.parseUserSettingsFile(null)?.get(AvdManager.USER_SETTINGS_INI_PREFERRED_ABI)
+      preferredAbi = avdInfo.userSettings[USER_SETTINGS_INI_PREFERRED_ABI]
       avdConfigProperties.putAll(avdInfo.properties)
     }
 
diff --git a/device-provisioner/src/test/com/android/sdklib/deviceprovisioner/FakeAvdManager.kt b/device-provisioner/src/test/com/android/sdklib/deviceprovisioner/FakeAvdManager.kt
index 6b949d1..77c8f91 100644
--- a/device-provisioner/src/test/com/android/sdklib/deviceprovisioner/FakeAvdManager.kt
+++ b/device-provisioner/src/test/com/android/sdklib/deviceprovisioner/FakeAvdManager.kt
@@ -36,6 +36,12 @@
   val avds = mutableListOf<AvdInfo>()
   val runningDevices = mutableSetOf<FakeEmulatorConsole>()
   var avdIndex = 1
+  var avdEditor: (AvdInfo) -> AvdInfo = { avdInfo: AvdInfo ->
+    avdInfo.copy(
+      properties =
+        avdInfo.properties + (AvdManager.AVD_INI_DISPLAY_NAME to avdInfo.displayName + " Edited")
+    )
+  }
 
   override suspend fun rescanAvds(): List<AvdInfo> = synchronized(avds) { avds.toList() }
 
@@ -70,6 +76,7 @@
         AvdManager.AVD_INI_TAG_ID to tag.id,
         AvdManager.AVD_INI_TAG_DISPLAY to tag.display,
       ),
+      null,
       avdStatus,
     )
   }
@@ -77,12 +84,7 @@
   override suspend fun editAvd(avdInfo: AvdInfo): AvdInfo? =
     synchronized(avds) {
       avds.remove(avdInfo)
-      val newAvdInfo =
-        avdInfo.copy(
-          properties =
-            avdInfo.properties +
-              (AvdManager.AVD_INI_DISPLAY_NAME to avdInfo.displayName + " Edited")
-        )
+      val newAvdInfo = avdEditor(avdInfo)
       avds += newAvdInfo
       return newAvdInfo
     }
@@ -154,6 +156,7 @@
         avdInfo.dataFolderPath,
         avdInfo.systemImage,
         avdInfo.properties,
+        avdInfo.userSettings,
         AvdInfo.AvdStatus.OK,
       )
   }
@@ -190,7 +193,8 @@
   folderPath: Path = this.dataFolderPath,
   systemImage: ISystemImage? = this.systemImage,
   properties: Map<String, String> = this.properties,
+  userSettings: Map<String, String?>? = this.userSettings,
   status: AvdInfo.AvdStatus = this.status,
-): AvdInfo = AvdInfo(name, iniFile, folderPath, systemImage, properties, status)
+): AvdInfo = AvdInfo(name, iniFile, folderPath, systemImage, properties, userSettings, status)
 
 const val LAUNCH_EXCEPTION_MESSAGE = "launch_exception_message"
diff --git a/device-provisioner/src/test/com/android/sdklib/deviceprovisioner/LocalEmulatorProvisionerPluginTest.kt b/device-provisioner/src/test/com/android/sdklib/deviceprovisioner/LocalEmulatorProvisionerPluginTest.kt
index 51feffd..fe0f4cf 100644
--- a/device-provisioner/src/test/com/android/sdklib/deviceprovisioner/LocalEmulatorProvisionerPluginTest.kt
+++ b/device-provisioner/src/test/com/android/sdklib/deviceprovisioner/LocalEmulatorProvisionerPluginTest.kt
@@ -387,6 +387,47 @@
     job.cancel()
   }
 
+  // Some of the data for an AVD can be edited and updated while running.
+  // Check that it's fine to edit these properties while the AVD is offline or online.
+  @Test
+  fun editMetadata() = runBlockingWithTimeout {
+    val info = avdManager.makeAvdInfo(1, tag = SystemImageTags.GOOGLE_TV_TAG)
+    avdManager.createAvd(info)
+
+    val channel = Channel<DeviceState>()
+
+    yieldUntil { provisioner.devices.value.size == 1 }
+
+    val handle = provisioner.devices.value[0]
+    val job = launch { handle.stateFlow.collect { channel.send(it) } }
+
+    // Check if editing the AVD name works while offline.
+    val originalName = info.name
+    avdManager.avdEditor = { avdInfo: AvdInfo -> avdInfo.copy("New $originalName") }
+
+    handle.editAction?.edit()
+    channel.receiveUntilPassing { newState ->
+      assertThat((newState.properties as LocalEmulatorProperties).avdName)
+        .isEqualTo("New $originalName")
+    }
+
+    handle.activationAction?.activate()
+    channel.receiveUntilPassing { newState ->
+      assertThat(newState).isInstanceOf(Connected::class.java)
+    }
+
+    // Editing metadata while running shouldn't have problems.
+    avdManager.avdEditor = { avdInfo: AvdInfo -> avdInfo.copy("Final AVD name") }
+    handle.editAction?.edit()
+    channel.receiveUntilPassing { newState ->
+      assertThat((newState.properties as LocalEmulatorProperties).avdName)
+        .isEqualTo("Final AVD name")
+      assertThat(newState.error).isNull()
+    }
+
+    job.cancel()
+  }
+
   /** Verify that the device updates the expected number of times. */
   @Test
   fun updateCount() = runBlockingWithTimeout {
diff --git a/sdklib/src/main/java/com/android/sdklib/internal/avd/AvdInfo.java b/sdklib/src/main/java/com/android/sdklib/internal/avd/AvdInfo.java
index 34c190e..9bd930e 100644
--- a/sdklib/src/main/java/com/android/sdklib/internal/avd/AvdInfo.java
+++ b/sdklib/src/main/java/com/android/sdklib/internal/avd/AvdInfo.java
@@ -73,6 +73,7 @@
     @NonNull private final Path mIniFile;
     @NonNull private final Path mFolderPath;
     @NonNull private final ImmutableMap<String, String> mProperties;
+    @NonNull private final ImmutableMap<String, String> mUserSettings;
     @NonNull private final AvdStatus mStatus;
     @Nullable private final ISystemImage mSystemImage;
     private final boolean mHasPlayStore;
@@ -93,8 +94,9 @@
             @NonNull Path iniFile,
             @NonNull Path folderPath,
             @NonNull ISystemImage systemImage,
-            @Nullable Map<String, String> properties) {
-        this(name, iniFile, folderPath, systemImage, properties, AvdStatus.OK);
+            @Nullable Map<String, String> properties,
+            @Nullable Map<String, String> userSettings) {
+        this(name, iniFile, folderPath, systemImage, properties, userSettings, AvdStatus.OK);
     }
 
     /**
@@ -115,12 +117,15 @@
             @NonNull Path folderPath,
             @Nullable ISystemImage systemImage,
             @Nullable Map<String, String> properties,
+            @Nullable Map<String, String> userSettings,
             @NonNull AvdStatus status) {
         mName = name;
         mIniFile = iniFile;
         mFolderPath = folderPath;
         mSystemImage = systemImage;
         mProperties = properties == null ? ImmutableMap.of() : ImmutableMap.copyOf(properties);
+        mUserSettings =
+                userSettings == null ? ImmutableMap.of() : ImmutableMap.copyOf(userSettings);
         mStatus = status;
         String psString = mProperties.get(AvdManager.AVD_INI_PLAYSTORE_ENABLED);
         mHasPlayStore = "true".equalsIgnoreCase(psString) || "yes".equalsIgnoreCase(psString);
@@ -331,12 +336,13 @@
 
     /** Helper method that returns the User Settings Path. */
     @NonNull
-    public Path getUserSettingsPath() {
-        return mFolderPath.resolve(AvdManager.USER_SETTINGS_INI);
+    public static Path getUserSettingsPath(@NonNull Path dataFolder) {
+        return dataFolder.resolve(AvdManager.USER_SETTINGS_INI);
     }
 
-    public Map<String, String> parseUserSettingsFile(@Nullable ILogger logger) {
-        PathFileWrapper settingsPath = new PathFileWrapper(getUserSettingsPath());
+    static Map<String, String> parseUserSettingsFile(
+            @NonNull Path dataFolder, @Nullable ILogger logger) {
+        PathFileWrapper settingsPath = new PathFileWrapper(getUserSettingsPath(dataFolder));
         if (settingsPath.exists()) {
             Map<String, String> parsedSettings = AvdManager.parseIniFile(settingsPath, logger);
             if (parsedSettings != null) {
@@ -346,6 +352,11 @@
         return new HashMap<>();
     }
 
+    @NonNull
+    public Map<String, String> getUserSettings() {
+        return mUserSettings;
+    }
+
     /** Returns the Config file for this AVD. */
     @NonNull
     public Path getConfigFile() {
@@ -411,6 +422,25 @@
         return null;
     }
 
+    public boolean isSameMetadata(@Nullable Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        AvdInfo avdInfo = (AvdInfo) o;
+        return mName.equals(avdInfo.mName) && mUserSettings.equals(avdInfo.mUserSettings);
+    }
+
+    @NonNull
+    public AvdInfo copyMetadata(@NonNull AvdInfo other) {
+        return new AvdInfo(
+                other.getName(),
+                getIniFile(),
+                getDataFolderPath(),
+                getSystemImage(),
+                getProperties(),
+                other.getUserSettings(),
+                getStatus());
+    }
+
     @Override
     public boolean equals(@Nullable Object o) {
         if (this == o) return true;
@@ -421,6 +451,7 @@
                 && mIniFile.equals(avdInfo.mIniFile)
                 && mFolderPath.equals(avdInfo.mFolderPath)
                 && mProperties.equals(avdInfo.mProperties)
+                && mUserSettings.equals(avdInfo.mUserSettings)
                 && mStatus == avdInfo.mStatus
                 && Objects.equals(mSystemImage, avdInfo.mSystemImage);
     }
@@ -432,6 +463,7 @@
         hashCode = 31 * hashCode + mIniFile.hashCode();
         hashCode = 31 * hashCode + mFolderPath.hashCode();
         hashCode = 31 * hashCode + mProperties.hashCode();
+        hashCode = 31 * hashCode + mUserSettings.hashCode();
         hashCode = 31 * hashCode + mStatus.hashCode();
         hashCode = 31 * hashCode + Objects.hashCode(mSystemImage);
         hashCode = 31 * hashCode + Objects.hashCode(mHasPlayStore);
@@ -455,6 +487,9 @@
                 + "mProperties = "
                 + mProperties
                 + separator
+                + "mUserSettings = "
+                + mUserSettings
+                + separator
                 + "mStatus = "
                 + mStatus
                 + separator
diff --git a/sdklib/src/main/java/com/android/sdklib/internal/avd/AvdManager.java b/sdklib/src/main/java/com/android/sdklib/internal/avd/AvdManager.java
index f7e2a97..31ac78b 100644
--- a/sdklib/src/main/java/com/android/sdklib/internal/avd/AvdManager.java
+++ b/sdklib/src/main/java/com/android/sdklib/internal/avd/AvdManager.java
@@ -986,7 +986,8 @@
                                 iniFile,
                                 avdFolder,
                                 oldAvdInfo,
-                                configValues);
+                                configValues,
+                                userSettings);
             }
 
             if ((removePrevious || editExisting) &&
@@ -1055,6 +1056,8 @@
             // Modify the ID and display name in the new config.ini
             Path configIni = destAvdFolder.resolve(CONFIG_INI);
             Map<String, String> configVals = parseIniFile(new PathFileWrapper(configIni), mLog);
+            Map<String, String> userSettingsVals =
+                    AvdInfo.parseUserSettingsFile(destAvdFolder, mLog);
             configVals.put(AVD_INI_AVD_ID, newAvdName);
             configVals.put(AVD_INI_DISPLAY_NAME, newAvdName);
             writeIniFile(configIni, configVals, true);
@@ -1077,7 +1080,8 @@
                             newAvdName, destAvdFolder, false, systemImage.getAndroidVersion());
 
             // Create an AVD object from these files
-            return new AvdInfo(newAvdName, iniFile, destAvdFolder, systemImage, configVals);
+            return new AvdInfo(
+                    newAvdName, iniFile, destAvdFolder, systemImage, configVals, userSettingsVals);
         } catch (AndroidLocationsException | IOException e) {
             mLog.warning("Exception while duplicating an AVD: %1$s", e);
             return null;
@@ -1320,12 +1324,14 @@
                 }
 
                 // update AVD info
-                AvdInfo info = new AvdInfo(
-                        avdInfo.getName(),
-                        avdInfo.getIniFile(),
-                        paramFolderPath,
-                        avdInfo.getSystemImage(),
-                        avdInfo.getProperties());
+                AvdInfo info =
+                        new AvdInfo(
+                                avdInfo.getName(),
+                                avdInfo.getIniFile(),
+                                paramFolderPath,
+                                avdInfo.getSystemImage(),
+                                avdInfo.getProperties(),
+                                avdInfo.getUserSettings());
                 replaceAvd(avdInfo, info);
 
                 // update the ini file
@@ -1345,12 +1351,14 @@
                 }
 
                 // update AVD info
-                AvdInfo info = new AvdInfo(
-                        newName,
-                        avdInfo.getIniFile(),
-                        avdInfo.getDataFolderPath(),
-                        avdInfo.getSystemImage(),
-                        avdInfo.getProperties());
+                AvdInfo info =
+                        new AvdInfo(
+                                newName,
+                                avdInfo.getIniFile(),
+                                avdInfo.getDataFolderPath(),
+                                avdInfo.getSystemImage(),
+                                avdInfo.getProperties(),
+                                avdInfo.getUserSettings());
                 replaceAvd(avdInfo, info);
             }
 
@@ -1489,7 +1497,7 @@
                 avdName = avdName.substring(0, avdName.length() - 4);
             }
             return new AvdInfo(
-                    avdName, iniPath, iniPath, null, null, AvdStatus.ERROR_CORRUPTED_INI);
+                    avdName, iniPath, iniPath, null, null, null, AvdStatus.ERROR_CORRUPTED_INI);
         }
 
         PathFileWrapper configIniFile;
@@ -1623,7 +1631,10 @@
             }
         }
 
-        AvdInfo info = new AvdInfo(name, iniPath, avdPath, sysImage, properties, status);
+        Map<String, String> userSettings = AvdInfo.parseUserSettingsFile(avdPath, mLog);
+
+        AvdInfo info =
+                new AvdInfo(name, iniPath, avdPath, sysImage, properties, userSettings, status);
 
         if (updateHashV2) {
             try {
@@ -1853,13 +1864,16 @@
         // finally create a new AvdInfo for this unbroken avd and add it to the list.
         // instead of creating the AvdInfo object directly we reparse it, to detect other possible
         // errors
-        // FIXME: We may want to create this AvdInfo by reparsing the AVD instead. This could detect other errors.
-        AvdInfo newAvd = new AvdInfo(
-                avd.getName(),
-                avd.getIniFile(),
-                avd.getDataFolderPath(),
-                avd.getSystemImage(),
-                newProperties);
+        // FIXME: We may want to create this AvdInfo by reparsing the AVD instead. This could detect
+        // other errors.
+        AvdInfo newAvd =
+                new AvdInfo(
+                        avd.getName(),
+                        avd.getIniFile(),
+                        avd.getDataFolderPath(),
+                        avd.getSystemImage(),
+                        newProperties,
+                        avd.getUserSettings());
 
         replaceAvd(avd, newAvd);
 
@@ -2290,11 +2304,13 @@
             @NonNull Path iniFile,
             @NonNull Path avdFolder,
             @Nullable AvdInfo oldAvdInfo,
-            @Nullable Map<String, String> values)
+            @Nullable Map<String, String> values,
+            @Nullable Map<String, String> userSettings)
             throws AvdMgrException {
 
         // create the AvdInfo object, and add it to the list
-        AvdInfo theAvdInfo = new AvdInfo(avdName, iniFile, avdFolder, systemImage, values);
+        AvdInfo theAvdInfo =
+                new AvdInfo(avdName, iniFile, avdFolder, systemImage, values, userSettings);
 
         synchronized (mAllAvdList) {
             if (oldAvdInfo != null && (removePrevious || editExisting)) {