paulscode.android.mupen64plusae.jni.CoreService.java Source code

Java tutorial

Introduction

Here is the source code for paulscode.android.mupen64plusae.jni.CoreService.java

Source

/*
 * Mupen64PlusAE, an N64 emulator for the Android platform
 * 
 * Copyright (C) 2013 Paul Lamb
 * 
 * This file is part of Mupen64PlusAE.
 * 
 * Mupen64PlusAE is free software: you can redistribute it and/or modify it under the terms of the
 * GNU General Public License as published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * Mupen64PlusAE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with Mupen64PlusAE. If
 * not, see <http://www.gnu.org/licenses/>.
 * 
 * Authors: fzurita
 */
package paulscode.android.mupen64plusae.jni;

import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.graphics.drawable.BitmapDrawable;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.Vibrator;
import android.support.v4.app.NotificationCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.Surface;

import org.mupen64plusae.v3.alpha.R;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;

import paulscode.android.mupen64plusae.ActivityHelper;
import paulscode.android.mupen64plusae.GalleryActivity;

import static paulscode.android.mupen64plusae.ActivityHelper.Keys.RESUME_SERVICE;
import static paulscode.android.mupen64plusae.ActivityHelper.Keys.ROM_PATH;
import static paulscode.android.mupen64plusae.jni.NativeExports.emuGetFramelimiter;
import static paulscode.android.mupen64plusae.jni.NativeImports.removeOnStateCallbackListener;

public class CoreService extends Service {
    interface CoreServiceListener {
        /**
         * Called on failure with an error code
         * @param errorCode Error number
         */
        void onFailure(int errorCode);

        /**
         * Called once emulation has stopped
         */
        void onFinish();

        /**
         * Called when the service has been destroyed
         */
        void onCoreServiceDestroyed();
    }

    public interface AutoSaveCompleteAction {
        /**
         * Called when an auto save completes
         */
        void onSaveStateComplete();
    }

    private static boolean mIsServiceRunning = false;

    public static final String COMPLETE_EXTENSION = "complete";
    // Slot info - used internally
    private static final int NUM_SLOTS = 10;

    // Startup info - used internally
    private String mRomGoodName = null;
    private String mRomPath = null;
    private String mCheatOptions = null;
    private boolean mIsRestarting = false;
    private String mSaveToLoad = null;
    private String mCoreLib = null;
    private boolean mUseHighPriorityThread = false;
    private ArrayList<Integer> mPakType = null;
    private ArrayList<Boolean> mIsPlugged = null;
    private boolean mIsFrameLimiterEnabled = true;
    private String mCoreUserDataDir = null;
    private String mCoreUserCacheDir = null;
    private String mCoreUserConfigDir = null;
    private String mUserSaveDir = null;
    private boolean mIsRunning = false;
    private boolean mIsPaused = false;
    private String mArtPath = null;

    //Service attributes
    private int mStartId;
    private ServiceHandler mServiceHandler;
    private boolean mIsShuttingDown = false;

    private final IBinder mBinder = new LocalBinder();
    private CoreServiceListener mListener = null;

    final static int ONGOING_NOTIFICATION_ID = 1;

    boolean isRunning() {
        return mIsRunning;
    }

    void shutdownEmulator() {
        mIsShuttingDown = true;
        updateNotification();

        // Tell the core to quit
        NativeExports.emuStop();
    }

    void resumeEmulator() {
        mIsPaused = false;
        NativeExports.emuResume();

        updateNotification();
    }

    void autoSaveState(final String latestSave, final AutoSaveCompleteAction autoSaveCompleteAction) {
        // Auto-save in case device doesn't resume properly (e.g. OS kills process, battery dies, etc.)

        //Resume to allow save to take place
        resumeEmulator();
        Log.i("CoreService", "Saving file: " + latestSave);

        NativeImports.addOnStateCallbackListener(new NativeImports.OnStateCallbackListener() {
            @Override
            public void onStateCallback(int paramChanged, int newValue) {
                if (paramChanged == NativeConstants.M64CORE_STATE_SAVECOMPLETE) {
                    removeOnStateCallbackListener(this);

                    if (autoSaveCompleteAction != null) {
                        autoSaveCompleteAction.onSaveStateComplete();
                    }

                    //newValue == 1, then it was successful
                    if (newValue == 1) {
                        try {
                            new File(latestSave + "." + COMPLETE_EXTENSION).createNewFile();
                        } catch (IOException e) {
                            Log.e("CoreService", "Unable to save file due to file write failure: " + latestSave);
                        }
                    } else {
                        Log.e("CoreService", "Unable to save file due to bad return: " + latestSave);
                    }
                } else {
                    Log.i("CoreService", "Param changed = " + paramChanged + " value = " + newValue);
                }
            }
        });

        NativeExports.emuSaveFile(latestSave);
    }

