/* -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t; -*- */

/*
 * Strato HAL
 * Android Java main code.
 */

/*
 * See also ndkmain.c and ndkfile.c for the C part.
 */

/*-
 * SPDX-License-Identifier: Zlib
 *
 * Copyright (c) 2025-2026 Awe Morris
 * Copyright (c) 1996-2024 Keiichi Tabata
 *
 * This software is provided 'as-is', without any express or implied
 * warranty. In no event will the authors be held liable for any damages
 * arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must not
 *    claim that you wrote the original software. If you use this software
 *    in a product, an acknowledgment in the product documentation would be
 *    appreciated but is not required.
 * 2. Altered source versions must be plainly marked as such, and must not be
 *    misrepresented as being the original software.
 * 3. This notice may not be removed or altered from any source distribution.
 */

package io.noctvm.strato;

import static android.opengl.GLSurfaceView.RENDERMODE_CONTINUOUSLY;
import static android.opengl.GLSurfaceView.RENDERMODE_WHEN_DIRTY;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.AssetFileDescriptor;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.media.MediaPlayer;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.opengl.GLSurfaceView;
import android.opengl.GLSurfaceView.Renderer;
import android.opengl.GLES20;
import android.view.Gravity;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SurfaceView;
import android.view.View;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
import android.view.WindowManager;
import android.view.SurfaceHolder;
import android.view.WindowManager.LayoutParams;
import android.widget.LinearLayout;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.File;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

//
// Main class
//
public class MainActivity extends Activity {
    //
    // Config values (Please change these constants for your game)
    //

    // Window title
    public static String appName;

    // Window width
    public static int viewportWidth = 1280;

    // Window height
    public static int viewportHeight = 720;

	// Language
	private static String language = "en";

    //
    // JNI
    //

	static {
		// Load an Android NDK library.
        System.loadLibrary("strato");
	}

    // These are the native methods implemented in ndk*.c
    private native void nativeOnBootstrap();
    private native void nativeOnStart();
    private native int nativeGetScreenWidth();
    private native int nativeGetScreenHeight();
    private native String nativeGetTitle();
    private native void nativeOnRestart();
    private native void nativeOnStop();
    private native boolean nativeOnFrame();
    private native boolean nativeOnPause();
    private native boolean nativeOnResume();
    private native void nativeOnTouchStart(int x, int y, int points);
    private native void nativeOnTouchMove(int x, int y);
    private native void nativeOnTouchEnd(int x, int y, int points);
	private native void nativeOnKeyDown(int key);
	private native void nativeOnKeyUp(int key);
	private native void nativeOnGamepadAnalog(float x1, float y1, float x2, float y2, float l, float r);

    //
    // Variables (do not touch)
    //

    // The main view.
    private MainView view;

    // The viewport scale factor.
    private float scale;

    // The viewport offset X.
    private int offsetX;

    // The viewport offset Y.
    private int offsetY;

    // The last touched Y coordinate.
    private int touchLastY;

    // Finger count of a last touch.
    private int touchCount;

    // A flag that indicates if the game is loaded.
    private boolean isLoaded;

    // A flag that indicates if the game is finished.
    private boolean isFinished;

    // MediaPlayer for a video playback.
    private MediaPlayer video;

    // The view for video playback.
    private VideoSurfaceView videoView;

    // The layout for videoView.
    private LinearLayout videoLayout;

    // A flag that indicates if we are right after back from video playback.
    private boolean resumeFromVideo;

    // The synchronization object for the mutual exclusion between the main thread and the rendering thread.
    private final Object syncObj = new Object();

	// Rendering in-flight flag.
	private boolean in_flight;

