Android: Be able to have assets in a zip file

This commit is contained in:
zilmar 2023-02-06 17:41:23 +10:30
parent f79a2e9bde
commit cd7b0b42c3
5 changed files with 250 additions and 360 deletions

1
.gitignore vendored
View File

@ -96,3 +96,4 @@ Thumbs.db
/Source/Project64-input/Version.h
/Source/Project64-video/Version.h
/Source/RSP/Version.h
/Android/app/src/main/assets/assets.zip

View File

@ -33,6 +33,10 @@ IF NOT EXIST "%base_dir%/Android/assets/project64_data/Config/Enhancements/" mkd
xcopy "%base_dir%/Config/Enhancements" "%base_dir%/Android/assets/project64_data/Config/Enhancements/" /D /I /F /Y /E
IF %ERRORLEVEL% NEQ 0 (exit /B 1)
IF NOT EXIST "%base_dir%/Android/app/src/main/assets/" mkdir "%base_dir%/Android/app/src/main/assets/"
IF EXIST "%base_dir%/Android/app/src/main/assets/assets.zip" del "%base_dir%\Android\app\src\main\assets\assets.zip"
powershell Compress-Archive "%base_dir%/Android/assets/*" "%base_dir%/Android/app/src/main/assets/assets.zip"
goto :end
:End

View File

@ -1,17 +1,15 @@
package emu.project64;
import java.io.File;
import java.io.IOException;
import java.util.List;
import emu.project64.R;
import emu.project64.jni.NativeExports;
import emu.project64.jni.SettingsID;
import emu.project64.jni.UISettingID;
import emu.project64.task.ExtractAssetsTask;
import emu.project64.task.ExtractAssetsTask.ExtractAssetsListener;
import emu.project64.task.ExtractAssetZipTask;
import emu.project64.task.ExtractAssetZipTask.ExtractAssetZipListener;
import emu.project64.task.ExtractAssetsTask.Failure;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
@ -19,7 +17,6 @@ import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.app.ActivityCompat.OnRequestPermissionsResultCallback;
import androidx.core.content.ContextCompat;
@ -30,7 +27,7 @@ import android.util.Log;
import android.view.WindowManager.LayoutParams;
import android.widget.TextView;
public class SplashActivity extends AppCompatActivity implements ExtractAssetsListener, OnRequestPermissionsResultCallback
public class SplashActivity extends AppCompatActivity implements ExtractAssetZipListener, OnRequestPermissionsResultCallback
{
static final int PERMISSION_REQUEST = 177;
static final int NUM_PERMISSIONS = 2;
@ -251,58 +248,29 @@ public class SplashActivity extends AppCompatActivity implements ExtractAssetsLi
}
};
private boolean CountTotalAssetFiles(String path)
{
String [] list;
try
{
list = getAssets().list(path);
if (list.length > 0)
{
for (String file : list)
{
if (!CountTotalAssetFiles(path + "/" + file))
{
return false;
}
else
{
TOTAL_ASSETS += 1;
}
}
}
}
catch (IOException e)
{
return false;
}
return true;
}
private final Runnable extractAssetsTaskLauncher = new Runnable()
{
@Override
public void run()
{
Log.i( "Splash", "extractAssetsTaskLauncher - start");
TOTAL_ASSETS = 0;
CountTotalAssetFiles(SOURCE_DIR);
mAssetsExtracted = 0;
new ExtractAssetsTask( getAssets(), SOURCE_DIR, AndroidDevice.PACKAGE_DIRECTORY, SplashActivity.this ).execute();
new ExtractAssetZipTask( getAssets(), AndroidDevice.PACKAGE_DIRECTORY, SplashActivity.this ).execute();
}
};
@Override
public void onExtractAssetsProgress( String nextFileToExtract )
public void onExtractAssetsProgress( String text )
{
final float percent = ( 100f * mAssetsExtracted ) / (float) TOTAL_ASSETS;
final String text = getString( R.string.assetExtractor_progress, percent, nextFileToExtract );
mTextView.setText(text);
mAssetsExtracted++;
}
runOnUiThread(new Runnable() {
@Override
public void run() {
mTextView.setText(text);
}
});
};
@Override
public void onExtractAssetsFinished( List<Failure> failures )
public void onExtractAssetsFinished( List<ExtractAssetZipTask.Failure> failures )
{
if( failures.size() == 0 )
{
@ -322,12 +290,12 @@ public class SplashActivity extends AppCompatActivity implements ExtractAssetsLi
String weblink = getResources().getString( R.string.assetExtractor_uriHelp );
String message = getString( R.string.assetExtractor_failed, weblink );
String textHtml = message.replace( "\n", "<br/>" ) + "<p><small>";
for( Failure failure : failures )
for( ExtractAssetZipTask.Failure failure : failures )
{
textHtml += failure.toString() + "<br/>";
}
textHtml += "</small>";
mTextView.setText( Html.fromHtml( textHtml ) );
}
}
};
}

