diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/BuildArchive.cs b/src/Xamarin.Android.Build.Tasks/Tasks/BuildArchive.cs
index db57c4eb989..4b3389e92e2 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/BuildArchive.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/BuildArchive.cs
@@ -4,6 +4,7 @@
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using System.Runtime.InteropServices;
 using Microsoft.Android.Build.Tasks;
 using Microsoft.Build.Framework;
 using Xamarin.Tools.Zip;
@@ -32,6 +33,8 @@ public class BuildArchive : AndroidTask
 
 	public string? UncompressedFileExtensions { get; set; }
 
+	public bool UseLibZipSharp { get; set; }
+
 	public string? ZipFlushFilesLimit { get; set; }
 
 	public string? ZipFlushSizeLimit { get; set; }
@@ -43,8 +46,10 @@ public class BuildArchive : AndroidTask
 
 	public override bool RunTask ()
 	{
+		var is_aab = string.Compare (AndroidPackageFormat, "aab", true) == 0;
+
 		// Nothing needs to be compressed with app bundles. BundleConfig.json specifies the final compression mode.
-		if (string.Compare (AndroidPackageFormat, "aab", true) == 0)
+		if (is_aab)
 			uncompressedMethod = CompressionMethod.Default;
 
 		var refresh = true;
@@ -57,7 +62,7 @@ public override bool RunTask ()
 			refresh = false;
 		}
 
-		using var apk = new ZipArchiveEx (ApkOutputPath, FileMode.Open);
+		using var apk = ZipArchiveDotNet.Create (Log, ApkOutputPath, System.IO.Compression.ZipArchiveMode.Update, ShouldFallbackToLibZipSharp ());
 
 		// Set up AutoFlush
 		if (int.TryParse (ZipFlushFilesLimit, out int flushFilesLimit)) {
@@ -73,10 +78,9 @@ public override bool RunTask ()
 		var existingEntries = new List<string> ();
 
 		if (refresh) {
-			for (var i = 0; i < apk.Archive.EntryCount; i++) {
-				var entry = apk.Archive.ReadEntry ((ulong) i);
-				Log.LogDebugMessage ($"Registering item {entry.FullName}");
-				existingEntries.Add (entry.FullName);
+			foreach (var entry in apk.GetAllEntryNames ()) {
+				Log.LogDebugMessage ($"Registering item {entry}");
+				existingEntries.Add (entry);
 			}
 		}
 
@@ -98,6 +102,11 @@ public override bool RunTask ()
 						Log.LogDebugMessage ($"Fixing up malformed entry `{entry.FullName}` -> `{entryName}`");
 					}
 
+					if (entryName == "AndroidManifest.xml" && is_aab) {
+						Log.LogDebugMessage ("Renaming AndroidManifest.xml to manifest/AndroidManifest.xml");
+						entryName = "manifest/AndroidManifest.xml";
+					}
+
 					Log.LogDebugMessage ($"Deregistering item {entryName}");
 					existingEntries.Remove (entryName);
 
@@ -106,24 +115,28 @@ public override bool RunTask ()
 						continue;
 					}
 
-					if (apk.Archive.ContainsEntry (entryName)) {
-						ZipEntry e = apk.Archive.ReadEntry (entryName);
+					if (apk.ContainsEntry (entryName)) {
+						var e = apk.GetEntry (entryName);
 						// check the CRC values as the ModifiedDate is always 01/01/1980 in the aapt generated file.
 						if (entry.CRC == e.CRC && entry.CompressedSize == e.CompressedSize) {
 							Log.LogDebugMessage ($"Skipping {entryName} from {ApkInputPath} as its up to date.");
 							continue;
 						}
+
+						// Delete the existing entry so we can replace it with the new one.
+						apk.DeleteEntry (entryName);
 					}
 
 					var ms = new MemoryStream ();
 					entry.Extract (ms);
+					ms.Position = 0;
 					Log.LogDebugMessage ($"Refreshing {entryName} from {ApkInputPath}");
-					apk.Archive.AddStream (ms, entryName, compressionMethod: entry.CompressionMethod);
+					apk.AddEntry (ms, entryName, entry.CompressionMethod.ToCompressionLevel ());
 				}
 			}
 		}
 