    void saveState(String filename) {
        File currentSaveStateFile = new File(mUserSaveDir + "/" + filename);
        NativeExports.emuSaveFile(currentSaveStateFile.getAbsolutePath());
    }

    void pauseEmulator() {
        mIsPaused = true;
        NativeExports.emuPause();

        updateNotification();
    }

    void togglePause() {
        int state = NativeExports.emuGetState();
        if (state == NativeConstants.EMULATOR_STATE_PAUSED) {
            mIsPaused = false;
            NativeExports.emuResume();
        } else if (state == NativeConstants.EMULATOR_STATE_RUNNING) {
            mIsPaused = true;
            NativeExports.emuPause();
        }

        updateNotification();
    }

    boolean isPaused() {
        return mIsPaused;
    }

    void toggleFramelimiter() {
        boolean state = emuGetFramelimiter();
        NativeExports.emuSetFramelimiter(!state);
    }

    boolean getFramelimiter() {
        return NativeExports.emuGetFramelimiter();
    }

    void setSlot(int value) {
        int slot = value % NUM_SLOTS;
        NativeExports.emuSetSlot(slot);
    }

    int getSlot() {
        return NativeExports.emuGetSlot();
    }

    void saveSlot() {
        NativeExports.emuSaveSlot();
    }

    void loadSlot() {
        NativeExports.emuLoadSlot();
    }

    void loadState(File file) {
        NativeExports.emuLoadFile(file.getAbsolutePath());
    }

    void screenshot() {
        NativeExports.emuScreenshot();
    }

    void setCustomSpeed(int value) {
        NativeExports.emuSetSpeed(value);
    }

    void updateControllerConfig(int player, boolean plugged, int value) {
        NativeInput.setConfig(player, plugged, value);
    }

    void advanceFrame() {
        NativeExports.emuPause();
        NativeExports.emuAdvanceFrame();
    }

    void emuGameShark(boolean pressed) {
        NativeExports.emuGameShark(pressed);
    }

    void setOnFpsChangedListener(NativeImports.OnFpsChangedListener fpsListener, int fpsRecalcPeriod) {
        NativeImports.setOnFpsChangedListener(fpsListener, fpsRecalcPeriod);
    }

    void setControllerState(int controllerNum, boolean[] buttons, int axisX, int axisY) {
        NativeInput.setState(controllerNum, buttons, axisX, axisY);
    }

    void registerVibrator(int player, Vibrator vibrator) {
        NativeInput.registerVibrator(player, vibrator);
    }

    void restart() {
        NativeExports.emuReset();
    }

    int getState() {
        return NativeExports.emuGetState();
    }

    void setSurface(Surface surface) {
        NativeExports.setNativeWindow(surface);
    }

    void destroySurface() {
        NativeExports.emuDestroySurface();
    }

    /**
     * Class used for the client Binder.  Because we know this service always
     * runs in the same process as its clients, we don't need to deal with IPC.
     */
    class LocalBinder extends Binder {
        public CoreService getService() {
            // Return this instance of ExtractTexturesService so clients can call public methods
            return CoreService.this;
        }
    }

    // Handler that receives messages from the thread
    private final class ServiceHandler extends Handler {
        ServiceHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            // Only increase priority if we have more than one processor. The call to check the number of
            // processors is only available in API level 17
            if (Runtime.getRuntime().availableProcessors() > 1 && mUseHighPriorityThread) {
                android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
                Log.i("CoreService", "Using high priority mode");
            }

            // Initialize input-android plugin (even if we aren't going to use it)
            NativeInput.init();
            NativeInput.setConfig(0, mIsPlugged.get(0), mPakType.get(0));
            NativeInput.setConfig(1, mIsPlugged.get(1), mPakType.get(1));
            NativeInput.setConfig(2, mIsPlugged.get(2), mPakType.get(2));
            NativeInput.setConfig(3, mIsPlugged.get(3), mPakType.get(3));

            ArrayList<String> arglist = new ArrayList<>();
            arglist.add("mupen64plus");
            arglist.add("--corelib");
            arglist.add(mCoreLib);
            arglist.add("--configdir");
            arglist.add(mCoreUserConfigDir);

            if (!mIsRestarting) {
                arglist.add("--savestate");
                arglist.add(mSaveToLoad);
            }

            if (!mIsFrameLimiterEnabled) {
                arglist.add("--nospeedlimit");
            }
            if (mCheatOptions != null) {
                arglist.add("--cheats");
                arglist.add(mCheatOptions);
            }
            arglist.add(mRomPath);

            //This call blocks until emulation is stopped
            final int result = NativeExports.emuStart(mCoreUserDataDir, mCoreUserCacheDir, arglist.toArray());

            mIsRunning = false;

            NativeExports.unloadLibraries();

            if (mListener != null) {
                if (result != 0) {
                    mListener.onFailure(result);
                }

                mListener.onFinish();
            }