	//
	// Key code
	//
	private static int KEY_CONTROL = 0;
	private static int KEY_SPACE = 1;
	private static int KEY_RETURN = 2;
	private static int KEY_UP = 3;
	private static int KEY_DOWN = 4;
	private static int KEY_LEFT = 5;
	private static int KEY_RIGHT = 6;
	private static int KEY_ESCAPE = 7;
	private static int KEY_C = 8;
	private static int KEY_S = 9;
	private static int KEY_L = 10;
	private static int KEY_H = 11;
	private static int KEY_GAMEPAD_UP = 12;
	private static int KEY_GAMEPAD_DOWN = 13;
	private static int KEY_GAMEPAD_LEFT = 14;
	private static int KEY_GAMEPAD_RIGHT = 15;
	private static int KEY_GAMEPAD_A = 16;
	private static int KEY_GAMEPAD_B = 17;
	private static int KEY_GAMEPAD_X = 18;
	private static int KEY_GAMEPAD_Y = 19;
	private static int KEY_GAMEPAD_L = 20;
	private static int KEY_GAMEPAD_R = 21;

	//
	// Constructor
	//
	public MainActivity() {
	}

    //
    // Called when the app instance is created.
    //
    @Override
    @SuppressWarnings("deprecation")
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        isFinished = false;

		// Call the native code.
		nativeOnBootstrap();
		viewportWidth = nativeGetScreenWidth();
		viewportHeight = nativeGetScreenHeight();
		appName = nativeGetTitle();

        // Do full screen settings. (step1)
        requestWindowFeature(Window.FEATURE_NO_TITLE);

        // Create the main view.
        view = new MainView(this);
        setContentView(view);