View File

@ -0,0 +1,230 @@
package emu.project64.task;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import android.content.res.AssetManager;
import android.os.AsyncTask;
import android.text.TextUtils;
import android.util.Log;
public class ExtractAssetZipTask extends AsyncTask<Void, String, List<ExtractAssetZipTask.Failure>>
{
public interface ExtractAssetZipListener
{
public void onExtractAssetsProgress( String nextFileToExtract );
public void onExtractAssetsFinished( List<Failure> failures );
}
public ExtractAssetZipTask( AssetManager assetManager, String dstPath, ExtractAssetZipListener listener)
{
if (assetManager == null )
throw new IllegalArgumentException( "Asset manager cannot be null" );
if( TextUtils.isEmpty( dstPath ) )
throw new IllegalArgumentException( "Destination path cannot be null or empty" );
mAssetManager = assetManager;
mDstPath = dstPath;
mListener = listener;
}
private final AssetManager mAssetManager;
private final String mDstPath;
private final ExtractAssetZipListener mListener;
@Override
protected List<Failure> doInBackground( Void... params )
{
return extractAssets( mDstPath );
}
@Override
protected void onProgressUpdate( String... values )
{
mListener.onExtractAssetsProgress( values[0] );
}
@Override
protected void onPostExecute( List<ExtractAssetZipTask.Failure> result )
{
mListener.onExtractAssetsFinished( result );
}
public static final class Failure
{
public enum Reason
{
FILE_UNWRITABLE,
FILE_UNCLOSABLE,
ASSET_UNCLOSABLE,
ASSET_IO_EXCEPTION,
FILE_IO_EXCEPTION,
}
public final String srcPath;
public final String dstPath;
public final Reason reason;
public Failure( String srcPath, String dstPath, Reason reason )
{
this.srcPath = srcPath;
this.dstPath = dstPath;
this.reason = reason;
}
@Override
public String toString()
{
switch( reason )
{
case FILE_UNWRITABLE:
return "Failed to open file " + dstPath;
case FILE_UNCLOSABLE:
return "Failed to close file " + dstPath;
case ASSET_UNCLOSABLE:
return "Failed to close asset " + srcPath;
case ASSET_IO_EXCEPTION:
return "Failed to extract asset " + srcPath + " to file " + dstPath;
case FILE_IO_EXCEPTION:
return "Failed to add file " + srcPath + " to file " + dstPath;
default:
return "Failed using source " + srcPath + " and destination " + dstPath;
}
}
}
private List<Failure> extractAssets( String dstPath )
{
final List<Failure> failures = new ArrayList<Failure>();
// Ensure the parent directories exist
File root = new File(dstPath + "/");
if (!root.exists()) {
root.mkdirs();
}
String dstFile = dstPath + "/assets.zip";
String srcFile = "assets.zip";
// Call the progress listener before extracting
publishProgress( dstPath );
// IO objects, initialize null to eliminate lint error
OutputStream out = null;
InputStream in = null;
Boolean ExtractZip = false;
// Extract the file
try
{
out = new FileOutputStream( dstFile );
in = mAssetManager.open( srcFile);
byte[] buffer = new byte[1024];
int read;
mListener.onExtractAssetsProgress( "copying asset zip file" );
while( ( read = in.read( buffer ) ) != -1 )
{
out.write( buffer, 0, read );
}
mListener.onExtractAssetsProgress( "Finished copying assset zip" );
out.flush();
ExtractZip = true;
}
catch( FileNotFoundException e )
{
Failure failure = new Failure( srcFile, dstPath, Failure.Reason.FILE_UNWRITABLE );
Log.e( "ExtractAssetZipTask", failure.toString() );
failures.add( failure );
}
catch( IOException e )
{
Failure failure = new Failure( srcFile, dstPath, Failure.Reason.ASSET_IO_EXCEPTION );
Log.e( "ExtractAssetZipTask", failure.toString() );
failures.add( failure );
}
finally
{
if( out != null )
{
try
{
out.close();
}
catch( IOException e )
{
Failure failure = new Failure( srcFile, dstPath, Failure.Reason.FILE_UNCLOSABLE );
Log.e( "ExtractAssetZipTask", failure.toString() );
failures.add( failure );
}
}
if( in != null )
{
try
{
in.close();
}
catch( IOException e )
{
Failure failure = new Failure( srcFile, dstPath, Failure.Reason.ASSET_UNCLOSABLE );
Log.e( "ExtractAssetZipTask", failure.toString() );
failures.add( failure );
}
}
}
if (ExtractZip)
{
String zipFilePath = dstPath + "/assets.zip";
String destDir = dstPath + "/";
File dir = new File(destDir);
// create output directory if it doesn't exist
if(!dir.exists()) dir.mkdirs();
FileInputStream fis;
//buffer for read and write data to file
byte[] buffer = new byte[1024];
try {
fis = new FileInputStream(zipFilePath);
ZipInputStream zis = new ZipInputStream(fis);
ZipEntry ze = zis.getNextEntry();
while(ze != null){
String fileName = ze.getName();
File newFile = new File(destDir + File.separator + fileName.replace("\\", "/"));
mListener.onExtractAssetsProgress( "Unzipping "+fileName );
//create directories for sub directories in zip
new File(newFile.getParent()).mkdirs();
if (ze.getSize() != 0) {
FileOutputStream fos = new FileOutputStream(newFile);
int len;
while ((len = zis.read(buffer)) > 0) {
fos.write(buffer, 0, len);
}
fos.close();
}
//close this ZipEntry
zis.closeEntry();
ze = zis.getNextEntry();
}
//close last ZipEntry
zis.closeEntry();
zis.close();
fis.close();
} catch (IOException e) {
Failure failure = new Failure( srcFile, dstPath, Failure.Reason.ASSET_IO_EXCEPTION );
Log.e( "ExtractAssetZipTask", failure.toString() );
failures.add( failure );
}
}
return failures;
}
}