-		apk.FixupWindowsPathSeparators ((a, b) => Log.LogDebugMessage ($"Fixing up malformed entry `{a}` -> `{b}`"));
+		apk.FixupWindowsPathSeparators (Log);
 
 		// Add the files to the apk
 		foreach (var file in FilesToAddToArchive) {
@@ -135,6 +148,8 @@ public override bool RunTask ()
 				return !Log.HasLoggedErrors;
 			}
 
+			apk_path = apk_path.Replace ('\\', '/');
+
 			// This is a temporary hack for adding files directly from inside a .jar/.aar
 			// into the APK. Eventually another task should be writing them to disk and just
 			// passing us a filename like everything else.
@@ -145,7 +160,7 @@ public override bool RunTask ()
 				// eg: "obj/myjar.jar#myfile.txt"
 				var jar_file_path = disk_path.Substring (0, disk_path.Length - (jar_entry_name.Length + 1));
 
-				if (apk.Archive.Any (ze => ze.FullName == apk_path)) {
+				if (apk.ContainsEntry (apk_path)) {
 					Log.LogDebugMessage ("Failed to add jar entry {0} from {1}: the same file already exists in the apk", jar_entry_name, Path.GetFileName (jar_file_path));
 					continue;
 				}
@@ -165,7 +180,7 @@ public override bool RunTask ()
 					}
 
 					Log.LogDebugMessage ($"Adding {jar_entry_name} from {jar_file_path} as the archive file is out of date.");
-					apk.AddEntryAndFlush (data, apk_path);
+					apk.AddEntry (data, apk_path);
 				}
 
 				continue;
@@ -180,30 +195,71 @@ public override bool RunTask ()
 			if (string.Compare (Path.GetFileName (entry), "AndroidManifest.xml", StringComparison.OrdinalIgnoreCase) == 0)
 				continue;
 
-			Log.LogDebugMessage ($"Removing {entry} as it is not longer required.");
-			apk.Archive.DeleteEntry (entry);
+			Log.LogDebugMessage ($"Removing {entry} as it is no longer required.");
+			apk.DeleteEntry (entry);
 		}
 
-		if (string.Compare (AndroidPackageFormat, "aab", true) == 0)
+		if (is_aab)
 			FixupArchive (apk);
 
 		return !Log.HasLoggedErrors;
 	}
 
