Apk expansion files on Android

Is anyone using Expansion Files for android?
It seems like the StorageManager class should be able to mount the expansion files if they are stored into an Obb file, making them appear as a folder.
Then you could get the files by pointing a new contentmanager to that location…
But can the contentmanager be targeted to an absolute location rather than a relative one?

I’ll post my findings as i coudn’t find any reference here…

This is kinda messy but here is how i implemented a ContentManager derived that supports expansion files. Code is a bit messy but i will update it depending on the interest/suggestions.

using Android.Content.PM;
using Android.Content;
using Android.App;
using Android;

using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Reflection;
using System.Text;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Path = System.IO.Path;
using System.Diagnostics;
using System.IO.Compression;
using ICSharpCode.SharpZipLib.Zip;

#if !WINRT
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Media;
#endif

namespace Microsoft.Xna.Framework.Content
{
public class Android_ContentManager:ContentManager
    {
        // Keep this static so we only call Game.Activity.Assets.List() once
        // No need to call it for each file if the list will never change.
        // We do need one file list per folder though.
        static ZipFile zif;
        static ApplicationInfo ainfo;

        static PackageInfo pinfo;

        static string obbPath;

        static Dictionary<string,Dictionary<string,int>> entries;

    public Android_ContentManager (IServiceProvider serviceProvider) : base ( serviceProvider)
        {
            init ();
    }
    public Android_ContentManager (IServiceProvider serviceProvider, string rootDirectory) : base(serviceProvider,rootDirectory)
    {
        init ();
    }

        public static int expansionPackVersion;

        void init ()
        {
            if (entries == null) {
            Activity activity = Game.Activity;
            ainfo = activity.ApplicationInfo;
            pinfo = activity.PackageManager.GetPackageInfo (ainfo.PackageName, PackageInfoFlags.MetaData);
            #if DEBUG
            obbPath = Path.Combine (Android.OS.Environment.ExternalStorageDirectory.AbsolutePath, "Android", "obb", ainfo.PackageName+".debug", String.Format ("main.{0}.{1}.obb", expansionPackVersion, ainfo.PackageName));
            #else
            obbPath = Path.Combine (Android.OS.Environment.ExternalStorageDirectory.AbsolutePath, "Android", "obb", ainfo.PackageName, String.Format ("main.{0}.{1}.obb", expansionPackVersion, ainfo.PackageName));
            #endif
                try {
                    zif = new ZipFile (obbPath); 
                    entries = new Dictionary<string,Dictionary<string,int>> ();
                    Dictionary<string,int> name_to_index = null;
                    for (int i = 0; i < zif.Count; i++) {
                        if (Path.GetExtension (zif [i].Name).Length > 0) {
                            var dirname = Path.GetDirectoryName (zif [i].Name);
                            name_to_index = null;
                            if (!entries.TryGetValue (dirname, out name_to_index)) {
                                name_to_index = new Dictionary<string, int> ();
                                entries [dirname] = name_to_index;
                            }
                            name_to_index [Path.GetFileName (zif [i].Name)] = i;
                        }
                    }
                } catch (Exception e) {
                    System.Diagnostics.Debug.WriteLine ("++    Zip expansion file could not be opened    ++  {0}", e.Message);
                    zif = null;
                }
            }
        }

        object ContentManagerLock=(object)1;

        public override Stream OpenStream(string assetName)
        {
            if (zif == null)
                return base.OpenStream (assetName);
            Stream stream;
            try {
                string assetPath = Path.Combine (RootDirectory, assetName) + ".xnb";

                Activity activity = Game.Activity;                
                
                string filePath = assetPath;
                                
                lock (ContentManagerLock) {
                    ZipEntry ze = zif.GetEntry (filePath);
                    stream = zif.GetInputStream (ze);
                            
                    MemoryStream mstream = new MemoryStream ((int)ze.Size);
                            
                    stream.CopyTo (mstream);
                            
                    mstream.Seek (0, SeekOrigin.Begin);  
                            
                    stream.Close ();
                    stream = mstream;                
                }
            } catch (FileNotFoundException fileNotFound) {
                return base.OpenStream (assetName); // if file or folder are not found we can try with the standard read method
            }
#if !WINRT
            catch (DirectoryNotFoundException directoryNotFound) {
                return base.OpenStream (assetName); // if file or folder are not found we can try with the standard read method
            }
#endif
            catch (Exception exception) {
                return base.OpenStream (assetName); // if file or folder are not found we can try with the standard read method
            }
            return stream;
        }
        protected override void Dispose (bool disposing)
        {
            base.Dispose (disposing);
        }


        static string[] textureExtensions = new string[] { ".jpg", ".bmp", ".jpeg", ".png", ".gif" };
        static string[] songExtensions = new string[] { ".mp3", ".ogg", ".mid" };
        static string[] soundEffectExtensions = new string[] { ".wav", ".mp3", ".ogg", ".mid" };
        protected override string Normalize<T>(string assetName)
        {
            string result = null;
            if (zif == null)
                return base.Normalize<T>(assetName);

            if (typeof(T) == typeof(Texture2D) || typeof(T) == typeof(Texture))
            {
                result =  Normalize(assetName,textureExtensions);
            }
            else if ((typeof(T) == typeof(Song)))
            {
                result =  Normalize(assetName,songExtensions);
            }
            else if ((typeof(T) == typeof(SoundEffect)))
            {
                result =  Normalize(assetName,soundEffectExtensions);
            }
            if (result == null) { //item might not be in the package or be an unsupported file type
                result = base.Normalize<T>(assetName);
            }
            return result;
        }