View File

@ -1,313 +0,0 @@
package emu.project64.task;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.content.res.AssetManager;
import android.os.AsyncTask;
import android.text.TextUtils;
import android.util.Log;
public class ExtractAssetsTask extends AsyncTask<Void, String, List<ExtractAssetsTask.Failure>>
{
public interface ExtractAssetsListener
{
public void onExtractAssetsProgress( String nextFileToExtract );
public void onExtractAssetsFinished( List<Failure> failures );
}
public ExtractAssetsTask( AssetManager assetManager, String srcPath, String dstPath, ExtractAssetsListener listener )
{
if (assetManager == null )
throw new IllegalArgumentException( "Asset manager cannot be null" );
if( TextUtils.isEmpty( srcPath ) )
throw new IllegalArgumentException( "Source path cannot be null or empty" );
if( TextUtils.isEmpty( dstPath ) )
throw new IllegalArgumentException( "Destination path cannot be null or empty" );
if( listener == null )
throw new IllegalArgumentException( "Listener cannot be null" );
mAssetManager = assetManager;
mSrcPath = srcPath;
mDstPath = dstPath;
mListener = listener;
}
private final AssetManager mAssetManager;
private final String mSrcPath;
private final String mDstPath;
private final ExtractAssetsListener mListener;
@Override
protected List<Failure> doInBackground( Void... params )
{
return extractAssets( mSrcPath, mDstPath );
}
@Override
protected void onProgressUpdate( String... values )
{
mListener.onExtractAssetsProgress( values[0] );
}
@Override
protected void onPostExecute( List<ExtractAssetsTask.Failure> result )
{
mListener.onExtractAssetsFinished( result );
}
public static final class Failure
{
public enum Reason
{
FILE_UNWRITABLE,
FILE_UNCLOSABLE,
ASSET_UNCLOSABLE,
ASSET_IO_EXCEPTION,
FILE_IO_EXCEPTION,
}
public final String srcPath;
public final String dstPath;
public final Reason reason;
public Failure( String srcPath, String dstPath, Reason reason )
{
this.srcPath = srcPath;
this.dstPath = dstPath;
this.reason = reason;
}
@Override
public String toString()
{
switch( reason )
{
case FILE_UNWRITABLE:
return "Failed to open file " + dstPath;
case FILE_UNCLOSABLE:
return "Failed to close file " + dstPath;
case ASSET_UNCLOSABLE:
return "Failed to close asset " + srcPath;
case ASSET_IO_EXCEPTION:
return "Failed to extract asset " + srcPath + " to file " + dstPath;
case FILE_IO_EXCEPTION:
return "Failed to add file " + srcPath + " to file " + dstPath;
default:
return "Failed using source " + srcPath + " and destination " + dstPath;
}
}
}
private List<Failure> extractAssets( String srcPath, String dstPath )
{
final List<Failure> failures = new ArrayList<Failure>();
if( srcPath.startsWith( "/" ) )
srcPath = srcPath.substring( 1 );
String[] srcSubPaths = getAssetList( mAssetManager, srcPath );
if( srcSubPaths.length > 0 )
{
// srcPath is a directory
// Ensure the parent directories exist
new File( dstPath ).mkdirs();
// Some files are too big for Android 2.2 and below, so we break them into parts.
// We use a simple naming scheme where we just append .part0, .part1, etc.
Pattern pattern = Pattern.compile( "(.+)\\.part(\\d+)$" );
HashMap<String, Integer> fileParts = new HashMap<String, Integer>();
// Recurse into each subdirectory
for( String srcSubPath : srcSubPaths )
{
Matcher matcher = pattern.matcher( srcSubPath );
if( matcher.matches() )
{
String name = matcher.group(1);
if( fileParts.containsKey( name ) )
fileParts.put( name, fileParts.get( name ) + 1 );
else
fileParts.put( name, 1 );
}
String suffix = "/" + srcSubPath;
failures.addAll( extractAssets( srcPath + suffix, dstPath + suffix ) );
}
// Combine the large broken files, if any
combineFileParts( fileParts, dstPath );
}
else // srcPath is a file.
{
// Call the progress listener before extracting
publishProgress( dstPath );
// IO objects, initialize null to eliminate lint error
OutputStream out = null;
InputStream in = null;
// Extract the file
try
{
out = new FileOutputStream( dstPath );
in = mAssetManager.open( srcPath );
byte[] buffer = new byte[1024];
int read;
while( ( read = in.read( buffer ) ) != -1 )
{
out.write( buffer, 0, read );
}
out.flush();
}
catch( FileNotFoundException e )
{
Failure failure = new Failure( srcPath, dstPath, Failure.Reason.FILE_UNWRITABLE );
Log.e( "ExtractAssetsTask", failure.toString() );
failures.add( failure );
}
catch( IOException e )
{
Failure failure = new Failure( srcPath, dstPath, Failure.Reason.ASSET_IO_EXCEPTION );
Log.e( "ExtractAssetsTask", failure.toString() );
failures.add( failure );
}
finally
{
if( out != null )
{
try
{
out.close();
}
catch( IOException e )
{
Failure failure = new Failure( srcPath, dstPath, Failure.Reason.FILE_UNCLOSABLE );
Log.e( "ExtractAssetsTask", failure.toString() );
failures.add( failure );
}
}
if( in != null )
{
try
{
in.close();
}
catch( IOException e )
{
Failure failure = new Failure( srcPath, dstPath, Failure.Reason.ASSET_UNCLOSABLE );
Log.e( "ExtractAssetsTask", failure.toString() );
failures.add( failure );
}
}
}
}
return failures;
}
private static String[] getAssetList( AssetManager assetManager, String srcPath )
{
String[] srcSubPaths = null;
try
{
srcSubPaths = assetManager.list( srcPath );
}
catch( IOException e )
{
Log.w( "ExtractAssetsTask", "Failed to get asset file list." );
}
return srcSubPaths;
}
private static List<Failure> combineFileParts( Map<String, Integer> filePieces, String dstPath )
{
List<Failure> failures = new ArrayList<Failure>();
for (String name : filePieces.keySet() )
{
String src = null;
String dst = dstPath + "/" + name;
OutputStream out = null;
InputStream in = null;
try
{
out = new FileOutputStream( dst );
byte[] buffer = new byte[1024];
int read;
for( int i = 0; i < filePieces.get( name ); i++ )
{
src = dst + ".part" + i;
try
{
in = new FileInputStream( src );
while( ( read = in.read( buffer ) ) != -1 )
{
out.write( buffer, 0, read );
}
out.flush();
}
catch( IOException e )
{
Failure failure = new Failure( src, dst, Failure.Reason.FILE_IO_EXCEPTION );
Log.e( "ExtractAssetsTask", failure.toString() );
failures.add( failure );
}
finally
{
if( in != null )
{
try
{
in.close();
new File( src ).delete();
}
catch( IOException e )
{
Failure failure = new Failure( src, dst, Failure.Reason.FILE_UNCLOSABLE );
Log.e( "ExtractAssetsTask", failure.toString() );
failures.add( failure );
}
}
}
}
}
catch( FileNotFoundException e )
{
Failure failure = new Failure( src, dst, Failure.Reason.FILE_UNWRITABLE );
Log.e( "ExtractAssetsTask", failure.toString() );
failures.add( failure );
}
finally
{
if( out != null )
{
try
{
out.close();
}
catch( IOException e )
{
Failure failure = new Failure( src, dst, Failure.Reason.FILE_UNCLOSABLE );
Log.e( "ExtractAssetsTask", failure.toString() );
failures.add( failure );
}
}
}
}
return failures;
}
}