-	bool AddFileToArchiveIfNewer (ZipArchiveEx apk, string file, string inArchivePath, ITaskItem item, List<string> existingEntries)
+	// .NET Framework has a bug where it doesn't handle uncompressed files correctly.
+	// It writes them as "compressed" (DEFLATE) but with a compression level of 0. This causes
+	// issues with Android, which expect uncompressed files to be stored correctly.
+	// We can work around this by using LibZipSharp, which doesn't have this bug.
+	// This is only necessary if we're on .NET Framework (MSBuild in VSWin) and we have uncompressed files.
+	bool ShouldFallbackToLibZipSharp ()
 	{
-		var compressionMethod = GetCompressionMethod (item);
-		existingEntries.Remove (inArchivePath.Replace (Path.DirectorySeparatorChar, '/'));
+		// Explicitly requested via MSBuild property.
+		if (UseLibZipSharp) {
+			Log.LogDebugMessage ("Falling back to LibZipSharp because '$(_AndroidUseLibZipSharp)' is 'true'.");
+			return true;
+		}
 
-		if (apk.SkipExistingFile (file, inArchivePath, compressionMethod)) {
-			Log.LogDebugMessage ($"Skipping {file} as the archive file is up to date.");
+		// .NET 6+ handles uncompressed files correctly, so we don't need to fallback.
+		if (RuntimeInformation.FrameworkDescription == ".NET") {
+			Log.LogDebugMessage ("Using System.IO.Compression because we're running on .NET 6+.");
 			return false;
 		}
 
-		Log.LogDebugMessage ($"Adding {file} as the archive file is out of date.");
-		apk.AddFileAndFlush (file, inArchivePath, compressionMethod);
+		// Nothing is going to get written uncompressed, so we don't need to fallback.
+		if (uncompressedMethod != CompressionMethod.Store) {
+			Log.LogDebugMessage ("Using System.IO.Compression because uncompressedMethod isn't 'Store'.");
+			return false;
+		}
 
-		return true;
+		// No uncompressed file extensions were specified, so we don't need to fallback.
+		if (UncompressedFileExtensionsSet.Count == 0) {
+			Log.LogDebugMessage ("Using System.IO.Compression because no uncompressed file extensions were specified.");
+			return false;
+		}
+
+		// See if any of the files to be added need to be uncompressed.
+		foreach (var file in FilesToAddToArchive) {
+			var file_path = file.ItemSpec;
+
+			// Handle files from inside a .jar/.aar
+			if (file.GetMetadataOrDefault ("JavaArchiveEntry", (string?)null) is string jar_entry_name)
+				file_path = jar_entry_name;
+
+			if (UncompressedFileExtensionsSet.Contains (Path.GetExtension (file_path))) {
+				Log.LogDebugMessage ($"Falling back to LibZipSharp because '{file_path}' needs to be stored uncompressed.");
+				return true;
+			}
+		}
+
+		Log.LogDebugMessage ("Using System.IO.Compression because no files need to be stored uncompressed.");
+		return false;
+	}
+
+	bool AddFileToArchiveIfNewer (IZipArchive apk, string file, string inArchivePath, ITaskItem item, List<string> existingEntries)
+	{
+		var compressionMethod = GetCompressionLevel (item);
+		existingEntries.Remove (inArchivePath.Replace (Path.DirectorySeparatorChar, '/'));
+
+		return apk.AddFileIfChanged (Log, file, inArchivePath, compressionMethod);
 	}
 
 	/// <summary>
@@ -211,32 +267,24 @@ bool AddFileToArchiveIfNewer (ZipArchiveEx apk, string file, string inArchivePat
 	/// I see no way to change this behavior, so we can move the file for now:
 	/// https://github.com/aosp-mirror/platform_frameworks_base/blob/e80b45506501815061b079dcb10bf87443bd385d/tools/aapt2/LoadedApk.h#L34
 	/// </summary>
-	void FixupArchive (ZipArchiveEx zip)
+	void FixupArchive (IZipArchive zip)
 	{
-		if (!zip.Archive.ContainsEntry ("AndroidManifest.xml")) {
+		if (!zip.ContainsEntry ("AndroidManifest.xml")) {
 			Log.LogDebugMessage ($"No AndroidManifest.xml. Skipping Fixup");
 			return;
 		}
 
-		var entry = zip.Archive.ReadEntry ("AndroidManifest.xml");
 		Log.LogDebugMessage ($"Fixing up AndroidManifest.xml to be manifest/AndroidManifest.xml.");
 
-		if (zip.Archive.ContainsEntry ("manifest/AndroidManifest.xml"))
-			zip.Archive.DeleteEntry (zip.Archive.ReadEntry ("manifest/AndroidManifest.xml"));
+		if (zip.ContainsEntry ("manifest/AndroidManifest.xml"))
+			zip.DeleteEntry ("manifest/AndroidManifest.xml");
 
-		entry.Rename ("manifest/AndroidManifest.xml");
+		zip.MoveEntry ("AndroidManifest.xml", "manifest/AndroidManifest.xml");
 	}
 
-	CompressionMethod GetCompressionMethod (ITaskItem item)
+	System.IO.Compression.CompressionLevel GetCompressionLevel (ITaskItem item)
 	{
-		var compression = item.GetMetadataOrDefault ("Compression", "");
-
-		if (compression.HasValue ()) {
-			if (Enum.TryParse (compression, out CompressionMethod result))
-				return result;
-		}
-
-		return UncompressedFileExtensionsSet.Contains (Path.GetExtension (item.ItemSpec)) ? uncompressedMethod : CompressionMethod.Default;
+		return (UncompressedFileExtensionsSet.Contains (Path.GetExtension (item.ItemSpec)) ? uncompressedMethod : CompressionMethod.Default).ToCompressionLevel ();
 	}
 
 	HashSet<string> ParseUncompressedFileExtensions ()
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/UtilityExtensions.cs b/src/Xamarin.Android.Build.Tasks/Utilities/UtilityExtensions.cs
new file mode 100644
index 00000000000..a566a6b754f
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/UtilityExtensions.cs
@@ -0,0 +1,46 @@
+using System;
+using System.IO;
+using System.IO.Compression;
+using Xamarin.Tools.Zip;
+
+namespace Xamarin.Android.Tasks;
+
+static class UtilityExtensions
+{
+	public static System.IO.Compression.CompressionLevel ToCompressionLevel (this CompressionMethod method)
+	{
+		switch (method) {
+			case CompressionMethod.Store:
+				return System.IO.Compression.CompressionLevel.NoCompression;
+			case CompressionMethod.Default:
+			case CompressionMethod.Deflate:
+				return System.IO.Compression.CompressionLevel.Optimal;
+			default:
+				throw new ArgumentOutOfRangeException (nameof (method), method, null);
+		}
+	}
+
+	public static CompressionMethod ToCompressionMethod (this System.IO.Compression.CompressionLevel level)
+	{
+		switch (level) {
+			case System.IO.Compression.CompressionLevel.NoCompression:
+				return CompressionMethod.Store;
+			case System.IO.Compression.CompressionLevel.Optimal:
+				return CompressionMethod.Deflate;
+			default:
+				throw new ArgumentOutOfRangeException (nameof (level), level, null);
+		}
+	}
+
+	public static FileMode ToFileMode (this ZipArchiveMode mode)
+	{
+		switch (mode) {
+			case ZipArchiveMode.Create:
+				return FileMode.Create;
+			case ZipArchiveMode.Update:
+				return FileMode.Open;
+			default:
+				throw new ArgumentOutOfRangeException (nameof (mode), mode, null);
+		}
+	}
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveDotNet.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveDotNet.cs
new file mode 100644
index 00000000000..12a84f83ab4
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveDotNet.cs
@@ -0,0 +1,263 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Reflection;
+using Microsoft.Android.Build.Tasks;
+using Microsoft.Build.Utilities;
+
+namespace Xamarin.Android.Tasks;
+
+interface IZipArchive : IDisposable
+{
+	int ZipFlushFilesLimit { get; set; }
+	int ZipFlushSizeLimit { get; set; }
+
+	void AddEntry (byte [] data, string apkPath);
+	void AddEntry (Stream stream, string apkPath, CompressionLevel compression);
+	bool AddFileIfChanged (TaskLoggingHelper log, string filename, string archiveFileName, CompressionLevel compression);
+	bool ContainsEntry (string entryPath);
+	void DeleteEntry (string entry);
+	void FixupWindowsPathSeparators (TaskLoggingHelper log);
+	IEnumerable<string> GetAllEntryNames ();
+	IZipArchiveEntry GetEntry (string entryName);
+	void MoveEntry (string oldEntry, string newEntry);
+}
+
+interface IZipArchiveEntry
+{
+	uint CRC { get; }
+	ulong CompressedSize { get; }
+}
+
+class ZipArchiveDotNet : IZipArchive
+{
+	const int DEFAULT_FLUSH_SIZE_LIMIT = 100 * 1024 * 1024;
+	const int DEFAULT_FLUSH_FILES_LIMIT = 512;
+
+	// ZipFile seems to be performant enough that we have not currently implemented a flush mechanism.
+	public int ZipFlushSizeLimit { get; set; } = DEFAULT_FLUSH_SIZE_LIMIT;
+	public int ZipFlushFilesLimit { get; set; } = DEFAULT_FLUSH_FILES_LIMIT;
+
+	public ZipArchive Archive { get; }
+
+	static readonly FieldInfo? crc_field;
+	static readonly FieldInfo? comp_field;
+
+	// This is private to ensure the only way to create an instance is through the Create method.
+	ZipArchiveDotNet (string archive, ZipArchiveMode mode)
+	{
+		Archive = ZipFile.Open (archive, mode);
+	}
+
+	static ZipArchiveDotNet ()
+	{
+		// netstandard2.0 does not provide a way to access a ZipArchiveEntry's CRC or compresion level.
+		// We need to use reflection to access the private fields.
+
+		// These private fields exist on both .NET Framework 4.7.2 and .NET 9.0. If they are not found,
+		// we will return a ZipArchiveEx which uses LibZipSharp instead.
+		crc_field = typeof (ZipArchiveEntry).GetField ("_crc32", BindingFlags.NonPublic | BindingFlags.Instance);
+		comp_field = typeof (ZipArchiveEntry).GetField ("_storedCompressionMethod", BindingFlags.NonPublic | BindingFlags.Instance);
+	}
+
+	public static IZipArchive Create (TaskLoggingHelper log, string archive, ZipArchiveMode mode, bool fallback)
+	{
+		if (fallback) {
+			log.LogDebugMessage ($"ZipArchiveDotNet: Falling back to LibZipSharp as requested.");
+			return new ZipArchiveEx (archive, mode.ToFileMode ());
+		}
+
+		if (crc_field is null) {
+			log.LogDebugMessage ($"ZipArchiveDotNet: Could not find private CRC field, falling back to LibZipSharp.");
+			return new ZipArchiveEx (archive, mode.ToFileMode ());
+		}
+
+		if (comp_field is null) {
+			log.LogDebugMessage ($"ZipArchiveDotNet: Could not find private CompressionMethod field, falling back to LibZipSharp.");
+			return new ZipArchiveEx (archive, mode.ToFileMode ());
+		}
+
+		log.LogDebugMessage ($"ZipArchiveDotNet: Using ZipArchiveDotNet.");
+		return new ZipArchiveDotNet (archive, mode);
+	}
+
+	public void AddEntry (byte [] data, string apkPath)
+	{
+		var entry = Archive.CreateEntry (apkPath);
+
+		using (var stream = entry.Open ())
+			stream.Write (data, 0, data.Length);
+	}
+
+	public void AddEntry (Stream stream, string apkPath, CompressionLevel compression)
+	{
+		var entry = Archive.CreateEntry (apkPath, compression);
+
+		using (var entry_stream = entry.Open ())
+			stream.CopyTo (entry_stream);
+	}
+
+	public bool AddFileIfChanged (TaskLoggingHelper log, string filename, string archiveFileName, CompressionLevel compression)
+	{
+		if (!FileNeedsUpdating (log, filename, archiveFileName, compression))
+			return false;
+
+		DeleteEntry (archiveFileName);
+		Archive.CreateEntryFromFile (filename, archiveFileName, compression);
+
+		return true;
+	}
+
+	public bool ContainsEntry (string entryName)
+	{
+		return Archive.GetEntry (entryName) is not null;
+	}
+
+	public void DeleteEntry (string entryName)
+	{
+		var entry = Archive.GetEntry (entryName);
+
+		entry?.Delete ();
+	}
+
+	/// <summary>
+	/// HACK: aapt2 is creating zip entries on Windows such as `assets\subfolder/asset2.txt`
+	/// </summary>
+	public void FixupWindowsPathSeparators (TaskLoggingHelper log)
+	{
+		var malformed_entries = Archive.Entries.Where (entry => entry.FullName.Contains ('\\')).ToList ();
+
+		foreach (var entry in malformed_entries) {
+			var name = entry.FullName.Replace ('\\', '/');
+			if (name != entry.FullName) {
+				log.LogDebugMessage ($"Fixing up malformed entry `{entry.FullName}` -> `{name}`");
+				MoveEntry (entry.FullName, name);
+			}
+		}
+	}
+
+	public IEnumerable<string> GetAllEntryNames ()
+	{
+		return Archive.Entries.Select (entry => entry.FullName);
+	}
+
+	public IZipArchiveEntry GetEntry (string entryName)
+	{
+		var entry = Archive.GetEntry (entryName);
+
+		if (entry is null)
+			throw new ArgumentOutOfRangeException (nameof (entryName));
+
+		return new ZipArchiveEntryDotNet (entry, GetCrc32 (entry));
+	}
+
+	public void Dispose ()
+	{
+		Archive.Dispose ();
+	}
+
+	public void MoveEntry (string oldPath, string newPath)
+	{
+		var old_entry = Archive.GetEntry (oldPath);
+
+		if (old_entry is null)
+			return;
+
+		var new_entry = Archive.CreateEntry (newPath);
+
+		using (var oldStream = old_entry.Open ())
+		using (var newStream = new_entry.Open ())
+			oldStream.CopyTo (newStream);
+
+		old_entry.Delete ();
+	}
+
+	bool FileNeedsUpdating (TaskLoggingHelper log, string filename, string archiveFileName, CompressionLevel compression)
+	{
+		var entry = Archive.GetEntry (archiveFileName);
+
+		if (entry is null) {
+			log.LogDebugMessage ($"Adding {filename} as it doesn't already exist.");
+			return true;
+		}
+
+		var stored_compression = GetCompressionLevel (entry);
+
+		if (stored_compression != compression) {
+			log.LogDebugMessage ($"Updating {filename} as the compression level changed: existing - '{stored_compression}', requested - '{compression}'.");
+			return true;
+		}
+
+		var last_write = File.GetLastWriteTimeUtc (filename);
+		var file_write_dos_time = DateTimeToDosTime (last_write);
+		var zip_write_dos_time = DateTimeToDosTime (entry.LastWriteTime.UtcDateTime);
+
+		if (DateTimeToDosTime (entry.LastWriteTime.UtcDateTime) < DateTimeToDosTime (last_write)) {
+			log.LogDebugMessage ($"Updating {filename} as the file write time is newer: file in zip - '{zip_write_dos_time}', file on disk - '{file_write_dos_time}'.");
+			return true;
+		}
+
+		log.LogDebugMessage ($"Skipping {filename} as the archive file is up to date.");
+		return false;
+	}
+
+	static uint GetCrc32 (ZipArchiveEntry entry)
+	{
+		if (crc_field is null)
+			throw new NotSupportedException ("This method is not supported on this platform.");
+
+		return (uint) crc_field.GetValue (entry);
+	}
+
+	static CompressionLevel GetCompressionLevel (ZipArchiveEntry entry)
+	{
+		if (comp_field is null)
+			throw new NotSupportedException ("This method is not supported on this platform.");
+
+		var level = comp_field.GetValue (entry).ToString ();
+
+		switch (level) {
+			case "Stored":
+				return CompressionLevel.NoCompression;
+			case "Deflate":
+				return CompressionLevel.Optimal;
+			default:
+				throw new NotSupportedException ($"Unsupported compression level: {level}");
+		}
+	}
+
+	// System.IO.Compression.ZipArchive apparently only provides a 2 second granularity for the LastWriteTime.
+	// This should be fine, it would be nearly impossible for someone to complete a build, make a change,
+	// and rebuild in under 2 seconds.
+	// This is a port of the DateTimeToDosTime method from System.IO.Compression.ZipHelper
+	// https://github.com/dotnet/runtime/blob/373f048bae3c46810bc030ed7c1ee0568ee5ecc0/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.cs#L88
+	const int ValidZipDate_YearMin = 1980;
+
+	static uint DateTimeToDosTime (DateTime dateTime)
+	{
+		int ret = ((dateTime.Year - ValidZipDate_YearMin) & 0x7F);
+		ret = (ret << 4) + dateTime.Month;
+		ret = (ret << 5) + dateTime.Day;
+		ret = (ret << 5) + dateTime.Hour;
+		ret = (ret << 6) + dateTime.Minute;
+		ret = (ret << 5) + (dateTime.Second / 2); // only 5 bits for second, so we only have a granularity of 2 sec.
+		return (uint) ret;
+	}
+
+	class ZipArchiveEntryDotNet : IZipArchiveEntry
+	{
+		readonly ZipArchiveEntry entry;
+		readonly uint crc;
+
+		public uint CRC => crc;
+		public ulong CompressedSize => (ulong) entry.CompressedLength;
+
+		public ZipArchiveEntryDotNet (ZipArchiveEntry entry, uint crc)
+		{
+			this.entry = entry;
+			this.crc = crc;
+		}
+	}
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveEx.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveEx.cs
index 54e510218ad..21ef964fe12 100644
--- a/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveEx.cs
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveEx.cs
@@ -1,11 +1,14 @@
-using System;
+using System;
+using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using Microsoft.Android.Build.Tasks;
+using Microsoft.Build.Utilities;
 using Xamarin.Tools.Zip;
 
 namespace Xamarin.Android.Tasks
 {
-	public class ZipArchiveEx : IDisposable
+	public class ZipArchiveEx : IZipArchive
 	{
 
 		const int DEFAULT_FLUSH_SIZE_LIMIT = 100 * 1024 * 1024;
@@ -235,5 +238,71 @@ protected virtual void Dispose(bool disposing) {
 				}
 			}
 		}
+
+		public void AddEntry (byte [] data, string apkPath)
+			=> AddEntryAndFlush (data, apkPath);
+
+		public void AddEntry (Stream stream, string apkPath, System.IO.Compression.CompressionLevel compression)
+			=> AddEntryAndFlush (apkPath, stream, compression.ToCompressionMethod ());
+
+		public bool AddFileIfChanged (TaskLoggingHelper log, string filename, string archiveFileName, System.IO.Compression.CompressionLevel compression)
+		{
+			var compressionMethod = compression.ToCompressionMethod ();
+
+			if (!SkipExistingFile (filename, archiveFileName, compressionMethod)) {
+				AddFileAndFlush (filename, archiveFileName, compressionMethod);
+				log.LogDebugMessage ($"Adding {filename} as the archive file is out of date.");
+				return true;
+			}
+
+			log.LogDebugMessage ($"Skipping {filename} as the archive file is up to date.");
+
+			return false;
+		}
+
+		public bool ContainsEntry (string entryPath)
+			=> zip.ContainsEntry (entryPath);
+
+		public void DeleteEntry (string entry)
+			=> zip.DeleteEntry (entry);
+
+		public void FixupWindowsPathSeparators (TaskLoggingHelper log)
+			=> FixupWindowsPathSeparators ((a, b) => log.LogDebugMessage ($"Fixing up malformed entry `{a}` -> `{b}`"));
+
+		public IEnumerable<string> GetAllEntryNames ()
+		{
+			for (var i = 0; i < Archive.EntryCount; i++) {
+				var entry = Archive.ReadEntry ((ulong) i);
+				yield return entry.FullName;
+			}
+		}
+
+		IZipArchiveEntry IZipArchive.GetEntry (string entryName)
+		{
+			return new ZipArchiveEntryEx (zip.ReadEntry (entryName));
+		}
+
+		void IZipArchive.MoveEntry (string oldEntry, string newEntry)
+		{
+			if (Archive.ContainsEntry (newEntry))
+				Archive.DeleteEntry (Archive.ReadEntry (newEntry));
+
+			var entry = zip.ReadEntry (oldEntry);
+			entry.Rename (newEntry);
+		}
+	}
+
+	class ZipArchiveEntryEx : IZipArchiveEntry
+	{
+		readonly ZipEntry entry;
+
+		public ZipArchiveEntryEx (ZipEntry entry)
+		{
+			this.entry = entry;
+		}
+
+		public uint CRC => entry.CRC;
+
+		public ulong CompressedSize => entry.CompressedSize;
 	}
 }
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveFileListBuilder.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveFileListBuilder.cs
deleted file mode 100644
index cdaf609ffc3..00000000000
--- a/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveFileListBuilder.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-#nullable enable
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using Microsoft.Build.Framework;
-using Microsoft.Build.Utilities;
-using Xamarin.Tools.Zip;
-
-namespace Xamarin.Android.Tasks;
-
-// This temporary class has a nonsensical API to allow it to be a drop-in replacement
-// for ZipArchiveEx. This allows us to refactor with smaller diffs that can be
-// reviewed easier. This class should not exist in this form in the final state.
-public class ZipArchiveFileListBuilder : IDisposable
-{
-	public List<ITaskItem> ApkFiles { get; } = [];
-
-	public ZipArchiveFileListBuilder (string archive, FileMode filemode)
-	{
-	}
-
-	public void Dispose ()
-	{
-		// No-op
-	}
-
-	public void Flush ()
-	{
-		// No-op
-	}
-
-	public void AddFileAndFlush (string filename, string archiveFileName, CompressionMethod compressionMethod)
-	{
-		var item = new TaskItem (filename);
-
-		item.SetMetadata ("ArchivePath", archiveFileName);
-		item.SetMetadata ("Compression", compressionMethod.ToString ());
-
-		ApkFiles.Add (item);
-	}
-
-	public void AddJavaEntryAndFlush (string javaFilename, string javaEntryName, string archiveFileName)
-	{
-		// An item's ItemSpec must be unique so use both the jar file name and the entry name
-		var item = new TaskItem ($"{javaFilename}#{javaEntryName}");
-		item.SetMetadata ("ArchivePath", archiveFileName);
-		item.SetMetadata ("JavaArchiveEntry", javaEntryName);
-
-		ApkFiles.Add (item);
-	}
-
-	public void FixupWindowsPathSeparators (Action<string, string> onRename)
-	{
-		// No-op
-	}
-
-	public bool SkipExistingFile (string file, string fileInArchive, CompressionMethod compressionMethod)
-	{
-		return false;
-	}
-
-	public bool SkipExistingEntry (ZipEntry sourceEntry, string fileInArchive)
-	{
-		return false;
-	}
-}
diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets
index f2eb3021660..84bc8a00c9a 100644
--- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets
+++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets
@@ -2182,6 +2182,7 @@ because xbuild doesn't support framework reference assemblies.
       ApkOutputPath="$(_ApkOutputPath)"
       FilesToAddToArchive="@(FilesToAddToArchive)"
       UncompressedFileExtensions="$(AndroidStoreUncompressedFileExtensions)"
+      UseLibZipSharp="$(_AndroidUseLibZipSharp)"
       ZipFlushFilesLimit="$(_ZipFlushFilesLimit)"
       ZipFlushSizeLimit="$(_ZipFlushSizeLimit)" />
 
@@ -2313,6 +2314,7 @@ because xbuild doesn't support framework reference assemblies.
       ApkOutputPath="$(_ApkOutputPath)"
       FilesToAddToArchive="@(FilesToAddToArchive)"
       UncompressedFileExtensions="$(AndroidStoreUncompressedFileExtensions)"
+      UseLibZipSharp="$(_AndroidUseLibZipSharp)"
       ZipFlushFilesLimit="$(_ZipFlushFilesLimit)"
       ZipFlushSizeLimit="$(_ZipFlushSizeLimit)" />