        protected override object ReadRawAsset<T>(string assetName, string originalAssetName)
        {
            if (zif == null)
                return base.ReadRawAsset<T>(assetName, originalAssetName);
            ZipEntry ze = zif.GetEntry (assetName);
            if (ze == null) {
                return base.ReadRawAsset<T>(assetName, originalAssetName);
            }
            if (typeof(T) == typeof(Texture2D) || typeof(T) == typeof(Texture)) {
                lock (ContentManagerLock) {
                    using (MemoryStream mstream = new MemoryStream ((int)ze.Size)) {

                        using (var stream = zif.GetInputStream (ze)) {
                            stream.CopyTo (mstream);

                            mstream.Seek (0, SeekOrigin.Begin);  

                        }

                        Texture2D texture = Texture2D.FromStream (
                                                graphicsDeviceService.GraphicsDevice, mstream);
                        texture.Name = originalAssetName;
                        return texture;
                    }
                }
            }
            else if ((typeof(T) == typeof(Song)))
            {
                return new Song(obbPath,zif.LocateEntry(ze),ze.CompressedSize);
            }
            else if ((typeof(T) == typeof(SoundEffect)))
            {
                return new SoundEffect(obbPath,zif.LocateEntry(ze),ze.CompressedSize);
            }
            throw new NotImplementedException ("This format of file is not supported as raw file");
        }

        internal string Normalize(string fileName, string[] extensions)
        {
            if (zif == null)
                return null;
            int index = fileName.LastIndexOf(Path.DirectorySeparatorChar);
            string path = string.Empty;
            string file = fileName;
            if (index >= 0)
            {
                path = fileName.Substring (0, index);
                file = fileName.Substring(index + 1, fileName.Length - index - 1);
            }

            Dictionary<string,int> files = null;
            if (!entries.TryGetValue (path,out files))
                return null;

            bool found = false;
            index=-1;
            foreach (string s in extensions) {
                if (files.TryGetValue (file + s, out index)) {
                    found = true;
                    break;
                }
            }
            if(!found)
                return null;

            return zif[index].Name;
        }
    }
}

I’ve just done a similar thing in the past week. The differences are that I used System.IO.Compression.Zip from the Android.Play.ExpansionLibrary project, I don’t support loading raw files and it falls back to the standard ContentManager if it cannot find the file in the zip. It also supports multiple content managers sharing the same zip file (to avoid opening the zip and loading the table of contents more than once), typically used for ContentManager instances created for loading and disposing of assets for a single game level.

using Android.Util;
using Microsoft.Xna.Framework.Content;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression.Zip;

namespace TaskForce
{
    public class ZipContentManager : ContentManager
    {
        List<ZipFile> _zipFiles = new List<ZipFile>();
        List<Dictionary<string, ZipFileEntry>> _zipFileEntries = new List<Dictionary<string, ZipFileEntry>>();
        ZipContentManager _master;

        public ZipContentManager(IServiceProvider serviceProvider)
            : base(serviceProvider)
        {
        }

        public ZipContentManager(IServiceProvider serviceProvider, string rootDirectory)
            : base(serviceProvider, rootDirectory)
        {
        }

        public ZipContentManager(ZipContentManager master)
            : base(master.ServiceProvider, master.RootDirectory)
        {
            _master = master;
        }

        public void AddZipFile(Stream stream)
        {
            var zipFile = new ZipFile(stream);
            _zipFiles.Add(zipFile);
            var files = zipFile.GetAllEntries();
            var dict = new Dictionary<string, ZipFileEntry>(files.Count);
            foreach (var file in files)
                dict.Add(file.FilenameInZip, file);
            _zipFileEntries.Add(dict);
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                for (var i = _zipFiles.Count; i >= 0; --i)
                {
                    _zipFileEntries[i].Clear();
                    _zipFiles[i].Dispose();
                }
            }
            _master = null;
            _zipFiles = null;
            _zipFileEntries = null;
            base.Dispose(disposing);
        }

        protected override Stream OpenStream(string assetName)
        {
            if (_master != null)
                return _master.OpenStream(assetName);
            Log.Debug("Content", assetName);
            var xnbName = assetName + ".xnb";
            for (int i = _zipFiles.Count - 1; i >= 0; --i)
            {
                var zipFile = _zipFiles[i];
                ZipFileEntry entry = null;
                if (_zipFileEntries[i].TryGetValue(xnbName, out entry))
                    return zipFile.ReadFile(entry);
            }
            return base.OpenStream(assetName);
        }
    }
}

Yours is so much cleaner ! I feel embarassed…

And I just spotted an error in my code. Off by one.

            for (var i = _zipFiles.Count; i >= 0; --i)

should be

            for (var i = _zipFiles.Count - 1; i >= 0; --i)