        // Do full screen settings. (step2)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            final WindowInsetsController controller = getWindow().getInsetsController();
            if (controller != null) {
                controller.hide(WindowInsets.Type.systemBars());
                controller.setSystemBarsBehavior(
                        WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
                );
            }
        } else {
            getWindow().setFlags(
                    WindowManager.LayoutParams.FLAG_FULLSCREEN,
                    WindowManager.LayoutParams.FLAG_FULLSCREEN
            );
        }

        // Prepare the video view.
        videoView = new VideoSurfaceView(this);
        videoLayout = new LinearLayout(this);
        videoLayout.setGravity(Gravity.CENTER);
        videoLayout.setBackgroundColor(Color.BLACK);
        videoLayout.addView(videoView);
        Thread videoThread = new Thread(videoView);
        videoThread.start();
    }

    //
    // Called when the app instance is paused.
    //
    @Override
    public void onPause() {
        super.onPause();
        nativeOnPause();
    }

    //
    // Called when the app instance is resumed.
    //
    @Override
    public void onResume() {
        super.onResume();
        nativeOnResume();
    }

    //
    // Called when the app instance is destroyed.
    //
    @Override
    public void onDestroy() {
        super.onDestroy();

        if(!isFinished) {
            synchronized (syncObj) {
                nativeOnStop();
            }
            isFinished = true;
        }
    }

    //
    // View class
    //
    private class MainView extends GLSurfaceView implements View.OnTouchListener, Renderer {
        //
        // Constructor of the view class
        //
        public MainView(Context context) {
            super(context);

            setFocusable(true);
			setFocusableInTouchMode(true);
			requestFocus();
            setOnTouchListener(this);
            setEGLConfigChooser(8, 8, 8, 8, 0, 0);
            setEGLContextClientVersion(2);
            setRenderer(this);
        }

        //
        // Called when the view is created.
        //
        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
            if(!resumeFromVideo) {
				synchronized(syncObj) {
					while(true) {
						if(!in_flight) {
							in_flight = true;
							break;
						}
						try {
							syncObj.wait();
						} catch (InterruptedException e) {
						}
					}
				}
				try {
					nativeOnStart();
					isLoaded = true;
				} finally {
					synchronized(syncObj) {
						in_flight = false;
						syncObj.notifyAll();
					}
                }
            } else {
                resumeFromVideo = false;
				synchronized(syncObj) {
					while(true) {
						if(!in_flight) {
							in_flight = true;
							break;
						}
						try {
							syncObj.wait();
						} catch (InterruptedException e) {
						}
					}
				}
				try {
                    nativeOnRestart();
				} finally {
					synchronized(syncObj) {
						in_flight = false;
						syncObj.notifyAll();
					}
                }
            }
        }

        //
        // Called when initialized or the screen size is changed.
        //
        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
            // Get the aspect ratio.
            float aspect = (float)viewportHeight / (float)viewportWidth;

            // Width-first.
            float w = width;
            float h = width * aspect;
            scale = w / (float)viewportWidth;
            offsetX = 0;
            offsetY = (int)((float)(height - h) / 2.0f);

            // Height-first.
            if(h > height) {
                h = height;
                w = height / aspect;
                scale = h / (float)viewportHeight;
                offsetX = (int)((float)(width - w) / 2.0f);
                offsetY = 0;
            }

            GLES20.glViewport(offsetX, offsetY, (int)w, (int)h);
        }

        //
        // Called when drawing to screen.
        //
        @Override
        public void onDrawFrame(GL10 gl) {
			synchronized (syncObj) {
				if (in_flight)
					return;

				if(video != null)
					return;
				if(!isLoaded)
					return;

				in_flight = true;
			}
			try {
				boolean ret = nativeOnFrame();
				if(!ret) {
					nativeOnStop();
					if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
						finishAndRemoveTask();
					isFinished = true;
				}
			} finally {
				synchronized (syncObj) {
					in_flight = false;
					syncObj.notifyAll();
				}
			}
        }

        //
        // Called when touched.
        //
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            int x = (int)((event.getX() - offsetX) / scale);
            int y = (int)((event.getY() - offsetY) / scale);
            int pointed = event.getPointerCount();
            int delta = y - touchLastY;

			synchronized(syncObj) {
				while(true) {
					if(!in_flight) {
						in_flight = true;
						break;
					}
					try {
						syncObj.wait();
					} catch (InterruptedException e) {
					}
				}
			}
			try {
                switch (event.getActionMasked()) {
                    case MotionEvent.ACTION_DOWN:
                        nativeOnTouchStart(x, y, pointed);
                        break;
                    case MotionEvent.ACTION_MOVE:
                        nativeOnTouchMove(x, y);
                        break;
                    case MotionEvent.ACTION_UP:
                        nativeOnTouchEnd(x, y, touchCount);
                        break;
				}
			} finally {
				synchronized(syncObj) {
					in_flight = false;
					syncObj.notifyAll();
				}
			}

			touchCount = pointed;
            return true;
        }

		@Override
		public boolean onKeyDown(int keyCode, KeyEvent event) {
			synchronized(syncObj) {
				while(true) {
					if(!in_flight) {
						in_flight = true;
						break;
					}
					try {
						syncObj.wait();
					} catch (InterruptedException e) {
					}
				}
			}
			try {
				switch (keyCode) {
				case KeyEvent.KEYCODE_BUTTON_A:
					nativeOnKeyDown(KEY_GAMEPAD_A);
					return true;
				case KeyEvent.KEYCODE_BUTTON_B:
					nativeOnKeyDown(KEY_GAMEPAD_B);
					return true;
				case KeyEvent.KEYCODE_BUTTON_X:
					nativeOnKeyDown(KEY_GAMEPAD_X);
					return true;
				case KeyEvent.KEYCODE_BUTTON_Y:
					nativeOnKeyDown(KEY_GAMEPAD_Y);
					return true;
				case KeyEvent.KEYCODE_BUTTON_L1:
					nativeOnKeyDown(KEY_GAMEPAD_L);
					return true;
				case KeyEvent.KEYCODE_BUTTON_R1:
					nativeOnKeyDown(KEY_GAMEPAD_R);
					return true;
				default:
					break;
				}
			} finally {
				synchronized(syncObj) {
					in_flight = false;
					syncObj.notifyAll();
				}
			}

			return super.onKeyDown(keyCode, event);
		}

		@Override
		public boolean onKeyUp(int keyCode, KeyEvent event) {
			synchronized(syncObj) {
				while(true) {
					if(!in_flight) {
						in_flight = true;
						break;
					}
					try {
						syncObj.wait();
					} catch (InterruptedException e) {
					}
				}
			}
			try {
				switch (keyCode) {
				case KeyEvent.KEYCODE_BUTTON_A:
					nativeOnKeyUp(KEY_GAMEPAD_A);
					return true;
				case KeyEvent.KEYCODE_BUTTON_B:
					nativeOnKeyUp(KEY_GAMEPAD_B);
					return true;
				case KeyEvent.KEYCODE_BUTTON_X:
					nativeOnKeyUp(KEY_GAMEPAD_X);
					return true;
				case KeyEvent.KEYCODE_BUTTON_Y:
					nativeOnKeyUp(KEY_GAMEPAD_Y);
					return true;
				case KeyEvent.KEYCODE_BUTTON_L1:
					nativeOnKeyUp(KEY_GAMEPAD_L);
					return true;
				case KeyEvent.KEYCODE_BUTTON_R1:
					nativeOnKeyUp(KEY_GAMEPAD_R);
					return true;
				default:
					break;
				}
			} finally {
				synchronized(syncObj) {
					in_flight = false;
					syncObj.notifyAll();
				}
			}

			return super.onKeyDown(keyCode, event);
		}

		@Override
		public boolean onGenericMotionEvent(MotionEvent event) {
			if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK &&
				event.getAction() == MotionEvent.ACTION_MOVE) {
				float x1 = event.getAxisValue(MotionEvent.AXIS_X);
				float y1 = event.getAxisValue(MotionEvent.AXIS_Y);
				float x2 = event.getAxisValue(MotionEvent.AXIS_RX);
				float y2 = event.getAxisValue(MotionEvent.AXIS_RY);
				float l = event.getAxisValue(MotionEvent.AXIS_LTRIGGER);
				float r = event.getAxisValue(MotionEvent.AXIS_RTRIGGER);
				nativeOnGamepadAnalog(x1, y2, x2, y2, l, r);

				float hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
				float hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);

				synchronized(syncObj) {
					while(true) {
						if(!in_flight) {
							in_flight = true;
							break;
						}
						try {
							syncObj.wait();
						} catch (InterruptedException e) {
						}
					}
				}
				try {
					if (hatX == -1.0f) {
						nativeOnKeyDown(KEY_GAMEPAD_LEFT);
						nativeOnKeyUp(KEY_GAMEPAD_RIGHT);
					} else if (hatX == 1.0f) {
						nativeOnKeyDown(KEY_GAMEPAD_RIGHT);
						nativeOnKeyUp(KEY_GAMEPAD_LEFT);
					} else {
						nativeOnKeyUp(KEY_GAMEPAD_RIGHT);
						nativeOnKeyUp(KEY_GAMEPAD_LEFT);
					}
					if (hatY == -1.0f) {
						nativeOnKeyDown(KEY_GAMEPAD_UP);
						nativeOnKeyUp(KEY_GAMEPAD_DOWN);
					} else if (hatY == 1.0f) {
						nativeOnKeyDown(KEY_GAMEPAD_DOWN);
						nativeOnKeyUp(KEY_GAMEPAD_UP);
					} else {
						nativeOnKeyUp(KEY_GAMEPAD_UP);
						nativeOnKeyUp(KEY_GAMEPAD_DOWN);
					}
				} finally {
					synchronized(syncObj) {
						in_flight = false;
						syncObj.notifyAll();
					}
				}

				return true;
			}
			return super.onGenericMotionEvent(event);
		}
    }

    //
    // Video rendering view class
    //
    class VideoSurfaceView extends SurfaceView implements SurfaceHolder.Callback, View.OnTouchListener, Runnable {
        //
        // Constructor
        //
        public VideoSurfaceView(Context context) {
            super(context);
            SurfaceHolder holder = getHolder();
            holder.addCallback(this);
            setOnTouchListener(this);
        }

        //
        // Called when created.
        //
        @Override
        public void surfaceCreated(SurfaceHolder paramSurfaceHolder) {
			synchronized(syncObj) {
				while(true) {
					if(!in_flight) {
						in_flight = true;
						break;
					}
					try {
						syncObj.wait();
					} catch (InterruptedException e) {
					}
				}
			}
			try {
				if(video != null) {
					SurfaceHolder holder = videoView.getHolder();
					video.setDisplay(holder);
					setWillNotDraw(false);
				}
			} finally {
				synchronized(syncObj) {
					in_flight = false;
					syncObj.notifyAll();
				}
			}
        }

        //
        // Called when initialized or the screen size is changed.
        //
        @Override
        public void surfaceChanged(SurfaceHolder paramSurfaceHolder, int paramInt1, int paramInt2, int paramInt3) {
        }

        //
        // Set video rendering size.
        //
        @Override
        public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int width = getDefaultSize(viewportWidth, widthMeasureSpec);
            int height = getDefaultSize(viewportHeight, heightMeasureSpec);
            if(viewportWidth > viewportHeight)
                width = (int)(height * ((float)viewportWidth / (float)viewportHeight));
            else
                height = (int)(width * ((float)viewportHeight / (float)viewportWidth));
            setMeasuredDimension(width, height);
            setForegroundGravity(Gravity.CENTER);
        }

        //
        // Called when destroyed.
        //
        @Override
        public void surfaceDestroyed(SurfaceHolder paramSurfaceHolder) {
        }

        //
        // Called when render to the screen.
        //
        @Override
        public void onDraw(Canvas canvas) {
			synchronized (syncObj) {
				if (in_flight)
					return;

				if(video == null)
					return;
				if(!isLoaded)
					return;

				in_flight = true;
			}
			try {
				boolean ret = nativeOnFrame();
				if(!ret)
					nativeOnStop();
				if (!ret) {
					if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
						finishAndRemoveTask();
					isFinished = true;
				}
			} finally {
				synchronized (syncObj) {
					in_flight = false;
					syncObj.notifyAll();
				}
			}
        }

        //
        // Video thread.
        //
        public void run() {
            //noinspection InfiniteLoopStatement
            do {
                synchronized (syncObj) {
                    if (video != null) {
                        new Handler(Looper.getMainLooper()).post(() -> {
							synchronized(syncObj) {
								while(true) {
									if(!in_flight) {
										in_flight = true;
										break;
									}
									try {
										syncObj.wait();
									} catch (InterruptedException e) {
									}
								}
							}
							try {
								if (video != null)
									videoView.invalidate();
							} finally {
								synchronized(syncObj) {
									in_flight = false;
									syncObj.notifyAll();
								}
							}
                        });
                    }
                }
                try {
                    //noinspection BusyWait
                    Thread.sleep(33);
                } catch (InterruptedException ignored) {
                }
            } while (true);
        }

        //
        // Called when touched during video playback.
        //
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            synchronized(syncObj) {
                nativeOnTouchStart(0, 0, 1);
                nativeOnTouchEnd(0, 0, 1);
            }
            return true;
        }
    }

    //
    // Helpers for JNI code
    //  - We name methods that are called from JNI code "bridge*()"
    //

    //
    // Start video playback.
    //
    private void bridgePlayVideo(String fileName, boolean isSkippable) {
		synchronized(syncObj) {
			while(true) {
				if(!in_flight) {
					in_flight = true;
					break;
				}
				try {
					syncObj.wait();
				} catch (InterruptedException e) {
				}
			}
		}
		try {
			if (video != null) {
				video.stop();
				video = null;
			}
			try {
				AssetFileDescriptor afd = getAssets().openFd("mov/" + fileName);
				video = new MediaPlayer();
				video.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
				video.prepare();
				new Handler(Looper.getMainLooper()).post(() -> {
					synchronized(syncObj) {
						view.setRenderMode(RENDERMODE_WHEN_DIRTY);
						setContentView(videoLayout);
						videoView.measure(viewportWidth, viewportHeight);
						video.start();
					}
				});
			} catch(IOException e) {
				Log.e(appName, "Failed to play video " + fileName);
			}
		} finally {
			synchronized(syncObj) {
				in_flight = false;
				syncObj.notifyAll();
			}
		}
    }

    //
    // Stop video playback.
    //
    private void bridgeStopVideo() {
		synchronized(syncObj) {
			while(true) {
				if(!in_flight) {
					in_flight = true;
					break;
				}
				try {
					syncObj.wait();
				} catch (InterruptedException e) {
				}
			}
		}
		try {
			if(video != null) {
				video.stop();
				video.reset();
				video.release();
				video = null;
				new Handler(Looper.getMainLooper()).post(() -> {
						resumeFromVideo = true;
						setContentView(view);
						view.setRenderMode(RENDERMODE_CONTINUOUSLY);
				});
			}
		} finally {
			synchronized(syncObj) {
				in_flight = false;
				syncObj.notifyAll();
			}
		}
    }

    //
    // Return whether video is playing.
    //  - If a video playback was finished, this method returns false.
    //
    private boolean bridgeIsVideoPlaying() {
		synchronized(syncObj) {
			while(true) {
				if(!in_flight) {
					in_flight = true;
					break;
				}
				try {
					syncObj.wait();
				} catch (InterruptedException e) {
				}
			}
		}
		try {
            if (video != null) {
                int pos = video.getCurrentPosition();
                if (pos == 0)
                    return true;
                if (video.isPlaying())
                    return true;
                video.stop();
                video = null;
                new Handler(Looper.getMainLooper()).post(() -> {
                    resumeFromVideo = true;
                    setContentView(view);
                    view.setRenderMode(RENDERMODE_CONTINUOUSLY);
                });
            }
		} finally {
			synchronized(syncObj) {
				in_flight = false;
				syncObj.notifyAll();
			}
		}
		return false;
    }

    //
    // Check if an asset file exists.
    //
    private boolean bridgeCheckFileExists(String fileName) {
        byte[] buf = null;
        try {
			InputStream is = null;
			if(fileName.startsWith("save/"))
				is = openFileInput(fileName);
			else
				is = getResources().getAssets().open(fileName);
            is.close();
        } catch(IOException e) {
            return false;
        }
        return true;
    }

    //
    // Get an entire file content.
    //
    private byte[] bridgeGetFileContent(String fileName) {
        if(fileName.startsWith("save/"))
            return getSaveFileContent(fileName.substring(5));
        else
            return getAssetFileContent(fileName);
    }

    //
    // Get a save file content.
    //
    private byte[] getSaveFileContent(String fileName) {
        byte[] buf = null;
        try {
            FileInputStream fis = openFileInput(fileName);
            buf = new byte[fis.available()];
            //noinspection ResultOfMethodCallIgnored
            fis.read(buf);
            fis.close();
        } catch(IOException ignored) {
        }
        return buf;
    }

    //
    // Get an asset file content.
    //
    private byte[] getAssetFileContent(String fileName) {
        byte[] buf = null;
        try {
            InputStream is = getResources().getAssets().open(fileName);
            buf = new byte[is.available()];
            //noinspection ResultOfMethodCallIgnored
            is.read(buf);
            is.close();
        } catch(IOException e) {
            Log.e(appName, "Failed to read file " + fileName);
        }
        return buf;
    }

    //
    // Remove a save file.
    //
    private void bridgeRemoveSaveFile(String fileName) {
        File file = new File(fileName);
        //noinspection ResultOfMethodCallIgnored
        file.delete();
    }

    //
    // Open a save file for subsequent writes.
    //
    private OutputStream bridgeOpenSaveFile(String fileName) {
        try {
            if(fileName.startsWith("save/"))
                fileName = fileName.substring(5);
            OutputStream os = openFileOutput(fileName, 0);
            return os;
        } catch(IOException e) {
            Log.e(appName, "Failed to write file " + fileName);
        }
        return null;
    }

    //
    // Write a byte to the opened save file.
    //
    private boolean bridgeWriteSaveFile(OutputStream os, byte[] b) {
        try {
            os.write(b);
            return true;
        } catch(IOException e) {
            Log.e(appName, "Failed to write file.");
        }
        return false;
    }

    //
    // Close the save file.
    //
    private void bridgeCloseSaveFile(OutputStream os) {
        try {
            os.close();
        } catch(IOException e) {
            Log.e(appName, "Failed to write file.");
        }
    }
}