            // Stop the service using the startId, so that we don't stop
            // the service in the middle of handling another job
            stopSelf(msg.arg1);
        }
    }

    @Override
    public void onCreate() {
        // Start up the thread running the service.  Note that we create a
        // separate thread because the service normally runs in the process's
        // main thread, which we don't want to block.  We also make it
        // background priority so CPU-intensive work will not disrupt our UI.
        HandlerThread thread = new HandlerThread("ServiceStartArguments", Process.THREAD_PRIORITY_DEFAULT);
        thread.start();

        // Get the HandlerThread's Looper and use it for our Handler
        Looper serviceLooper = thread.getLooper();
        mServiceHandler = new ServiceHandler(serviceLooper);
    }

    private void updateNotification() {
        //Show the notification
        Intent notificationIntent = new Intent(this, GalleryActivity.class);
        notificationIntent.putExtra(RESUME_SERVICE, true);
        notificationIntent.putExtra(ROM_PATH, mRomPath);
        notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);

        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent,
                PendingIntent.FLAG_UPDATE_CURRENT);

        NotificationCompat.Builder builder = new NotificationCompat.Builder(this).setSmallIcon(R.drawable.icon)
                .setContentTitle(mRomGoodName).setContentIntent(pendingIntent);

        if (mIsShuttingDown) {
            builder.setContentText(getString(R.string.toast_shutting_down));
        } else if (mIsPaused) {
            builder.setContentText(getString(R.string.toast_paused));
        } else {
            builder.setContentText(getString(R.string.toast_running));
        }

        if (!TextUtils.isEmpty(mArtPath) && new File(mArtPath).exists()) {
            builder.setLargeIcon(new BitmapDrawable(this.getResources(), mArtPath).getBitmap());
        }
        startForeground(ONGOING_NOTIFICATION_ID, builder.build());
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent != null) {
            Bundle extras = intent.getExtras();

            mRomGoodName = extras.getString(ActivityHelper.Keys.ROM_GOOD_NAME);
            mRomPath = extras.getString(ActivityHelper.Keys.ROM_PATH);
            mCheatOptions = extras.getString(ActivityHelper.Keys.CHEAT_ARGS);
            mIsRestarting = extras.getBoolean(ActivityHelper.Keys.DO_RESTART, false);
            mSaveToLoad = extras.getString(ActivityHelper.Keys.SAVE_TO_LOAD);
            mCoreLib = extras.getString(ActivityHelper.Keys.CORE_LIB);
            mUseHighPriorityThread = extras.getBoolean(ActivityHelper.Keys.HIGH_PRIORITY_THREAD, false);

            mPakType = extras.getIntegerArrayList(ActivityHelper.Keys.PAK_TYPE_ARRAY);

            boolean[] isPluggedArray = extras.getBooleanArray(ActivityHelper.Keys.IS_PLUGGED_ARRAY);
            mIsPlugged = new ArrayList<>();

            for (Boolean isPlugged : isPluggedArray) {
                mIsPlugged.add(isPlugged);
            }

            mIsFrameLimiterEnabled = extras.getBoolean(ActivityHelper.Keys.IS_FPS_LIMIT_ENABLED, true);
            mCoreUserDataDir = extras.getString(ActivityHelper.Keys.CORE_USER_DATA_DIR);
            mCoreUserCacheDir = extras.getString(ActivityHelper.Keys.CORE_USER_CACHE_DIR);
            mCoreUserConfigDir = extras.getString(ActivityHelper.Keys.CORE_USER_CONFIG_DIR);
            mUserSaveDir = extras.getString(ActivityHelper.Keys.USER_SAVE_DIR);

            mArtPath = extras.getString(ActivityHelper.Keys.ROM_ART_PATH);

            String libsDir = extras.getString(ActivityHelper.Keys.LIBS_DIR);
            // Load the native libraries, this must be done outside the thread to prevent race conditions
            // that depend on the libraries being loaded after this call is made
            NativeExports.loadLibraries(libsDir, Build.VERSION.SDK_INT);

            mIsServiceRunning = true;

            updateNotification();
        }

        mStartId = startId;

        // If we get killed, after returning from here, restart
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        mIsServiceRunning = false;

        if (mListener != null) {
            mListener.onCoreServiceDestroyed();

            if (mIsRunning) {
                shutdownEmulator();
            }
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    public void setCoreServiceListener(CoreServiceListener coreServiceListener) {
        mListener = coreServiceListener;

        if (!mIsRunning) {
            mIsRunning = true;
            // For each start request, send a message to start a job and deliver the
            // start ID so we know which request we're stopping when we finish the job
            Message msg = mServiceHandler.obtainMessage();
            msg.arg1 = mStartId;
            mServiceHandler.sendMessage(msg);
        }
    }

    public static boolean IsServiceRunning() {
        return mIsServiceRunning;
    }

}