failures )
+ {
+ if( failures.size() == 0 )
+ {
+ if (!mAppInit)
+ {
+ InitProject64();
+ }
+
+ // Extraction succeeded, record new asset version and merge cheats
+ mTextView.setText( R.string.assetExtractor_finished );
+ NativeExports.UISettingsSaveDword(UISettingID.Asserts_Version.getValue(), ASSET_VERSION);
+
+ // Launch gallery activity
+ Intent intent = new Intent( this, GalleryActivity.class );
+ this.startActivity( intent );
+
+ // We never want to come back to this activity, so finish it
+ finish();
+ }
+ else
+ {
+ // Extraction failed, update the on-screen text and don't start next activity
+ String weblink = getResources().getString( R.string.assetExtractor_uriHelp );
+ String message = getString( R.string.assetExtractor_failed, weblink );
+ String textHtml = message.replace( "\n", "
" ) + "";
+ for( Failure failure : failures )
+ {
+ textHtml += failure.toString() + "
";
+ }
+ textHtml += "";
+ mTextView.setText( Html.fromHtml( textHtml ) );
+ }
+ }
+}
diff --git a/Android/src/emu/project64/compat/AppCompatPreferenceActivity.java b/Android/src/emu/project64/compat/AppCompatPreferenceActivity.java
new file mode 100644
index 000000000..21ee8c6e5
--- /dev/null
+++ b/Android/src/emu/project64/compat/AppCompatPreferenceActivity.java
@@ -0,0 +1,17 @@
+/****************************************************************************
+* *
+* Project 64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.compat;
+
+import android.preference.PreferenceActivity;
+
+public class AppCompatPreferenceActivity extends PreferenceActivity
+{
+}
diff --git a/Android/src/emu/project64/dialog/Popups.java b/Android/src/emu/project64/dialog/Popups.java
new file mode 100644
index 000000000..463bf3086
--- /dev/null
+++ b/Android/src/emu/project64/dialog/Popups.java
@@ -0,0 +1,64 @@
+/****************************************************************************
+* *
+* Project 64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.dialog;
+
+import emu.project64.R;
+import emu.project64.util.DeviceUtil;
+import android.annotation.SuppressLint;
+import android.app.AlertDialog.Builder;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+
+public class Popups
+{
+
+ public static void showHardwareInfo( Context context )
+ {
+ String title = context.getString( R.string.menuItem_hardwareInfo );
+ String axisInfo = DeviceUtil.getAxisInfo();
+ String peripheralInfo = DeviceUtil.getPeripheralInfo();
+ String cpuInfo = DeviceUtil.getCpuInfo();
+ String message = axisInfo + peripheralInfo + cpuInfo;
+ showShareableText( context, title, message );
+ }
+
+ public static void showShareableText( final Context context, String title, final String message )
+ {
+ // Set up click handler to share text with a user-selected app (email, clipboard, etc.)
+ DialogInterface.OnClickListener shareHandler = new DialogInterface.OnClickListener()
+ {
+ @SuppressLint( "InlinedApi" )
+ @Override
+ public void onClick( DialogInterface dialog, int which )
+ {
+ launchPlainText( context, message,
+ context.getText( R.string.actionShare_title ) );
+ }
+ };
+
+ new Builder( context ).setTitle( title ).setMessage( message.toString() )
+ .setNeutralButton( R.string.actionShare_title, shareHandler ).create().show();
+ }
+
+ @SuppressLint( "InlinedApi" )
+ public static void launchPlainText( Context context, String text, CharSequence chooserTitle )
+ {
+ // See http://android-developers.blogspot.com/2012/02/share-with-intents.html
+ Intent intent = new Intent( android.content.Intent.ACTION_SEND );
+ intent.setType( "text/plain" );
+ intent.addFlags( Intent.FLAG_ACTIVITY_NEW_DOCUMENT );
+ intent.putExtra( Intent.EXTRA_TEXT, text );
+ // intent.putExtra( Intent.EXTRA_SUBJECT, subject );
+ // intent.putExtra( Intent.EXTRA_EMAIL, new String[] { emailTo } );
+ context.startActivity( Intent.createChooser( intent, chooserTitle ) );
+ }
+}
diff --git a/Android/src/emu/project64/dialog/ProgressDialog.java b/Android/src/emu/project64/dialog/ProgressDialog.java
new file mode 100644
index 000000000..4ecb0ccc0
--- /dev/null
+++ b/Android/src/emu/project64/dialog/ProgressDialog.java
@@ -0,0 +1,245 @@
+/****************************************************************************
+* *
+* Project 64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.dialog;
+
+import emu.project64.R;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+public class ProgressDialog implements OnClickListener
+{
+ private static final float PROGRESS_PRECISION = 1000f;
+
+ private final Activity mActivity;
+ private final TextView mTextProgress;
+ private final TextView mTextSubprogress;
+ private final TextView mTextMessage;
+ private final ProgressBar mProgressSubtotal;
+ private final ProgressBar mProgressTotal;
+ private AlertDialog mDialog;
+ private AlertDialog mAbortDialog;
+
+ private long mMaxProgress = -1;
+ private long mMaxSubprogress = -1;
+ private long mProgress = 0;
+ private long mSubprogress = 0;
+
+ @SuppressLint( "InflateParams" )
+ public ProgressDialog( Activity activity, CharSequence title,
+ CharSequence subtitle, CharSequence message, boolean cancelable )
+ {
+ mActivity = activity;
+
+ final LayoutInflater inflater = (LayoutInflater) mActivity
+ .getSystemService( Context.LAYOUT_INFLATER_SERVICE );
+ View layout = inflater.inflate( R.layout.progress_dialog, null );
+
+ mTextProgress = (TextView) layout.findViewById( R.id.textProgress );
+ mTextSubprogress = (TextView) layout.findViewById( R.id.textSubprogress );
+ mTextMessage = (TextView) layout.findViewById( R.id.textMessage );
+ mProgressSubtotal = (ProgressBar) layout.findViewById( R.id.progressSubtotal );
+ mProgressTotal = (ProgressBar) layout.findViewById( R.id.progressTotal );
+
+ // Create main dialog
+ Builder builder = getBuilder( activity, title, subtitle, message, cancelable, layout );
+ mDialog = builder.create();
+
+ // Create canceling dialog
+ subtitle = mActivity.getString( R.string.toast_canceling );
+ message = mActivity.getString( R.string.toast_canceling );
+ layout = inflater.inflate( R.layout.progress_dialog, null );
+ builder = getBuilder( activity, title, subtitle, message, false, layout );
+ mAbortDialog = builder.create();
+ }
+
+ public ProgressDialog(ProgressDialog original, Activity activity, CharSequence title,
+ CharSequence subtitle, CharSequence message, boolean cancelable)
+ {
+ this(activity, title, subtitle, message, cancelable);
+
+ if(original != null)
+ {
+ setMaxProgress(original.mMaxProgress);
+ setMaxSubprogress(original.mMaxSubprogress);
+
+ mProgress = original.mProgress;
+ mSubprogress = original.mSubprogress;
+
+ incrementProgress(0);
+ incrementSubprogress(0);
+
+ mTextProgress.setText(original.mTextProgress.getText());
+ mTextSubprogress.setText(original.mTextSubprogress.getText());
+ mTextMessage.setText(original.mTextMessage.getText());
+ }
+ }
+
+ public void show()
+ {
+ mAbortDialog.show();
+ mDialog.show();
+ }
+
+ public void dismiss()
+ {
+ mAbortDialog.dismiss();
+ mDialog.dismiss();
+ }
+
+ @Override
+ public void onClick( DialogInterface dlg, int which )
+ {
+ if( which == DialogInterface.BUTTON_NEGATIVE)
+ {
+ }
+ }
+
+ private Builder getBuilder( Activity activity, CharSequence title, CharSequence subtitle,
+ CharSequence message, boolean cancelable, View layout )
+ {
+ TextView textSubtitle = (TextView) layout.findViewById( R.id.textSubtitle );
+ TextView textMessage = (TextView) layout.findViewById( R.id.textMessage );
+ textSubtitle.setText( subtitle );
+ textMessage.setText( message );
+
+ Builder builder = new Builder( activity ).setTitle( title ).setCancelable( false )
+ .setPositiveButton( null, null ).setView( layout );
+ if( cancelable )
+ builder.setNegativeButton( android.R.string.cancel, this );
+ else
+ builder.setNegativeButton( null, null );
+ return builder;
+ }
+
+ public void setText( final CharSequence text )
+ {
+ mActivity.runOnUiThread( new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ mTextProgress.setText( text );
+ }
+ } );
+ }
+
+ public void setSubtext( final CharSequence text )
+ {
+ mActivity.runOnUiThread( new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ mTextSubprogress.setText( text );
+ }
+ } );
+ }
+
+ public void setMessage( final CharSequence text )
+ {
+ mActivity.runOnUiThread( new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ mTextMessage.setText( text );
+ }
+ } );
+ }
+
+ public void setMessage( final int resid )
+ {
+ mActivity.runOnUiThread( new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ mTextMessage.setText( resid );
+ }
+ } );
+ }
+
+ public void setMaxProgress( final long size )
+ {
+ mActivity.runOnUiThread( new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ mMaxProgress = size;
+ mProgress = 0;
+ mProgressTotal.setProgress( 0 );
+ mProgressTotal.setVisibility( mMaxProgress > 0 ? View.VISIBLE : View.GONE );
+ }
+ } );
+ }
+
+ public void setMaxSubprogress( final long size )
+ {
+ mActivity.runOnUiThread( new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ mMaxSubprogress = size;
+ mSubprogress = 0;
+ mProgressSubtotal.setProgress( 0 );
+ mProgressSubtotal.setVisibility( mMaxSubprogress > 0 ? View.VISIBLE : View.GONE );
+ }
+ } );
+ }
+
+ public void incrementProgress( final long inc )
+ {
+ mActivity.runOnUiThread( new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ if( mMaxProgress > 0 )
+ {
+ mProgress += inc;
+ int pctProgress = Math.round( ( PROGRESS_PRECISION * mProgress )
+ / mMaxProgress );
+ mProgressTotal.setProgress( pctProgress );
+ }
+ }
+ } );
+ }
+
+ public void incrementSubprogress( final long inc )
+ {
+ mActivity.runOnUiThread( new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ if( mMaxSubprogress > 0 )
+ {
+ mSubprogress += inc;
+ int pctSubprogress = Math.round( ( PROGRESS_PRECISION * mSubprogress )
+ / mMaxSubprogress );
+ mProgressSubtotal.setProgress( pctSubprogress );
+ }
+ }
+ } );
+ }
+}
diff --git a/Android/src/emu/project64/dialog/Prompt.java b/Android/src/emu/project64/dialog/Prompt.java
new file mode 100644
index 000000000..c1d417714
--- /dev/null
+++ b/Android/src/emu/project64/dialog/Prompt.java
@@ -0,0 +1,159 @@
+/****************************************************************************
+* *
+* Project64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.dialog;
+
+import java.io.File;
+import java.util.List;
+
+import emu.project64.R;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+/**
+ * A utility class that generates dialogs to prompt the user for information.
+ */
+public final class Prompt
+{
+ /**
+ * An interface that simplifies the population of list items.
+ *
+ * @param The type of the data to be wrapped.
+ * @see Prompt#createAdapter(Context, List, int, int, ListItemPopulator)
+ */
+ public interface ListItemPopulator
+ {
+ public void onPopulateListItem( T item, int position, View view );
+ }
+
+ /**
+ * An interface that simplifies the population of list items having two text fields and an icon.
+ *
+ * @param The type of the data to be wrapped.
+ * @see Prompt#createAdapter(Context, List, ListItemTwoTextIconPopulator)
+ */
+ public interface ListItemTwoTextIconPopulator
+ {
+ public void onPopulateListItem( T item, int position, TextView text1, TextView text2,
+ ImageView icon );
+ }
+
+ /**
+ * Create a {@link ListAdapter} where each list item has a specified layout.
+ *
+ * @param The type of the data to be wrapped.
+ * @param context The current context.
+ * @param items The data source for the list items.
+ * @param layoutResId The layout resource to be used for each list item.
+ * @param textResId The {@link TextView} resource within the layout to be populated by default.
+ * @param populator The object to populate the fields in each list item.
+ *
+ * @return An adapter that can be used to create list dialogs.
+ */
+ public static ArrayAdapter createAdapter( Context context, List items,
+ final int layoutResId, final int textResId, final ListItemPopulator populator )
+ {
+ return new ArrayAdapter( context, layoutResId, textResId, items )
+ {
+ @Override
+ public View getView( int position, View convertView, ViewGroup parent )
+ {
+ View row;
+ if( convertView == null )
+ {
+ LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE );
+ row = (View) inflater.inflate( layoutResId, null );
+ }
+ else
+ {
+ row = (View) convertView;
+ }
+
+ populator.onPopulateListItem( getItem( position ), position, row );
+ return row;
+ }
+ };
+ }
+
+ /**
+ * Create a {@link ListAdapter} where each list item has two text fields and an icon.
+ *
+ * @param The type of the data to be wrapped.
+ * @param context The activity context.
+ * @param items The data source for list items.
+ * @param populator The object to populate the fields in each list item.
+ *
+ * @return An adapter that can be used to create list dialogs.
+ */
+ public static ArrayAdapter createAdapter( Context context, List items,
+ final ListItemTwoTextIconPopulator populator )
+ {
+ return createAdapter( context, items, R.layout.list_item_two_text_icon, R.id.text1,
+ new ListItemPopulator()
+ {
+ @Override
+ public void onPopulateListItem( T item, int position, View view )
+ {
+ TextView text1 = (TextView) view.findViewById( R.id.text1 );
+ TextView text2 = (TextView) view.findViewById( R.id.text2 );
+ ImageView icon = (ImageView) view.findViewById( R.id.icon );
+ populator.onPopulateListItem( item, position, text1, text2, icon );
+ }
+ } );
+ }
+
+ public static ArrayAdapter createFilenameAdapter( Context context, List paths,
+ final List names )
+ {
+ return createAdapter( context, paths, new ListItemTwoTextIconPopulator()
+ {
+ @Override
+ public void onPopulateListItem( String path, int position, TextView text1,
+ TextView text2, ImageView icon )
+ {
+ if( !TextUtils.isEmpty( path ) )
+ {
+ String name = names.get( position ).toString();
+ if( name.equals( ".." ) )
+ {
+ text1.setText( R.string.pathPreference_parentFolder );
+ icon.setVisibility( View.VISIBLE );
+ icon.setImageResource( R.drawable.ic_arrow_u );
+ }
+ else
+ {
+ File file = new File( path );
+ text1.setText( name );
+ if( file.isDirectory() )
+ {
+ icon.setVisibility( View.VISIBLE );
+ icon.setImageResource( R.drawable.ic_folder );
+ }
+ else
+ {
+ icon.setVisibility( View.GONE );
+ icon.setImageResource( 0 );
+ }
+ }
+ text2.setVisibility( View.GONE );
+ text2.setText( null );
+ }
+ }
+ } );
+ }
+}
\ No newline at end of file
diff --git a/Android/src/emu/project64/game/GameActivity.java b/Android/src/emu/project64/game/GameActivity.java
new file mode 100644
index 000000000..2700e6956
--- /dev/null
+++ b/Android/src/emu/project64/game/GameActivity.java
@@ -0,0 +1,98 @@
+/****************************************************************************
+* *
+* Project 64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.game;
+
+import emu.project64.jni.NativeExports;
+import emu.project64.jni.SystemEvent;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+
+public class GameActivity extends Activity
+{
+ private GameLifecycleHandler mLifecycleHandler;
+ @SuppressWarnings("unused")
+ private GameMenuHandler mMenuHandler;
+
+ @Override
+ public void onWindowFocusChanged( boolean hasFocus )
+ {
+ super.onWindowFocusChanged( hasFocus );
+ mLifecycleHandler.onWindowFocusChanged( hasFocus );
+ }
+
+ @Override
+ public void onBackPressed()
+ {
+ NativeExports.ExternalEvent( SystemEvent.SysEvent_PauseCPU_AppLostActive.getValue());
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage("Confirm Exit\n\nDo you want to quit the game?")
+ .setPositiveButton("Quit Game", new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ NativeExports.CloseSystem();
+ finish();
+ }
+ })
+ .setNegativeButton("Play On", new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ NativeExports.ExternalEvent( SystemEvent.SysEvent_ResumeCPU_AppGainedActive.getValue());
+ }
+ })
+ .show();
+ }
+
+ @Override
+ protected void onCreate( Bundle savedInstanceState )
+ {
+ mLifecycleHandler = new GameLifecycleHandler( this );
+ mLifecycleHandler.onCreateBegin( savedInstanceState );
+ super.onCreate( savedInstanceState );
+ mLifecycleHandler.onCreateEnd( savedInstanceState );
+
+ mMenuHandler = new GameMenuHandler( this, mLifecycleHandler );
+ }
+
+ @Override
+ protected void onStart()
+ {
+ super.onStart();
+ mLifecycleHandler.onStart();
+ }
+
+ @Override
+ protected void onResume()
+ {
+ super.onResume();
+ mLifecycleHandler.onResume();
+ }
+
+ @Override
+ protected void onPause()
+ {
+ super.onPause();
+ mLifecycleHandler.onPause();
+ }
+
+ @Override
+ protected void onStop()
+ {
+ super.onStop();
+ mLifecycleHandler.onStop();
+ }
+
+ @Override
+ protected void onDestroy()
+ {
+ super.onDestroy();
+ mLifecycleHandler.onDestroy();
+ }
+}
diff --git a/Android/src/emu/project64/game/GameActivityXperiaPlay.java b/Android/src/emu/project64/game/GameActivityXperiaPlay.java
new file mode 100644
index 000000000..a050a3194
--- /dev/null
+++ b/Android/src/emu/project64/game/GameActivityXperiaPlay.java
@@ -0,0 +1,19 @@
+/****************************************************************************
+* *
+* Project 64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.game;
+
+import android.annotation.TargetApi;
+import android.app.NativeActivity;
+
+@TargetApi( 9 )
+public class GameActivityXperiaPlay extends NativeActivity
+{
+}
diff --git a/Android/src/emu/project64/game/GameLifecycleHandler.java b/Android/src/emu/project64/game/GameLifecycleHandler.java
new file mode 100644
index 000000000..679c06082
--- /dev/null
+++ b/Android/src/emu/project64/game/GameLifecycleHandler.java
@@ -0,0 +1,347 @@
+/****************************************************************************
+ * *
+ * Project 64 - A Nintendo 64 emulator. *
+ * http://www.pj64-emu.com/ *
+ * Copyright (C) 2012 Project64. All rights reserved. *
+ * *
+ * License: *
+ * GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+ * *
+ ****************************************************************************/
+package emu.project64.game;
+
+import java.lang.ref.WeakReference;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+import java.util.ArrayList;
+import java.util.Set;
+
+import emu.project64.AndroidDevice;
+import emu.project64.R;
+import emu.project64.input.AbstractController;
+import emu.project64.input.TouchController;
+import emu.project64.input.map.VisibleTouchMap;
+import emu.project64.jni.NativeExports;
+import emu.project64.jni.NativeXperiaTouchpad;
+import emu.project64.jni.SettingsID;
+import emu.project64.jni.SystemEvent;
+import emu.project64.jni.UISettingID;
+import emu.project64.persistent.ConfigFile;
+import emu.project64.profile.Profile;
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Bundle;
+import android.os.Vibrator;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager.LayoutParams;
+
+public class GameLifecycleHandler implements SurfaceHolder.Callback, GameSurface.SurfaceInfo
+{
+ private final static boolean LOG_GAMELIFECYCLEHANDLER = true;
+
+ // Activity and views
+ private Activity mActivity;
+ private GameSurface mSurface;
+ private GameOverlay mOverlay;
+ private final ArrayList mControllers;
+ private VisibleTouchMap mTouchscreenMap;
+ // Internal flags
+ private final boolean mIsXperiaPlay;
+ private boolean mStarted = false;
+ private boolean mStopped = false;
+
+ // Lifecycle state tracking
+ private boolean mIsFocused = false; // true if the window is focused
+ private boolean mIsResumed = false; // true if the activity is resumed
+ private boolean mIsSurface = false; // true if the surface is available
+
+ public GameLifecycleHandler(Activity activity)
+ {
+ mActivity = activity;
+ mControllers = new ArrayList();
+ mIsXperiaPlay = !(activity instanceof GameActivity);
+ }
+
+ @TargetApi(11)
+ public void onCreateBegin(Bundle savedInstanceState)
+ {
+ if (LOG_GAMELIFECYCLEHANDLER)
+ {
+ Log.i("GameLifecycleHandler", "onCreateBegin");
+ }
+
+ // For Honeycomb, let the action bar overlay the rendered view (rather
+ // than squeezing it)
+ // For earlier APIs, remove the title bar to yield more space
+ Window window = mActivity.getWindow();
+ window.requestFeature(AndroidDevice.IS_ACTION_BAR_AVAILABLE ? Window.FEATURE_ACTION_BAR_OVERLAY : Window.FEATURE_NO_TITLE);
+
+ // Enable full-screen mode
+ window.setFlags(LayoutParams.FLAG_FULLSCREEN, LayoutParams.FLAG_FULLSCREEN);
+
+ // Keep screen from going to sleep
+ window.setFlags(LayoutParams.FLAG_KEEP_SCREEN_ON, LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+ // Set the screen orientation
+ mActivity.setRequestedOrientation(NativeExports.UISettingsLoadDword(UISettingID.Screen_Orientation.getValue()));
+ }
+
+ @TargetApi(11)
+ public void onCreateEnd(Bundle savedInstanceState)
+ {
+ if (LOG_GAMELIFECYCLEHANDLER)
+ {
+ Log.i("GameLifecycleHandler", "onCreateEnd");
+ }
+
+ // Take control of the GameSurface if necessary
+ if (mIsXperiaPlay)
+ {
+ mActivity.getWindow().takeSurface(null);
+ }
+
+ // Lay out content and get the views
+ mActivity.setContentView(R.layout.game_activity);
+ mSurface = (GameSurface) mActivity.findViewById( R.id.gameSurface );
+ mOverlay = (GameOverlay) mActivity.findViewById(R.id.gameOverlay);
+
+ // Listen to game surface events (created, changed, destroyed)
+ mSurface.getHolder().addCallback( this );
+ mSurface.createGLContext((ActivityManager)mActivity.getSystemService(Context.ACTIVITY_SERVICE));
+
+ // Configure the action bar introduced in higher Android versions
+ if (AndroidDevice.IS_ACTION_BAR_AVAILABLE)
+ {
+ mActivity.getActionBar().hide();
+ ColorDrawable color = new ColorDrawable(Color.parseColor("#303030"));
+ color.setAlpha(50 /*mGlobalPrefs.displayActionBarTransparency*/);
+ mActivity.getActionBar().setBackgroundDrawable(color);
+ }
+
+ boolean isFpsEnabled = false; //mGlobalPrefs.isFpsEnabled
+ boolean isTouchscreenAnimated = false; //mGlobalPrefs.isTouchscreenAnimated
+ boolean isTouchscreenHidden = false; //!isTouchscreenEnabled || globalPrefs.touchscreenTransparency == 0;
+ String profilesDir = AndroidDevice.PACKAGE_DIRECTORY + "/profiles";
+ String touchscreenProfiles_cfg = profilesDir + "/touchscreen.cfg";
+ ConfigFile touchscreenConfigFile = new ConfigFile( touchscreenProfiles_cfg );
+ //SharedPreferences mPreferences = context.getSharedPreferences( sharedPrefsName, Context.MODE_PRIVATE );
+ Profile touchscreenProfile = new Profile( true, touchscreenConfigFile.get( "Analog")); //loadProfile( /*mPreferences*/ null, "touchscreenProfile", "Analog", touchscreenProfiles_cfg,touchscreenProfiles_cfg );
+ int touchscreenTransparency = 100;
+ String touchscreenSkinsDir = AndroidDevice.PACKAGE_DIRECTORY + "/skins/touchscreen";
+ String touchscreenSkin = touchscreenSkinsDir + "/Outline";
+ float touchscreenScale = 1.0f; //( (float) mPreferences.getInt( "touchscreenScale", 100 ) ) / 100.0f;
+
+ // The touch map and overlay are needed to display frame rate and/or controls
+ mTouchscreenMap = new VisibleTouchMap( mActivity.getResources() );
+ mTouchscreenMap.load(touchscreenSkin, touchscreenProfile,
+ isTouchscreenAnimated, isFpsEnabled,
+ touchscreenScale, touchscreenTransparency );
+ mOverlay.initialize( mTouchscreenMap, !isTouchscreenHidden, isFpsEnabled, isTouchscreenAnimated );
+
+ // Initialize user interface devices
+ View inputSource = mIsXperiaPlay ? new NativeXperiaTouchpad(mActivity) : mOverlay;
+ initControllers(inputSource);
+ }
+
+ public void onStart()
+ {
+ if (LOG_GAMELIFECYCLEHANDLER)
+ {
+ Log.i("GameLifecycleHandler", "onStart");
+ }
+ }
+
+ public void onResume()
+ {
+ if (LOG_GAMELIFECYCLEHANDLER)
+ {
+ Log.i("GameLifecycleHandler", "onResume");
+ }
+ mIsResumed = true;
+ tryRunning();
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder)
+ {
+ if (LOG_GAMELIFECYCLEHANDLER)
+ {
+ Log.i("GameLifecycleHandler", "surfaceCreated");
+ }
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
+ {
+ if (LOG_GAMELIFECYCLEHANDLER)
+ {
+ Log.i("GameLifecycleHandler", "surfaceChanged");
+ }
+ mIsSurface = true;
+ tryRunning();
+ }
+
+ public void onWindowFocusChanged(boolean hasFocus)
+ {
+ if (LOG_GAMELIFECYCLEHANDLER)
+ {
+ Log.i("GameLifecycleHandler", "onWindowFocusChanged: " + hasFocus);
+ }
+ // Only try to run; don't try to pause. User may just be touching the
+ // in-game menu.
+ mIsFocused = hasFocus;
+ if (hasFocus)
+ {
+ tryRunning();
+ }
+ }
+
+ public void AutoSave()
+ {
+ if (LOG_GAMELIFECYCLEHANDLER)
+ {
+ Log.i("GameLifecycleHandler", "OnAutoSave");
+ }
+ if (NativeExports.SettingsLoadBool(SettingsID.GameRunning_CPU_Running.getValue()) == true)
+ {
+ int CurrentSaveState = NativeExports.SettingsLoadDword(SettingsID.Game_CurrentSaveState.getValue());
+ int OriginalSaveTime = NativeExports.SettingsLoadDword(SettingsID.Game_LastSaveTime.getValue());
+ NativeExports.SettingsSaveDword(SettingsID.Game_CurrentSaveState.getValue(), 0);
+ NativeExports.ExternalEvent(SystemEvent.SysEvent_SaveMachineState.getValue());
+ for (int i = 0; i < 100; i++)
+ {
+ int LastSaveTime = NativeExports.SettingsLoadDword(SettingsID.Game_LastSaveTime.getValue());
+ if (LastSaveTime != OriginalSaveTime)
+ {
+ break;
+ }
+ try
+ {
+ Thread.sleep(100);
+ }
+ catch(InterruptedException ex)
+ {
+ Thread.currentThread().interrupt();
+ }
+ }
+ NativeExports.SettingsSaveDword(SettingsID.Game_CurrentSaveState.getValue(), CurrentSaveState);
+ }
+ else if (LOG_GAMELIFECYCLEHANDLER)
+ {
+ Log.i("GameLifecycleHandler", "CPU not running, not doing anything");
+ }
+
+ if (LOG_GAMELIFECYCLEHANDLER)
+ {
+ Log.i("GameLifecycleHandler", "OnAutoSave Done");
+ }
+ }
+
+ public void onPause()
+ {
+ if (LOG_GAMELIFECYCLEHANDLER)
+ {
+ Log.i("GameLifecycleHandler", "onPause");
+ }
+ AutoSave();
+ mIsResumed = false;
+ mStopped = true;
+ if (LOG_GAMELIFECYCLEHANDLER)
+ {
+ Log.i("GameLifecycleHandler", "Stop Emulation");
+ }
+ NativeExports.StopEmulation();
+ if (LOG_GAMELIFECYCLEHANDLER)
+ {
+ Log.i("GameLifecycleHandler", "onPause - done");
+ }
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder)
+ {
+ if (LOG_GAMELIFECYCLEHANDLER)
+ {
+ Log.i("GameLifecycleHandler", "surfaceDestroyed");
+ }
+ mIsSurface = false;
+ }
+
+ public void onStop()
+ {
+ if (LOG_GAMELIFECYCLEHANDLER)
+ {
+ Log.i("GameLifecycleHandler", "onStop");
+ }
+ }
+
+ public void onDestroy()
+ {
+ if (LOG_GAMELIFECYCLEHANDLER)
+ {
+ Log.i("GameLifecycleHandler", "onDestroy");
+ }
+ }
+
+ @SuppressLint("InlinedApi")
+ private void initControllers(View inputSource)
+ {
+ // By default, send Player 1 rumbles through phone vibrator
+ Vibrator vibrator = (Vibrator) mActivity.getSystemService( Context.VIBRATOR_SERVICE );
+ int touchscreenAutoHold = 0;
+ boolean isTouchscreenFeedbackEnabled = false;
+ Set autoHoldableButtons = null;
+
+ // Create the touchscreen controller
+ TouchController touchscreenController = new TouchController( mTouchscreenMap,
+ inputSource, mOverlay, vibrator, touchscreenAutoHold,
+ isTouchscreenFeedbackEnabled, autoHoldableButtons );
+ mControllers.add( touchscreenController );
+ }
+
+ private void tryRunning()
+ {
+ if (mIsFocused && mIsResumed && mIsSurface && mStopped)
+ {
+ mStopped = false;
+ NativeExports.StartEmulation();
+ }
+ if (mIsFocused && mIsResumed && mIsSurface && !mStarted)
+ {
+ mStarted = true;
+ final GameLifecycleHandler handler = this;
+
+ NativeExports.StartGame(mActivity, new GameSurface.GLThread(new WeakReference(mSurface), handler));
+ }
+ }
+
+ @Override
+ public void onSurfaceCreated(GL10 gl, EGLConfig config)
+ {
+ if (LOG_GAMELIFECYCLEHANDLER)
+ {
+ Log.i("GameLifecycleHandler", "onSurfaceCreated");
+ }
+ NativeExports.onSurfaceCreated();
+ }
+
+ @Override
+ public void onSurfaceChanged(GL10 gl, int width, int height)
+ {
+ if (LOG_GAMELIFECYCLEHANDLER)
+ {
+ Log.i("GameLifecycleHandler", "onSurfaceChanged");
+ }
+ NativeExports.onSurfaceChanged(width, height);
+ }
+}
diff --git a/Android/src/emu/project64/game/GameMenuHandler.java b/Android/src/emu/project64/game/GameMenuHandler.java
new file mode 100644
index 000000000..563649ace
--- /dev/null
+++ b/Android/src/emu/project64/game/GameMenuHandler.java
@@ -0,0 +1,187 @@
+/****************************************************************************
+* *
+* Project 64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.game;
+
+import java.io.File;
+import java.sql.Date;
+import java.text.SimpleDateFormat;
+
+import emu.project64.R;
+import emu.project64.jni.NativeExports;
+import emu.project64.jni.SettingsID;
+import emu.project64.jni.SystemEvent;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.PopupMenu;
+
+public class GameMenuHandler implements PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener
+{
+ private Activity mActivity = null;
+ private GameLifecycleHandler mLifecycleHandler = null;
+ private Boolean mOpeningSubmenu = false;
+
+ public GameMenuHandler( Activity activity, GameLifecycleHandler LifecycleHandler )
+ {
+ mActivity = activity;
+ mLifecycleHandler = LifecycleHandler;
+
+ final ImageButton MenuButton = (ImageButton)activity.findViewById( R.id.gameMenu );
+ final Activity activityContext = activity;
+ final GameMenuHandler menuHandler = this;
+ MenuButton.setOnClickListener(new View.OnClickListener()
+ {
+ @Override
+ public void onClick(View view)
+ {
+ Boolean GamePaused = NativeExports.SettingsLoadBool(SettingsID.GameRunning_CPU_Paused.getValue());
+
+ NativeExports.ExternalEvent( SystemEvent.SysEvent_PauseCPU_AppLostActive.getValue());
+
+ PopupMenu popupMenu = new PopupMenu(activityContext, MenuButton);
+ popupMenu.setOnDismissListener(menuHandler);
+ popupMenu.setOnMenuItemClickListener(menuHandler);
+ popupMenu.inflate(R.menu.game_activity);
+
+ int CurrentSaveState = NativeExports.SettingsLoadDword(SettingsID.Game_CurrentSaveState.getValue());
+ Menu menu = popupMenu.getMenu();
+
+ menu.findItem(R.id.menuItem_pause).setVisible(GamePaused ? false : true);
+ menu.findItem(R.id.menuItem_resume).setVisible(GamePaused ? true : false);
+
+ String SaveDirectory = NativeExports.SettingsLoadString(SettingsID.Directory_InstantSave.getValue());
+ if ( NativeExports.SettingsLoadBool(SettingsID.Setting_UniqueSaveDir.getValue()))
+ {
+ SaveDirectory += "/" + NativeExports.SettingsLoadString(SettingsID.Game_UniqueSaveDir.getValue());
+ }
+
+ FixSaveStateMenu(SaveDirectory, CurrentSaveState, menu, R.id.menuItem_CurrentSaveAuto, 0);
+ FixSaveStateMenu(SaveDirectory, CurrentSaveState, menu, R.id.menuItem_CurrentSave1, 1);
+ FixSaveStateMenu(SaveDirectory, CurrentSaveState, menu, R.id.menuItem_CurrentSave2, 2);
+ FixSaveStateMenu(SaveDirectory, CurrentSaveState, menu, R.id.menuItem_CurrentSave3, 3);
+ FixSaveStateMenu(SaveDirectory, CurrentSaveState, menu, R.id.menuItem_CurrentSave4, 4);
+ FixSaveStateMenu(SaveDirectory, CurrentSaveState, menu, R.id.menuItem_CurrentSave5, 5);
+ FixSaveStateMenu(SaveDirectory, CurrentSaveState, menu, R.id.menuItem_CurrentSave6, 6);
+ FixSaveStateMenu(SaveDirectory, CurrentSaveState, menu, R.id.menuItem_CurrentSave7, 7);
+ FixSaveStateMenu(SaveDirectory, CurrentSaveState, menu, R.id.menuItem_CurrentSave8, 8);
+ FixSaveStateMenu(SaveDirectory, CurrentSaveState, menu, R.id.menuItem_CurrentSave9, 9);
+ FixSaveStateMenu(SaveDirectory, CurrentSaveState, menu, R.id.menuItem_CurrentSave10, 10);
+ popupMenu.show();
+ }
+ });
+ }
+
+ public boolean onMenuItemClick(MenuItem item)
+ {
+ switch (item.getItemId())
+ {
+ case R.id.menuItem_CurrentSaveState:
+ mOpeningSubmenu = true;
+ break;
+ case R.id.menuItem_SaveState:
+ NativeExports.ExternalEvent(SystemEvent.SysEvent_SaveMachineState.getValue());
+ break;
+ case R.id.menuItem_LoadState:
+ NativeExports.ExternalEvent(SystemEvent.SysEvent_LoadMachineState.getValue());
+ break;
+ case R.id.menuItem_CurrentSaveAuto:
+ NativeExports.SettingsSaveDword(SettingsID.Game_CurrentSaveState.getValue(), 0);
+ break;
+ case R.id.menuItem_CurrentSave1:
+ NativeExports.SettingsSaveDword(SettingsID.Game_CurrentSaveState.getValue(), 1);
+ break;
+ case R.id.menuItem_CurrentSave2:
+ NativeExports.SettingsSaveDword(SettingsID.Game_CurrentSaveState.getValue(), 2);
+ break;
+ case R.id.menuItem_CurrentSave3:
+ NativeExports.SettingsSaveDword(SettingsID.Game_CurrentSaveState.getValue(), 3);
+ break;
+ case R.id.menuItem_CurrentSave4:
+ NativeExports.SettingsSaveDword(SettingsID.Game_CurrentSaveState.getValue(), 4);
+ break;
+ case R.id.menuItem_CurrentSave5:
+ NativeExports.SettingsSaveDword(SettingsID.Game_CurrentSaveState.getValue(), 5);
+ break;
+ case R.id.menuItem_CurrentSave6:
+ NativeExports.SettingsSaveDword(SettingsID.Game_CurrentSaveState.getValue(), 6);
+ break;
+ case R.id.menuItem_CurrentSave7:
+ NativeExports.SettingsSaveDword(SettingsID.Game_CurrentSaveState.getValue(), 7);
+ break;
+ case R.id.menuItem_CurrentSave8:
+ NativeExports.SettingsSaveDword(SettingsID.Game_CurrentSaveState.getValue(), 8);
+ break;
+ case R.id.menuItem_CurrentSave9:
+ NativeExports.SettingsSaveDword(SettingsID.Game_CurrentSaveState.getValue(), 9);
+ break;
+ case R.id.menuItem_CurrentSave10:
+ NativeExports.SettingsSaveDword(SettingsID.Game_CurrentSaveState.getValue(), 10);
+ break;
+ case R.id.menuItem_pause:
+ NativeExports.ExternalEvent( SystemEvent.SysEvent_PauseCPU_FromMenu.getValue());
+ break;
+ case R.id.menuItem_resume:
+ NativeExports.ExternalEvent( SystemEvent.SysEvent_ResumeCPU_FromMenu.getValue());
+ break;
+ case R.id.menuItem_HardReset:
+ NativeExports.ExternalEvent( SystemEvent.SysEvent_ResetCPU_Hard.getValue());
+ break;
+ case R.id.menuItem_EndEmulation:
+ NativeExports.ExternalEvent( SystemEvent.SysEvent_ResumeCPU_FromMenu.getValue());
+ mLifecycleHandler.AutoSave();
+ NativeExports.CloseSystem();
+ mActivity.finish();
+ break;
+ }
+ return false;
+ }
+
+ public void onDismiss (PopupMenu menu)
+ {
+ if (!mOpeningSubmenu)
+ {
+ NativeExports.ExternalEvent( SystemEvent.SysEvent_ResumeCPU_AppGainedActive.getValue());
+ }
+ mOpeningSubmenu = false;
+ }
+
+ @SuppressLint("SimpleDateFormat")
+ private void FixSaveStateMenu(String SaveDirectory, int CurrentSaveState,Menu menu, int MenuId, int SaveSlot )
+ {
+ MenuItem item = menu.findItem(MenuId);
+ if (CurrentSaveState == SaveSlot)
+ {
+ item.setChecked(true);
+ }
+ String SaveFileName = SaveDirectory + "/" + NativeExports.SettingsLoadString(SettingsID.Game_GoodName.getValue()) + ".pj";
+ String Timestamp = "";
+ if (SaveSlot != 0)
+ {
+ SaveFileName += SaveSlot;
+ }
+ File SaveFile = new File(SaveFileName+".zip");
+ long LastModified = SaveFile.lastModified();
+ if (LastModified == 0)
+ {
+ SaveFile = new File(SaveFileName);
+ LastModified = SaveFile.lastModified();
+ }
+ if (LastModified != 0)
+ {
+ Timestamp = new SimpleDateFormat(" [yyyy/MM/dd HH:mm]").format(new Date(LastModified));
+ }
+ String SlotName = SaveSlot == 0 ? "Auto" : "Slot " + SaveSlot;
+ item.setTitle(SlotName + Timestamp);
+ }
+}
diff --git a/Android/src/emu/project64/game/GameOverlay.java b/Android/src/emu/project64/game/GameOverlay.java
new file mode 100644
index 000000000..e9455fed8
--- /dev/null
+++ b/Android/src/emu/project64/game/GameOverlay.java
@@ -0,0 +1,99 @@
+/****************************************************************************
+* *
+* Project 64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.game;
+
+import emu.project64.input.TouchController;
+import emu.project64.input.map.VisibleTouchMap;
+import emu.project64.util.DeviceUtil;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.View;
+
+public class GameOverlay extends View implements TouchController.OnStateChangedListener
+{
+ private VisibleTouchMap mTouchMap;
+ private boolean mDrawingEnabled = true;
+ private int mHatRefreshPeriod = 0;
+ private int mHatRefreshCount = 0;
+
+ public GameOverlay( Context context, AttributeSet attribs )
+ {
+ super( context, attribs );
+ requestFocus();
+ }
+
+ public void initialize( VisibleTouchMap touchMap, boolean drawingEnabled, boolean fpsEnabled, boolean joystickAnimated )
+ {
+ mTouchMap = touchMap;
+ mDrawingEnabled = drawingEnabled;
+ mHatRefreshPeriod = joystickAnimated ? 3 : 0;
+ }
+
+ @Override
+ public void onAnalogChanged( float axisFractionX, float axisFractionY )
+ {
+ if( mHatRefreshPeriod > 0 && mDrawingEnabled )
+ {
+ // Increment the count since last refresh
+ mHatRefreshCount++;
+
+ // If stick re-centered, always refresh
+ if( axisFractionX == 0 && axisFractionY == 0 )
+ mHatRefreshCount = 0;
+
+ // Update the analog stick assets and redraw if required
+ if( mHatRefreshCount % mHatRefreshPeriod == 0 && mTouchMap != null
+ && mTouchMap.updateAnalog( axisFractionX, axisFractionY ) )
+ {
+ postInvalidate();
+ }
+ }
+ }
+
+ @Override
+ public void onAutoHold( boolean autoHold, int index )
+ {
+ // Update the AutoHold mask, and redraw if required
+ if( mTouchMap != null && mTouchMap.updateAutoHold( autoHold , index) )
+ {
+ postInvalidate();
+ }
+ }
+
+ @Override
+ protected void onSizeChanged( int w, int h, int oldw, int oldh )
+ {
+ // Recompute skin layout geometry
+ if( mTouchMap != null )
+ mTouchMap.resize( w, h, DeviceUtil.getDisplayMetrics( this ) );
+ super.onSizeChanged( w, h, oldw, oldh );
+ }
+
+ @Override
+ protected void onDraw( Canvas canvas )
+ {
+ if( mTouchMap == null || canvas == null )
+ return;
+
+ if( mDrawingEnabled )
+ {
+ // Redraw the static buttons
+ mTouchMap.drawButtons( canvas );
+
+ // Redraw the dynamic analog stick
+ mTouchMap.drawAnalog( canvas );
+
+ // Redraw the autoHold mask
+ mTouchMap.drawAutoHold( canvas );
+ }
+ }
+}
diff --git a/Android/src/emu/project64/game/GameSurface.java b/Android/src/emu/project64/game/GameSurface.java
new file mode 100644
index 000000000..91f1f1df2
--- /dev/null
+++ b/Android/src/emu/project64/game/GameSurface.java
@@ -0,0 +1,1739 @@
+/****************************************************************************
+* *
+* Project 64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.game;
+
+import java.io.Writer;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGL11;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLContext;
+import javax.microedition.khronos.egl.EGLDisplay;
+import javax.microedition.khronos.egl.EGLSurface;
+import javax.microedition.khronos.opengles.GL;
+import javax.microedition.khronos.opengles.GL10;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.pm.ConfigurationInfo;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+
+public class GameSurface extends SurfaceView implements SurfaceHolder.Callback
+{
+ public class EGL14
+ {
+ public static final int EGL_OPENGL_ES2_BIT = 0x0004;
+ }
+
+ public class EGLExt
+ {
+ public static final int EGL_OPENGL_ES3_BIT_KHR = 0x0040;
+ }
+
+ private final static boolean LOG_THREADS = false;
+ private final static boolean LOG_SURFACE = false;
+ private final static boolean LOG_EGL = false;
+
+ /**
+ * Check glError() after every GL call and throw an exception if glError indicates
+ * that an error has occurred. This can be used to help track down which OpenGL ES call
+ * is causing an error.
+ *
+ * @see #getDebugFlags
+ * @see #setDebugFlags
+ */
+ public final static int DEBUG_CHECK_GL_ERROR = 1;
+
+ /**
+ * Log GL calls to the system log at "verbose" level with tag "GLSurfaceView".
+ *
+ * @see #getDebugFlags
+ * @see #setDebugFlags
+ */
+ public final static int DEBUG_LOG_GL_CALLS = 2;
+
+ // LogCat strings for debugging, defined here to simplify maintenance/lookup
+ private static final String TAG = "GameSurface";
+
+ public interface SurfaceInfo
+ {
+ /**
+ * Called when the surface is created or recreated.
+ *
+ * Called when the rendering thread
+ * starts and whenever the EGL context is lost. The EGL context will typically
+ * be lost when the Android device awakes after going to sleep.
+ *
+ * Since this method is called at the beginning of rendering, as well as
+ * every time the EGL context is lost, this method is a convenient place to put
+ * code to create resources that need to be created when the rendering
+ * starts, and that need to be recreated when the EGL context is lost.
+ * Textures are an example of a resource that you might want to create
+ * here.
+ *
+ * Note that when the EGL context is lost, all OpenGL resources associated
+ * with that context will be automatically deleted. You do not need to call
+ * the corresponding "glDelete" methods such as glDeleteTextures to
+ * manually delete these lost resources.
+ *
+ * @param mGl the GL interface. Use instanceof
to
+ * test if the interface supports GL11 or higher interfaces.
+ * @param config the EGLConfig of the created surface. Can be used
+ * to create matching pbuffers.
+ */
+ void onSurfaceCreated(GL10 mGl, EGLConfig config);
+
+ /**
+ * Called when the surface changed size.
+ *
+ * Called after the surface is created and whenever
+ * the OpenGL ES surface size changes.
+ *
+ * Typically you will set your viewport here. If your camera
+ * is fixed then you could also set your projection matrix here:
+ *
+ * void onSurfaceChanged(GL10 mGl, int width, int height) {
+ * mGl.glViewport(0, 0, width, height);
+ * // for a fixed camera, set the projection too
+ * float ratio = (float) width / height;
+ * mGl.glMatrixMode(GL10.GL_PROJECTION);
+ * mGl.glLoadIdentity();
+ * mGl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
+ * }
+ *
+ * @param mGl the GL interface. Use instanceof
to
+ * test if the interface supports GL11 or higher interfaces.
+ * @param width
+ * @param height
+ */
+ void onSurfaceChanged(GL10 mGl, int width, int height);
+ }
+
+ /**
+ * Constructor that is called when inflating a view from XML. This is called when a view is
+ * being constructed from an XML file, supplying attributes that were specified in the XML file.
+ * This version uses a default style of 0, so the only attribute values applied are those in the
+ * Context's Theme and the given AttributeSet. The method onFinishInflate() will be called after
+ * all children have been added.
+ *
+ * @param context The Context the view is running in, through which it can access the current
+ * theme, resources, etc.
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ */
+ public GameSurface( Context context, AttributeSet attribs )
+ {
+ super( context, attribs );
+ init();
+ }
+
+ @Override
+ protected void finalize() throws Throwable
+ {
+ super.finalize();
+ }
+
+ private void init()
+ {
+ // Install a SurfaceHolder.Callback so we get notified when the
+ // underlying surface is created and destroyed
+ SurfaceHolder holder = getHolder();
+ holder.addCallback(this);
+ // setFormat is done by SurfaceView in SDK 2.3 and newer. Uncomment
+ // this statement if back-porting to 2.2 or older:
+ // holder.setFormat(PixelFormat.RGB_565);
+ //
+ // setType is not needed for SDK 2.0 or newer. Uncomment this
+ // statement if back-porting this code to older SDKs.
+ // holder.setType(SurfaceHolder.SURFACE_TYPE_GPU);
+ if (mEGLConfigChooser == null)
+ {
+ mEGLConfigChooser = new SimpleEGLConfigChooser(true);
+ }
+ if (mEGLContextFactory == null)
+ {
+ mEGLContextFactory = new DefaultContextFactory();
+ }
+ if (mEGLWindowSurfaceFactory == null)
+ {
+ mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory();
+ }
+ mEglHelper = new EglHelper(new WeakReference(this));
+ }
+
+ public boolean createGLContext( ActivityManager activityManager )
+ {
+ ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
+
+ final boolean supportsEs2 = configurationInfo.reqGlEsVersion >= 0x20000 || isProbablyEmulator();
+ if (supportsEs2)
+ {
+ if (isProbablyEmulator())
+ {
+ // Avoids crashes on startup with some emulator images.
+ this.setEGLConfigChooser(8, 8, 8, 8, 16, 0);
+ }
+
+ this.setEGLContextClientVersion(2);
+ }
+ else
+ {
+ // Should never be seen in production, since the manifest filters
+ // unsupported devices.
+ Log.e(TAG, "This device does not support OpenGL ES 2.0.");
+ return false;
+ }
+ return true;
+ }
+
+ private boolean isProbablyEmulator()
+ {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1
+ && (Build.FINGERPRINT.startsWith("generic")
+ || Build.FINGERPRINT.startsWith("unknown")
+ || Build.MODEL.contains("google_sdk")
+ || Build.MODEL.contains("Emulator")
+ || Build.MODEL.contains("Android SDK built for x86"));
+ }
+
+ public boolean ableToDraw()
+ {
+ return mHaveEglContext && mHaveEglSurface && readyToDraw();
+ }
+
+ boolean readyToDraw()
+ {
+ return (!mPaused) && mHasSurface && (!mSurfaceIsBad)
+ && (mWidth > 0) && (mHeight > 0);
+ }
+
+ /**
+ * Install a custom EGLWindowSurfaceFactory.
+ * If this method is
+ * called, it must be called before {@link #setRenderer(Renderer)}
+ * is called.
+ *
+ * If this method is not called, then by default
+ * a window surface will be created with a null attribute list.
+ */
+ public void setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory factory)
+ {
+ mEGLWindowSurfaceFactory = factory;
+ }
+
+ /**
+ * Install a custom EGLConfigChooser.
+ *
If this method is
+ * called, it must be called before {@link #setRenderer(Renderer)}
+ * is called.
+ *
+ * If no setEGLConfigChooser method is called, then by default the
+ * view will choose an EGLConfig that is compatible with the current
+ * android.view.Surface, with a depth buffer depth of
+ * at least 16 bits.
+ * @param configChooser
+ */
+ public void setEGLConfigChooser(EGLConfigChooser configChooser)
+ {
+ mEGLConfigChooser = configChooser;
+ }
+
+ /**
+ * Install a config chooser which will choose a config
+ * with at least the specified depthSize and stencilSize,
+ * and exactly the specified redSize, greenSize, blueSize and alphaSize.
+ *
If this method is
+ * called, it must be called before {@link #setRenderer(Renderer)}
+ * is called.
+ *
+ * If no setEGLConfigChooser method is called, then by default the
+ * view will choose an RGB_888 surface with a depth buffer depth of
+ * at least 16 bits.
+ *
+ */
+ public void setEGLConfigChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize)
+ {
+ setEGLConfigChooser(new ComponentSizeChooser(redSize, greenSize, blueSize, alphaSize, depthSize, stencilSize));
+ }
+
+ /**
+ * Inform the default EGLContextFactory and default EGLConfigChooser
+ * which EGLContext client version to pick.
+ *
Use this method to create an OpenGL ES 2.0-compatible context.
+ * Example:
+ *
+ * public MyView(Context context) {
+ * super(context);
+ * setEGLContextClientVersion(2); // Pick an OpenGL ES 2.0 context.
+ * setRenderer(new MyRenderer());
+ * }
+ *
+ * Note: Activities which require OpenGL ES 2.0 should indicate this by
+ * setting @lt;uses-feature android:glEsVersion="0x00020000" /> in the activity's
+ * AndroidManifest.xml file.
+ *
If this method is called, it must be called before {@link #setRenderer(Renderer)}
+ * is called.
+ *
This method only affects the behavior of the default EGLContexFactory and the
+ * default EGLConfigChooser. If
+ * {@link #setEGLContextFactory(EGLContextFactory)} has been called, then the supplied
+ * EGLContextFactory is responsible for creating an OpenGL ES 2.0-compatible context.
+ * If
+ * {@link #setEGLConfigChooser(EGLConfigChooser)} has been called, then the supplied
+ * EGLConfigChooser is responsible for choosing an OpenGL ES 2.0-compatible config.
+ * @param version The EGLContext client version to choose. Use 2 for OpenGL ES 2.0
+ */
+ public void setEGLContextClientVersion(int version)
+ {
+ mEGLContextClientVersion = version;
+ }
+ /**
+ * This method is part of the SurfaceHolder.Callback interface, and is
+ * not normally called or subclassed by clients of GLSurfaceView.
+ */
+ public void surfaceCreated(SurfaceHolder holder)
+ {
+ synchronized(sGLThreadManager)
+ {
+ if (LOG_THREADS)
+ {
+ Log.i("GLThread", "surfaceCreated tid=" + getId());
+ }
+ mHasSurface = true;
+ mFinishedCreatingEglSurface = false;
+ sGLThreadManager.notifyAll();
+ while (mWaitingForSurface && !mFinishedCreatingEglSurface && !mExited)
+ {
+ try
+ {
+ sGLThreadManager.wait();
+ }
+ catch (InterruptedException e)
+ {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ }
+
+ /**
+ * This method is part of the SurfaceHolder.Callback interface, and is
+ * not normally called or subclassed by clients of GLSurfaceView.
+ */
+ public void surfaceDestroyed(SurfaceHolder holder)
+ {
+ synchronized(sGLThreadManager)
+ {
+ if (LOG_THREADS)
+ {
+ Log.i("GLThread", "surfaceDestroyed tid=" + getId());
+ }
+ mHasSurface = false;
+ sGLThreadManager.notifyAll();
+ while((!mWaitingForSurface) && (!mExited))
+ {
+ try
+ {
+ sGLThreadManager.wait();
+ }
+ catch (InterruptedException e)
+ {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ }
+
+ /**
+ * This method is part of the SurfaceHolder.Callback interface, and is
+ * not normally called or subclassed by clients of GLSurfaceView.
+ */
+ public void surfaceChanged(SurfaceHolder holder, int format, int w, int h)
+ {
+ synchronized (sGLThreadManager)
+ {
+ mWidth = w;
+ mHeight = h;
+ mSizeChanged = true;
+ mRequestRender = true;
+ mRenderComplete = false;
+ sGLThreadManager.notifyAll();
+
+ // Wait for thread to react to resize and render a frame
+ while (! mExited && !mPaused && !mRenderComplete && ableToDraw())
+ {
+ if (LOG_SURFACE) {
+ Log.i("Main thread", "onWindowResize waiting for render complete from tid=" + getId());
+ }
+ try
+ {
+ sGLThreadManager.wait();
+ }
+ catch (InterruptedException ex)
+ {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ }
+
+ /**
+ * An interface used to wrap a GL interface.
+ *
Typically
+ * used for implementing debugging and tracing on top of the default
+ * GL interface. You would typically use this by creating your own class
+ * that implemented all the GL methods by delegating to another GL instance.
+ * Then you could add your own behavior before or after calling the
+ * delegate. All the GLWrapper would do was instantiate and return the
+ * wrapper GL instance:
+ *
+ * class MyGLWrapper implements GLWrapper {
+ * GL wrap(GL gl) {
+ * return new MyGLImplementation(gl);
+ * }
+ * static class MyGLImplementation implements GL,GL10,GL11,... {
+ * ...
+ * }
+ * }
+ *
+ * @see #setGLWrapper(GLWrapper)
+ */
+ public interface GLWrapper
+ {
+ /**
+ * Wraps a gl interface in another gl interface.
+ * @param gl a GL interface that is to be wrapped.
+ * @return either the input argument or another GL object that wraps the input argument.
+ */
+ GL wrap(GL gl);
+ }
+
+ /**
+ * An EGL helper class.
+ */
+ static class EglHelper
+ {
+ public EglHelper(WeakReference glSurfaceViewWeakRef)
+ {
+ mGLSurfaceViewWeakRef = glSurfaceViewWeakRef;
+ }
+
+ /**
+ * Initialize EGL for a given configuration spec.
+ * @param configSpec
+ */
+ public void start()
+ {
+ if (LOG_EGL)
+ {
+ Log.w("EglHelper", "start() tid=" + Thread.currentThread().getId());
+ }
+ /*
+ * Get an EGL instance
+ */
+ mEgl = (EGL10) EGLContext.getEGL();
+
+ /*
+ * Get to the default display.
+ */
+ mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+
+ if (mEglDisplay == EGL10.EGL_NO_DISPLAY)
+ {
+ throw new RuntimeException("eglGetDisplay failed");
+ }
+
+ /*
+ * We can now initialize EGL for that display
+ */
+ int[] version = new int[2];
+ if(!mEgl.eglInitialize(mEglDisplay, version))
+ {
+ throw new RuntimeException("eglInitialize failed");
+ }
+ GameSurface view = mGLSurfaceViewWeakRef.get();
+ if (view == null)
+ {
+ mEglConfig = null;
+ mEglContext = null;
+ }
+ else
+ {
+ mEglConfig = view.mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay);
+
+ /*
+ * Create an EGL context. We want to do this as rarely as we can, because an
+ * EGL context is a somewhat heavy object.
+ */
+ mEglContext = view.mEGLContextFactory.createContext(mEgl, mEglDisplay, mEglConfig);
+ }
+ if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT)
+ {
+ mEglContext = null;
+ throwEglException("createContext");
+ }
+ if (LOG_EGL)
+ {
+ Log.w("EglHelper", "createContext " + mEglContext + " tid=" + Thread.currentThread().getId());
+ }
+
+ mEglSurface = null;
+ }
+
+ /**
+ * Create an egl surface for the current SurfaceHolder surface. If a surface
+ * already exists, destroy it before creating the new surface.
+ *
+ * @return true if the surface was created successfully.
+ */
+ public boolean createSurface()
+ {
+ if (LOG_EGL)
+ {
+ Log.w("EglHelper", "createSurface() tid=" + Thread.currentThread().getId());
+ }
+ /*
+ * Check preconditions.
+ */
+ if (mEgl == null)
+ {
+ throw new RuntimeException("egl not initialized");
+ }
+ if (mEglDisplay == null)
+ {
+ throw new RuntimeException("eglDisplay not initialized");
+ }
+ if (mEglConfig == null)
+ {
+ throw new RuntimeException("mEglConfig not initialized");
+ }
+
+ /*
+ * The window size has changed, so we need to create a new
+ * surface.
+ */
+ destroySurfaceImp();
+
+ /*
+ * Create an EGL surface we can render into.
+ */
+ GameSurface view = mGLSurfaceViewWeakRef.get();
+ if (view != null)
+ {
+ mEglSurface = view.mEGLWindowSurfaceFactory.createWindowSurface(mEgl, mEglDisplay, mEglConfig, view.getHolder());
+ }
+ else
+ {
+ mEglSurface = null;
+ }
+
+ if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE)
+ {
+ int error = mEgl.eglGetError();
+ if (error == EGL10.EGL_BAD_NATIVE_WINDOW)
+ {
+ Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW.");
+ }
+ return false;
+ }
+
+ /*
+ * Before we can issue GL commands, we need to make sure
+ * the context is current and bound to a surface.
+ */
+ if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext))
+ {
+ /*
+ * Could not make the context current, probably because the underlying
+ * SurfaceView surface has been destroyed.
+ */
+ logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError());
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Create a GL object for the current EGL context.
+ * @return
+ */
+ GL createGL()
+ {
+ GL gl = mEglContext.getGL();
+ GameSurface view = mGLSurfaceViewWeakRef.get();
+ if (view != null)
+ {
+ if (view.mGLWrapper != null)
+ {
+ gl = view.mGLWrapper.wrap(gl);
+ }
+ }
+ return gl;
+ }
+
+ /**
+ * Display the current render surface.
+ * @return the EGL error code from eglSwapBuffers.
+ */
+ public int swap()
+ {
+ if (! mEgl.eglSwapBuffers(mEglDisplay, mEglSurface))
+ {
+ return mEgl.eglGetError();
+ }
+ return EGL10.EGL_SUCCESS;
+ }
+
+ public void destroySurface()
+ {
+ if (LOG_EGL)
+ {
+ Log.w("EglHelper", "destroySurface() tid=" + Thread.currentThread().getId());
+ }
+ destroySurfaceImp();
+ }
+
+ private void destroySurfaceImp()
+ {
+ if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE)
+ {
+ mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
+ GameSurface view = mGLSurfaceViewWeakRef.get();
+ if (view != null)
+ {
+ view.mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface);
+ }
+ mEglSurface = null;
+ }
+ }
+
+ public void finish()
+ {
+ if (LOG_EGL)
+ {
+ Log.w("EglHelper", "finish() tid=" + Thread.currentThread().getId());
+ }
+ if (mEglContext != null)
+ {
+ GameSurface view = mGLSurfaceViewWeakRef.get();
+ if (view != null)
+ {
+ view.mEGLContextFactory.destroyContext(mEgl, mEglDisplay, mEglContext);
+ }
+ mEglContext = null;
+ }
+ if (mEglDisplay != null)
+ {
+ mEgl.eglTerminate(mEglDisplay);
+ mEglDisplay = null;
+ }
+ }
+
+ private void throwEglException(String function)
+ {
+ throwEglException(function, mEgl.eglGetError());
+ }
+
+ public static void throwEglException(String function, int error)
+ {
+ String message = formatEglError(function, error);
+ if (LOG_THREADS)
+ {
+ Log.e("EglHelper", "throwEglException tid=" + Thread.currentThread().getId() + " " + message);
+ }
+ throw new RuntimeException(message);
+ }
+
+ public static void logEglErrorAsWarning(String tag, String function, int error)
+ {
+ Log.w(tag, formatEglError(function, error));
+ }
+
+ public static String formatEglError(String function, int error)
+ {
+ return function + " failed: " + error;
+ }
+
+ private WeakReference mGLSurfaceViewWeakRef;
+ EGL10 mEgl;
+ EGLDisplay mEglDisplay;
+ EGLSurface mEglSurface;
+ EGLConfig mEglConfig;
+ EGLContext mEglContext;
+ }
+
+ /**
+ * An interface for customizing the eglCreateContext and eglDestroyContext calls.
+ *
+ * This interface must be implemented by clients wishing to call
+ * {@link GLSurfaceView#setEGLContextFactory(EGLContextFactory)}
+ */
+ public interface EGLContextFactory
+ {
+ EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig);
+ void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context);
+ }
+
+ private class DefaultContextFactory implements EGLContextFactory
+ {
+ private int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
+
+ public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config)
+ {
+ int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion, EGL10.EGL_NONE };
+
+ return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, mEGLContextClientVersion != 0 ? attrib_list : null);
+ }
+
+ public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context)
+ {
+ if (!egl.eglDestroyContext(display, context))
+ {
+ Log.e("DefaultContextFactory", "display:" + display + " context: " + context);
+ if (LOG_THREADS)
+ {
+ Log.i("DefaultContextFactory", "tid=" + Thread.currentThread().getId());
+ }
+ String message = "eglDestroyContex failed: " + egl.eglGetError();
+ if (LOG_THREADS)
+ {
+ Log.e("EglHelper", "throwEglException tid=" + Thread.currentThread().getId() + " " + message);
+ }
+ throw new RuntimeException(message);
+ }
+ }
+ }
+
+ /**
+ * An interface for customizing the eglCreateWindowSurface and eglDestroySurface calls.
+ *
+ * This interface must be implemented by clients wishing to call
+ * {@link GLSurfaceView#setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory)}
+ */
+ public interface EGLWindowSurfaceFactory
+ {
+ /**
+ * @return null if the surface cannot be constructed.
+ */
+ EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow);
+ void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface);
+ }
+
+ private static class DefaultWindowSurfaceFactory implements EGLWindowSurfaceFactory
+ {
+
+ public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow)
+ {
+ EGLSurface result = null;
+ try
+ {
+ result = egl.eglCreateWindowSurface(display, config, nativeWindow, null);
+ }
+ catch (IllegalArgumentException e)
+ {
+ // This exception indicates that the surface flinger surface
+ // is not valid. This can happen if the surface flinger surface has
+ // been torn down, but the application has not yet been
+ // notified via SurfaceHolder.Callback.surfaceDestroyed.
+ // In theory the application should be notified first,
+ // but in practice sometimes it is not. See b/4588890
+ Log.e(TAG, "eglCreateWindowSurface", e);
+ }
+ return result;
+ }
+
+ public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface)
+ {
+ egl.eglDestroySurface(display, surface);
+ }
+ }
+
+ /**
+ * An interface for choosing an EGLConfig configuration from a list of
+ * potential configurations.
+ *
+ * This interface must be implemented by clients wishing to call
+ * {@link GLSurfaceView#setEGLConfigChooser(EGLConfigChooser)}
+ */
+ public interface EGLConfigChooser
+ {
+ /**
+ * Choose a configuration from the list. Implementors typically
+ * implement this method by calling
+ * {@link EGL10#eglChooseConfig} and iterating through the results. Please consult the
+ * EGL specification available from The Khronos Group to learn how to call eglChooseConfig.
+ * @param egl the EGL10 for the current display.
+ * @param display the current display.
+ * @return the chosen configuration.
+ */
+ EGLConfig chooseConfig(EGL10 egl, EGLDisplay display);
+ }
+
+ private abstract class BaseConfigChooser implements EGLConfigChooser
+ {
+ public BaseConfigChooser(int[] configSpec)
+ {
+ mConfigSpec = filterConfigSpec(configSpec);
+ }
+
+ public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display)
+ {
+ int[] num_config = new int[1];
+ if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, num_config))
+ {
+ throw new IllegalArgumentException("eglChooseConfig failed");
+ }
+
+ int numConfigs = num_config[0];
+
+ if (numConfigs <= 0)
+ {
+ throw new IllegalArgumentException( "No configs match configSpec");
+ }
+
+ EGLConfig[] configs = new EGLConfig[numConfigs];
+ if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, num_config))
+ {
+ throw new IllegalArgumentException("eglChooseConfig#2 failed");
+ }
+ EGLConfig config = chooseConfig(egl, display, configs);
+ if (config == null)
+ {
+ throw new IllegalArgumentException("No config chosen");
+ }
+ return config;
+ }
+
+ abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs);
+
+ protected int[] mConfigSpec;
+
+ private int[] filterConfigSpec(int[] configSpec)
+ {
+ if (mEGLContextClientVersion != 2 && mEGLContextClientVersion != 3)
+ {
+ return configSpec;
+ }
+ /* We know none of the subclasses define EGL_RENDERABLE_TYPE.
+ * And we know the configSpec is well formed.
+ */
+ int len = configSpec.length;
+ int[] newConfigSpec = new int[len + 2];
+ System.arraycopy(configSpec, 0, newConfigSpec, 0, len-1);
+ newConfigSpec[len-1] = EGL10.EGL_RENDERABLE_TYPE;
+ if (mEGLContextClientVersion == 2)
+ {
+ newConfigSpec[len] = EGL14.EGL_OPENGL_ES2_BIT; /* EGL_OPENGL_ES2_BIT */
+ }
+ else
+ {
+ newConfigSpec[len] = EGLExt.EGL_OPENGL_ES3_BIT_KHR; /* EGL_OPENGL_ES3_BIT_KHR */
+ }
+ newConfigSpec[len+1] = EGL10.EGL_NONE;
+ return newConfigSpec;
+ }
+ }
+
+ /**
+ * Choose a configuration with exactly the specified r,g,b,a sizes,
+ * and at least the specified depth and stencil sizes.
+ */
+ private class ComponentSizeChooser extends BaseConfigChooser
+ {
+ public ComponentSizeChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize)
+ {
+ super(new int[]
+ {
+ EGL10.EGL_RED_SIZE, redSize,
+ EGL10.EGL_GREEN_SIZE, greenSize,
+ EGL10.EGL_BLUE_SIZE, blueSize,
+ EGL10.EGL_ALPHA_SIZE, alphaSize,
+ EGL10.EGL_DEPTH_SIZE, depthSize,
+ EGL10.EGL_STENCIL_SIZE, stencilSize,
+ EGL10.EGL_NONE
+ });
+ mValue = new int[1];
+ mRedSize = redSize;
+ mGreenSize = greenSize;
+ mBlueSize = blueSize;
+ mAlphaSize = alphaSize;
+ mDepthSize = depthSize;
+ mStencilSize = stencilSize;
+ }
+
+ @Override
+ public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs)
+ {
+ for (EGLConfig config : configs)
+ {
+ int d = findConfigAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE, 0);
+ int s = findConfigAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE, 0);
+ if ((d >= mDepthSize) && (s >= mStencilSize))
+ {
+ int r = findConfigAttrib(egl, display, config, EGL10.EGL_RED_SIZE, 0);
+ int g = findConfigAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE, 0);
+ int b = findConfigAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE, 0);
+ int a = findConfigAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE, 0);
+ if ((r == mRedSize) && (g == mGreenSize) && (b == mBlueSize) && (a == mAlphaSize))
+ {
+ return config;
+ }
+ }
+ }
+ return null;
+ }
+
+ private int findConfigAttrib(EGL10 egl, EGLDisplay display, EGLConfig config, int attribute, int defaultValue)
+ {
+ if (egl.eglGetConfigAttrib(display, config, attribute, mValue))
+ {
+ return mValue[0];
+ }
+ return defaultValue;
+ }
+
+ private int[] mValue;
+ // Subclasses can adjust these values:
+ protected int mRedSize;
+ protected int mGreenSize;
+ protected int mBlueSize;
+ protected int mAlphaSize;
+ protected int mDepthSize;
+ protected int mStencilSize;
+ }
+
+ /**
+ * This class will choose a RGB_888 surface with
+ * or without a depth buffer.
+ *
+ */
+ private class SimpleEGLConfigChooser extends ComponentSizeChooser
+ {
+ public SimpleEGLConfigChooser(boolean withDepthBuffer)
+ {
+ super(8, 8, 8, 0, withDepthBuffer ? 16 : 0, 0);
+ }
+ }
+
+ static class LogWriter extends Writer
+ {
+ @Override
+ public void close()
+ {
+ flushBuilder();
+ }
+
+ @Override
+ public void flush()
+ {
+ flushBuilder();
+ }
+
+ @Override
+ public void write(char[] buf, int offset, int count)
+ {
+ for(int i = 0; i < count; i++)
+ {
+ char c = buf[offset + i];
+ if ( c == '\n')
+ {
+ flushBuilder();
+ }
+ else
+ {
+ mBuilder.append(c);
+ }
+ }
+ }
+
+ private void flushBuilder()
+ {
+ if (mBuilder.length() > 0)
+ {
+ Log.v("GLSurfaceView", mBuilder.toString());
+ mBuilder.delete(0, mBuilder.length());
+ }
+ }
+
+ private StringBuilder mBuilder = new StringBuilder();
+ }
+
+ static class GLThreadManager
+ {
+ private static String TAG = "GLThreadManager";
+
+ public synchronized void threadExiting(GLThread thread)
+ {
+ if (LOG_THREADS)
+ {
+ Log.i("GLThread", "exiting tid=" + Thread.currentThread().getId());
+ }
+ if (mEglOwner == thread)
+ {
+ mEglOwner = null;
+ }
+ notifyAll();
+ }
+
+ /*
+ * Tries once to acquire the right to use an EGL
+ * context. Does not block. Requires that we are already
+ * in the sGLThreadManager monitor when this is called.
+ *
+ * @return true if the right to use an EGL context was acquired.
+ */
+ public boolean tryAcquireEglContextLocked(GLThread thread)
+ {
+ if (mEglOwner == thread || mEglOwner == null)
+ {
+ mEglOwner = thread;
+ notifyAll();
+ return true;
+ }
+ checkGLESVersion();
+ if (mMultipleGLESContextsAllowed)
+ {
+ return true;
+ }
+ // Notify the owning thread that it should release the context.
+ // TODO: implement a fairness policy. Currently
+ // if the owning thread is drawing continuously it will just
+ // reacquire the EGL context.
+ if (mEglOwner != null)
+ {
+ mEglOwner.requestReleaseEglContextLocked();
+ }
+ return false;
+ }
+
+ /*
+ * Releases the EGL context. Requires that we are already in the
+ * sGLThreadManager monitor when this is called.
+ */
+ public void releaseEglContextLocked(GLThread thread)
+ {
+ if (mEglOwner == thread) {
+ mEglOwner = null;
+ }
+ notifyAll();
+ }
+
+ public synchronized boolean shouldReleaseEGLContextWhenPausing()
+ {
+ // Release the EGL context when pausing even if
+ // the hardware supports multiple EGL contexts.
+ // Otherwise the device could run out of EGL contexts.
+ return mLimitedGLESContexts;
+ }
+
+ public synchronized boolean shouldTerminateEGLWhenPausing()
+ {
+ checkGLESVersion();
+ return !mMultipleGLESContextsAllowed;
+ }
+
+ public synchronized void checkGLDriver(GL10 gl)
+ {
+ if (! mGLESDriverCheckComplete)
+ {
+ checkGLESVersion();
+ String renderer = gl.glGetString(GL10.GL_RENDERER);
+ if (mGLESVersion < kGLES_20)
+ {
+ mMultipleGLESContextsAllowed = ! renderer.startsWith(kMSM7K_RENDERER_PREFIX);
+ notifyAll();
+ }
+ mLimitedGLESContexts = !mMultipleGLESContextsAllowed;
+ if (LOG_SURFACE)
+ {
+ Log.w(TAG, "checkGLDriver renderer = \"" + renderer + "\" multipleContextsAllowed = " + mMultipleGLESContextsAllowed + " mLimitedGLESContexts = " + mLimitedGLESContexts);
+ }
+ mGLESDriverCheckComplete = true;
+ }
+ }
+
+ private void checkGLESVersion()
+ {
+ if (! mGLESVersionCheckComplete)
+ {
+ try
+ {
+ Class> SP = Class.forName("android.os.SystemProperties");
+ mGLESVersion = (Integer)SP.getMethod("getInt", String.class, int.class).invoke(null, "ro.opengles.version", ConfigurationInfo.GL_ES_VERSION_UNDEFINED);
+ if (mGLESVersion >= kGLES_20)
+ {
+ mMultipleGLESContextsAllowed = true;
+ }
+ }
+ catch (Exception e)
+ {
+ }
+ if (LOG_SURFACE)
+ {
+ Log.w(TAG, "checkGLESVersion mGLESVersion =" + " " + mGLESVersion + " mMultipleGLESContextsAllowed = " + mMultipleGLESContextsAllowed);
+ }
+ mGLESVersionCheckComplete = true;
+ }
+ }
+
+ /**
+ * This check was required for some pre-Android-3.0 hardware. Android 3.0 provides
+ * support for hardware-accelerated views, therefore multiple EGL contexts are
+ * supported on all Android 3.0+ EGL drivers.
+ */
+ private boolean mGLESVersionCheckComplete;
+ private int mGLESVersion;
+ private boolean mGLESDriverCheckComplete;
+ private boolean mMultipleGLESContextsAllowed;
+ private boolean mLimitedGLESContexts;
+ private static final int kGLES_20 = 0x20000;
+ private static final String kMSM7K_RENDERER_PREFIX = "Q3Dimension MSM7500 ";
+ private GLThread mEglOwner;
+ }
+
+ /**
+ * A generic GL Thread. Takes care of initializing EGL and GL. Delegates
+ * to a Renderer instance to do the actual drawing. Can be configured to
+ * render continuously or on request.
+ *
+ * All potentially blocking synchronization is done through the
+ * sGLThreadManager object. This avoids multiple-lock ordering issues.
+ *
+ */
+ public static class GLThread
+ {
+ private final static boolean LOG_THREADS = false;
+ private final static boolean LOG_SURFACE = false;
+ private final static boolean LOG_PAUSE_RESUME = false;
+ private final static boolean LOG_RENDERER = false;
+
+ /**
+ * Check glError() after every GL call and throw an exception if glError indicates
+ * that an error has occurred. This can be used to help track down which OpenGL ES call
+ * is causing an error.
+ *
+ * @see #getDebugFlags
+ * @see #setDebugFlags
+ */
+ public final static int DEBUG_CHECK_GL_ERROR = 1;
+
+ /**
+ * Log GL calls to the system log at "verbose" level with tag "GLSurfaceView".
+ *
+ * @see #getDebugFlags
+ * @see #setDebugFlags
+ */
+ public final static int DEBUG_LOG_GL_CALLS = 2;
+
+ public GLThread(WeakReference glSurfaceViewWeakRef, GameSurface.SurfaceInfo surfaceInfo)
+ {
+ super();
+ glSurfaceViewWeakRef.get().mRequestRender = true;
+ mGLSurfaceViewWeakRef = glSurfaceViewWeakRef;
+ mSurfaceInfo = surfaceInfo;
+ }
+
+ public void ThreadStarting()
+ {
+ Log.i("GLThread", "ThreadStarting: start tid=" + Thread.currentThread().getId());
+ if (LOG_THREADS)
+ {
+ Log.i("GLThread", "starting tid=" + Thread.currentThread().getId());
+ }
+ GameSurface view = mGLSurfaceViewWeakRef.get();
+ view.mHaveEglContext = false;
+ view.mHaveEglSurface = false;
+ ReadyToDraw();
+ if (LOG_THREADS)
+ {
+ Log.i("GLThread", "starting tid=" + Thread.currentThread().getId());
+ }
+ Log.i("GLThread", "ThreadStarting: done tid=" + Thread.currentThread().getId());
+ }
+
+ public void ThreadExiting()
+ {
+ Log.i("GLThread", "ThreadExiting: start tid=" + Thread.currentThread().getId());
+ /*
+ * clean-up everything...
+ */
+ GameSurface.sGLThreadManager.threadExiting(this);
+ GameSurface view = mGLSurfaceViewWeakRef.get();
+ view.mExited = true;
+ synchronized (GameSurface.sGLThreadManager)
+ {
+ stopEglSurfaceLocked();
+ stopEglContextLocked();
+ }
+ Log.i("GLThread", "ThreadExiting: done tid=" + Thread.currentThread().getId());
+ }
+
+ /*
+ * This private method should only be called inside a
+ * synchronized(sGLThreadManager) block.
+ */
+ private void stopEglSurfaceLocked()
+ {
+ GameSurface view = mGLSurfaceViewWeakRef.get();
+ if (view.mHaveEglSurface)
+ {
+ view.mHaveEglSurface = false;
+ view.mEglHelper.destroySurface();
+ }
+ }
+
+ /*
+ * This private method should only be called inside a
+ * synchronized(sGLThreadManager) block.
+ */
+ private void stopEglContextLocked()
+ {
+ GameSurface view = mGLSurfaceViewWeakRef.get();
+ if (view.mHaveEglContext)
+ {
+ view.mEglHelper.finish();
+ view.mHaveEglContext = false;
+ GameSurface.sGLThreadManager.releaseEglContextLocked(this);
+ }
+ }
+
+ public void ReadyToDraw()
+ {
+ GameSurface view = mGLSurfaceViewWeakRef.get();
+ try
+ {
+ synchronized (GameSurface.sGLThreadManager)
+ {
+ while (true)
+ {
+ if (mShouldExit)
+ {
+ return;
+ }
+
+ if (! mEventQueue.isEmpty())
+ {
+ mEvent = mEventQueue.remove(0);
+ break;
+ }
+
+ // Update the pause state.
+ boolean pausing = false;
+ if (view.mPaused != mRequestPaused)
+ {
+ pausing = mRequestPaused;
+ view.mPaused = mRequestPaused;
+ GameSurface.sGLThreadManager.notifyAll();
+ if (LOG_PAUSE_RESUME)
+ {
+ Log.i("GLThread", "mPaused is now " + view.mPaused + " tid=" + Thread.currentThread().getId());
+ }
+ }
+
+ // Do we need to give up the EGL context?
+ if (mShouldReleaseEglContext)
+ {
+ if (LOG_SURFACE)
+ {
+ Log.i("GLThread", "releasing EGL context because asked to tid=" + Thread.currentThread().getId());
+ }
+ stopEglSurfaceLocked();
+ stopEglContextLocked();
+ mShouldReleaseEglContext = false;
+ mAskedToReleaseEglContext = true;
+ }
+
+ // Have we lost the EGL context?
+ if (mLostEglContext)
+ {
+ stopEglSurfaceLocked();
+ stopEglContextLocked();
+ mLostEglContext = false;
+ }
+
+ // When pausing, release the EGL surface:
+ if (pausing && view.mHaveEglSurface)
+ {
+ if (LOG_SURFACE)
+ {
+ Log.i("GLThread", "releasing EGL surface because paused tid=" + Thread.currentThread().getId());
+ }
+ stopEglSurfaceLocked();
+ }
+
+ // When pausing, optionally release the EGL Context:
+ if (pausing && view.mHaveEglContext)
+ {
+ boolean preserveEglContextOnPause = view == null ? false : view.mPreserveEGLContextOnPause;
+ if (!preserveEglContextOnPause || GameSurface.sGLThreadManager.shouldReleaseEGLContextWhenPausing())
+ {
+ stopEglContextLocked();
+ if (LOG_SURFACE)
+ {
+ Log.i("GLThread", "releasing EGL context because paused tid=" + Thread.currentThread().getId());
+ }
+ }
+ }
+
+ // When pausing, optionally terminate EGL:
+ if (pausing)
+ {
+ if (GameSurface.sGLThreadManager.shouldTerminateEGLWhenPausing())
+ {
+ view.mEglHelper.finish();
+ if (LOG_SURFACE)
+ {
+ Log.i("GLThread", "terminating EGL because paused tid=" + Thread.currentThread().getId());
+ }
+ }
+ }
+
+ // Have we lost the SurfaceView surface?
+ if ((!view.mHasSurface) && (!view.mWaitingForSurface))
+ {
+ if (LOG_SURFACE)
+ {
+ Log.i("GLThread", "noticed surfaceView surface lost tid=" + Thread.currentThread().getId());
+ }
+ if (view.mHaveEglSurface)
+ {
+ stopEglSurfaceLocked();
+ }
+ view.mWaitingForSurface = true;
+ view.mSurfaceIsBad = false;
+ GameSurface.sGLThreadManager.notifyAll();
+ }
+
+ // Have we acquired the surface view surface?
+ if (view.mHasSurface && view.mWaitingForSurface)
+ {
+ if (LOG_SURFACE)
+ {
+ Log.i("GLThread", "noticed surfaceView surface acquired tid=" + Thread.currentThread().getId());
+ }
+ view.mWaitingForSurface = false;
+ GameSurface.sGLThreadManager.notifyAll();
+ }
+
+ if (mDoRenderNotification)
+ {
+ if (LOG_SURFACE)
+ {
+ Log.i("GLThread", "sending render notification tid=" + Thread.currentThread().getId());
+ }
+ mWantRenderNotification = false;
+ mDoRenderNotification = false;
+ view.mRenderComplete = true;
+ GameSurface.sGLThreadManager.notifyAll();
+ }
+
+ // Ready to draw?
+ if (view.readyToDraw())
+ {
+ // If we don't have an EGL context, try to acquire one.
+ if (!view.mHaveEglContext)
+ {
+ if (mAskedToReleaseEglContext)
+ {
+ mAskedToReleaseEglContext = false;
+ }
+ else if (GameSurface.sGLThreadManager.tryAcquireEglContextLocked(this))
+ {
+ try
+ {
+ view.mEglHelper.start();
+ }
+ catch (RuntimeException t)
+ {
+ GameSurface.sGLThreadManager.releaseEglContextLocked(this);
+ throw t;
+ }
+ view.mHaveEglContext = true;
+ mCreateEglContext = true;
+
+ GameSurface.sGLThreadManager.notifyAll();
+ }
+ }
+
+ if (view.mHaveEglContext && !view.mHaveEglSurface)
+ {
+ view.mHaveEglSurface = true;
+ mCreateEglSurface = true;
+ mCreateGlInterface = true;
+ mSizeChanged = true;
+ }
+
+ if (view.mHaveEglSurface)
+ {
+ if (view.mSizeChanged)
+ {
+ mSizeChanged = true;
+ mW = view.mWidth;
+ mH = view.mHeight;
+ mWantRenderNotification = true;
+ if (LOG_SURFACE)
+ {
+ Log.i("GLThread", "noticing that we want render notification tid=" + Thread.currentThread().getId());
+ }
+
+ // Destroy and recreate the EGL surface.
+ mCreateEglSurface = true;
+
+ view.mSizeChanged = false;
+ }
+ view.mRequestRender = false;
+ GameSurface.sGLThreadManager.notifyAll();
+ break;
+ }
+ }
+
+ // By design, this is the only place in a GLThread thread where we wait().
+ if (LOG_THREADS)
+ {
+ Log.i("GLThread", "waiting tid=" + Thread.currentThread().getId()
+ + " mHaveEglContext: " + view.mHaveEglContext
+ + " mHaveEglSurface: " + view.mHaveEglSurface
+ + " mFinishedCreatingEglSurface: " + view.mFinishedCreatingEglSurface
+ + " mPaused: " + view.mPaused
+ + " mHasSurface: " + view.mHasSurface
+ + " mSurfaceIsBad: " + view.mSurfaceIsBad
+ + " mWaitingForSurface: " + view.mWaitingForSurface
+ + " mWidth: " + view.mWidth
+ + " mHeight: " + view.mHeight
+ + " mRequestRender: " + view.mRequestRender);
+ }
+
+ try
+ {
+ GameSurface.sGLThreadManager.wait();
+ }
+ catch (InterruptedException e)
+ {
+ // fall thru and exit normally
+ }
+ }
+ } // end of synchronized(sGLThreadManager)
+
+ if (mEvent != null)
+ {
+ mEvent.run();
+ mEvent = null;
+ ReadyToDraw();
+ return;
+ }
+
+ if (mCreateEglSurface)
+ {
+ if (LOG_SURFACE)
+ {
+ Log.w("GLThread", "egl createSurface");
+ }
+ if (view.mEglHelper.createSurface())
+ {
+ synchronized(GameSurface.sGLThreadManager)
+ {
+ view.mFinishedCreatingEglSurface = true;
+ GameSurface.sGLThreadManager.notifyAll();
+ }
+ }
+ else
+ {
+ synchronized(GameSurface.sGLThreadManager)
+ {
+ view.mFinishedCreatingEglSurface = true;
+ view.mSurfaceIsBad = true;
+ GameSurface.sGLThreadManager.notifyAll();
+ }
+ ReadyToDraw();
+ return;
+ }
+ mCreateEglSurface = false;
+ }
+
+ if (mCreateGlInterface)
+ {
+ mGl = (GL10) view.mEglHelper.createGL();
+
+ GameSurface.sGLThreadManager.checkGLDriver(mGl);
+ mCreateGlInterface = false;
+ }
+
+ if (mCreateEglContext)
+ {
+ if (LOG_RENDERER)
+ {
+ Log.w("GLThread", "onSurfaceCreated");
+ }
+ if (view != null)
+ {
+ try
+ {
+ //Trace.traceBegin(Trace.TRACE_TAG_VIEW, "onSurfaceCreated");
+ mSurfaceInfo.onSurfaceCreated(mGl, view.mEglHelper.mEglConfig);
+ }
+ finally
+ {
+ //Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ }
+ mCreateEglContext = false;
+ }
+
+ if (mSizeChanged)
+ {
+ if (LOG_RENDERER)
+ {
+ Log.w("GLThread", "onSurfaceChanged(" + mW + ", " + mH + ")");
+ }
+ if (view != null)
+ {
+ try
+ {
+ //Trace.traceBegin(Trace.TRACE_TAG_VIEW, "onSurfaceChanged");
+ mSurfaceInfo.onSurfaceChanged(mGl, mW, mH);
+ }
+ finally
+ {
+ //Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ }
+ mSizeChanged = false;
+ }
+ }
+ finally
+ {
+ /*
+ * clean-up everything...
+ */
+ }
+ }
+
+ public void SwapBuffers()
+ {
+ GameSurface view = mGLSurfaceViewWeakRef.get();
+ int swapError = view.mEglHelper.swap();
+ switch (swapError)
+ {
+ case EGL10.EGL_SUCCESS:
+ break;
+ case EGL11.EGL_CONTEXT_LOST:
+ if (LOG_SURFACE)
+ {
+ Log.i("GLThread", "egl context lost tid=" + Thread.currentThread().getId());
+ }
+ mLostEglContext = true;
+ break;
+ default:
+ // Other errors typically mean that the current surface is bad,
+ // probably because the SurfaceView surface has been destroyed,
+ // but we haven't been notified yet.
+ // Log the error to help developers understand why rendering stopped.
+ EglHelper.logEglErrorAsWarning("GLThread", "eglSwapBuffers", swapError);
+
+ synchronized(GameSurface.sGLThreadManager)
+ {
+ view.mSurfaceIsBad = true;
+ GameSurface.sGLThreadManager.notifyAll();
+ }
+ break;
+ }
+
+ if (mWantRenderNotification)
+ {
+ mDoRenderNotification = true;
+ }
+ ReadyToDraw();
+ }
+
+ public void requestRender()
+ {
+ GameSurface view = mGLSurfaceViewWeakRef.get();
+ synchronized(GameSurface.sGLThreadManager)
+ {
+ view.mRequestRender = true;
+ GameSurface.sGLThreadManager.notifyAll();
+ }
+ }
+
+ public void onPause()
+ {
+ Log.i("GLThread", "onPause start");
+ GameSurface view = mGLSurfaceViewWeakRef.get();
+ synchronized (GameSurface.sGLThreadManager)
+ {
+ if (LOG_PAUSE_RESUME)
+ {
+ Log.i("GLThread", "onPause tid=" + Thread.currentThread().getId());
+ }
+ mRequestPaused = true;
+ GameSurface.sGLThreadManager.notifyAll();
+ while ((! view.mExited) && (! view.mPaused))
+ {
+ if (LOG_PAUSE_RESUME)
+ {
+ Log.i("Main thread", "onPause waiting for mPaused.");
+ }
+ try
+ {
+ GameSurface.sGLThreadManager.wait();
+ }
+ catch (InterruptedException ex)
+ {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ Log.i("GLThread", "onPause done");
+ }
+
+ public void onResume()
+ {
+ Log.i("GLThread", "onResume start");
+ GameSurface view = mGLSurfaceViewWeakRef.get();
+ synchronized (GameSurface.sGLThreadManager)
+ {
+ if (LOG_PAUSE_RESUME)
+ {
+ Log.i("GLThread", "onResume tid=" + Thread.currentThread().getId());
+ }
+ mRequestPaused = false;
+ view.mRequestRender = true;
+ view.mRenderComplete = false;
+ GameSurface.sGLThreadManager.notifyAll();
+ while ((!view.mExited) && view.mPaused && (!view.mRenderComplete))
+ {
+ if (LOG_PAUSE_RESUME)
+ {
+ Log.i("Main thread", "onResume waiting for !mPaused.");
+ }
+ try
+ {
+ GameSurface.sGLThreadManager.wait();
+ }
+ catch (InterruptedException ex)
+ {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ Log.i("GLThread", "onResume Done");
+ }
+
+ public void requestExitAndWait()
+ {
+ Log.i("GLThread", "requestExitAndWait start");
+ // don't call this from GLThread thread or it is a guaranteed
+ // deadlock!
+ GameSurface view = mGLSurfaceViewWeakRef.get();
+ synchronized(GameSurface.sGLThreadManager)
+ {
+ mShouldExit = true;
+ GameSurface.sGLThreadManager.notifyAll();
+ while (!view.mExited)
+ {
+ try
+ {
+ GameSurface.sGLThreadManager.wait();
+ }
+ catch (InterruptedException ex)
+ {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ Log.i("GLThread", "requestExitAndWait exit");
+ }
+
+ public void requestReleaseEglContextLocked()
+ {
+ mShouldReleaseEglContext = true;
+ GameSurface.sGLThreadManager.notifyAll();
+ }
+
+ /**
+ * Queue an "mEvent" to be run on the GL rendering thread.
+ * @param r the runnable to be run on the GL rendering thread.
+ */
+ public void queueEvent(Runnable r)
+ {
+ Log.i("GLThread", "queueEvent start");
+ if (r == null)
+ {
+ throw new IllegalArgumentException("r must not be null");
+ }
+ synchronized(GameSurface.sGLThreadManager)
+ {
+ mEventQueue.add(r);
+ GameSurface.sGLThreadManager.notifyAll();
+ }
+ Log.i("GLThread", "queueEvent Done");
+ }
+
+ // Once the thread is started, all accesses to the following member
+ // variables are protected by the sGLThreadManager monitor
+ private boolean mShouldExit;
+ private boolean mRequestPaused;
+ private boolean mShouldReleaseEglContext;
+ private GameSurface.SurfaceInfo mSurfaceInfo;
+ private ArrayList mEventQueue = new ArrayList();
+
+ // End of member variables protected by the sGLThreadManager monitor.
+
+ /**
+ * Set once at thread construction time, nulled out when the parent view is garbage
+ * called. This weak reference allows the GLSurfaceView to be garbage collected while
+ * the GLThread is still alive.
+ */
+ private WeakReference mGLSurfaceViewWeakRef;
+
+ GL10 mGl = null;
+ boolean mCreateEglContext = false;
+ boolean mCreateEglSurface = false;
+ boolean mCreateGlInterface = false;
+ boolean mLostEglContext = false;
+ boolean mSizeChanged = false;
+ boolean mWantRenderNotification = false;
+ boolean mDoRenderNotification = false;
+ boolean mAskedToReleaseEglContext = false;
+ int mW = 0;
+ int mH = 0;
+ Runnable mEvent = null;
+ }
+
+ static final GLThreadManager sGLThreadManager = new GLThreadManager();
+
+ EGLConfigChooser mEGLConfigChooser;
+ EGLContextFactory mEGLContextFactory;
+ EGLWindowSurfaceFactory mEGLWindowSurfaceFactory;
+ GLWrapper mGLWrapper;
+ int mDebugFlags;
+ int mEGLContextClientVersion;
+ boolean mPreserveEGLContextOnPause;
+ int mWidth;
+ int mHeight;
+ boolean mSizeChanged = true;
+ boolean mRequestRender;
+ boolean mRenderComplete;
+ boolean mExited;
+ boolean mPaused;
+ boolean mHasSurface;
+ boolean mSurfaceIsBad;
+ boolean mHaveEglSurface;
+ boolean mWaitingForSurface;
+ boolean mFinishedCreatingEglSurface;
+ boolean mHaveEglContext;
+ EglHelper mEglHelper;
+}
\ No newline at end of file
diff --git a/Android/src/emu/project64/inAppPurchase/IabBroadcastReceiver.java b/Android/src/emu/project64/inAppPurchase/IabBroadcastReceiver.java
new file mode 100644
index 000000000..5cae146fb
--- /dev/null
+++ b/Android/src/emu/project64/inAppPurchase/IabBroadcastReceiver.java
@@ -0,0 +1,60 @@
+/* Copyright (c) 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package emu.project64.inAppPurchase;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * Receiver for the "com.android.vending.billing.PURCHASES_UPDATED" Action
+ * from the Play Store.
+ *
+ * It is possible that an in-app item may be acquired without the
+ * application calling getBuyIntent(), for example if the item can be
+ * redeemed from inside the Play Store using a promotional code. If this
+ * application isn't running at the time, then when it is started a call
+ * to getPurchases() will be sufficient notification. However, if the
+ * application is already running in the background when the item is acquired,
+ * a message to this BroadcastReceiver will indicate that the an item
+ * has been acquired.
+ */
+public class IabBroadcastReceiver extends BroadcastReceiver {
+ /**
+ * Listener interface for received broadcast messages.
+ */
+ public interface IabBroadcastListener {
+ void receivedBroadcast();
+ }
+
+ /**
+ * The Intent action that this Receiver should filter for.
+ */
+ public static final String ACTION = "com.android.vending.billing.PURCHASES_UPDATED";
+
+ private final IabBroadcastListener mListener;
+
+ public IabBroadcastReceiver(IabBroadcastListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (mListener != null) {
+ mListener.receivedBroadcast();
+ }
+ }
+}
diff --git a/Android/src/emu/project64/inAppPurchase/IabException.java b/Android/src/emu/project64/inAppPurchase/IabException.java
new file mode 100644
index 000000000..a7cc54e5f
--- /dev/null
+++ b/Android/src/emu/project64/inAppPurchase/IabException.java
@@ -0,0 +1,43 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package emu.project64.inAppPurchase;
+
+/**
+ * Exception thrown when something went wrong with in-app billing.
+ * An IabException has an associated IabResult (an error).
+ * To get the IAB result that caused this exception to be thrown,
+ * call {@link #getResult()}.
+ */
+public class IabException extends Exception {
+ IabResult mResult;
+
+ public IabException(IabResult r) {
+ this(r, null);
+ }
+ public IabException(int response, String message) {
+ this(new IabResult(response, message));
+ }
+ public IabException(IabResult r, Exception cause) {
+ super(r.getMessage(), cause);
+ mResult = r;
+ }
+ public IabException(int response, String message, Exception cause) {
+ this(new IabResult(response, message), cause);
+ }
+
+ /** Returns the IAB result (error) that this exception signals. */
+ public IabResult getResult() { return mResult; }
+}
\ No newline at end of file
diff --git a/Android/src/emu/project64/inAppPurchase/IabHelper.java b/Android/src/emu/project64/inAppPurchase/IabHelper.java
new file mode 100644
index 000000000..c8dea07b9
--- /dev/null
+++ b/Android/src/emu/project64/inAppPurchase/IabHelper.java
@@ -0,0 +1,1116 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package emu.project64.inAppPurchase;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender.SendIntentException;
+import android.content.ServiceConnection;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.vending.billing.IInAppBillingService;
+
+import org.json.JSONException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Provides convenience methods for in-app billing. You can create one instance of this
+ * class for your application and use it to process in-app billing operations.
+ * It provides synchronous (blocking) and asynchronous (non-blocking) methods for
+ * many common in-app billing operations, as well as automatic signature
+ * verification.
+ *
+ * After instantiating, you must perform setup in order to start using the object.
+ * To perform setup, call the {@link #startSetup} method and provide a listener;
+ * that listener will be notified when setup is complete, after which (and not before)
+ * you may call other methods.
+ *
+ * After setup is complete, you will typically want to request an inventory of owned
+ * items and subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync}
+ * and related methods.
+ *
+ * When you are done with this object, don't forget to call {@link #dispose}
+ * to ensure proper cleanup. This object holds a binding to the in-app billing
+ * service, which will leak unless you dispose of it correctly. If you created
+ * the object on an Activity's onCreate method, then the recommended
+ * place to dispose of it is the Activity's onDestroy method. It is invalid to
+ * dispose the object while an asynchronous operation is in progress. You can
+ * call {@link #disposeWhenFinished()} to ensure that any in-progress operation
+ * completes before the object is disposed.
+ *
+ * A note about threading: When using this object from a background thread, you may
+ * call the blocking versions of methods; when using from a UI thread, call
+ * only the asynchronous versions and handle the results via callbacks.
+ * Also, notice that you can only call one asynchronous operation at a time;
+ * attempting to start a second asynchronous operation while the first one
+ * has not yet completed will result in an exception being thrown.
+ *
+ */
+public class IabHelper {
+ // Is debug logging enabled?
+ boolean mDebugLog = false;
+ String mDebugTag = "IabHelper";
+
+ // Is setup done?
+ boolean mSetupDone = false;
+
+ // Has this object been disposed of? (If so, we should ignore callbacks, etc)
+ boolean mDisposed = false;
+
+ // Do we need to dispose this object after an in-progress asynchronous operation?
+ boolean mDisposeAfterAsync = false;
+
+ // Are subscriptions supported?
+ boolean mSubscriptionsSupported = false;
+
+ // Is subscription update supported?
+ boolean mSubscriptionUpdateSupported = false;
+
+ // Is an asynchronous operation in progress?
+ // (only one at a time can be in progress)
+ boolean mAsyncInProgress = false;
+
+ // Ensure atomic access to mAsyncInProgress and mDisposeAfterAsync.
+ private final Object mAsyncInProgressLock = new Object();
+
+ // (for logging/debugging)
+ // if mAsyncInProgress == true, what asynchronous operation is in progress?
+ String mAsyncOperation = "";
+
+ // Context we were passed during initialization
+ Context mContext;
+
+ // Connection to the service
+ IInAppBillingService mService;
+ ServiceConnection mServiceConn;
+
+ // The request code used to launch purchase flow
+ int mRequestCode;
+
+ // The item type of the current purchase flow
+ String mPurchasingItemType;
+
+ // Public key for verifying signature, in base64 encoding
+ String mSignatureBase64 = null;
+
+ // Billing response codes
+ public static final int BILLING_RESPONSE_RESULT_OK = 0;
+ public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
+ public static final int BILLING_RESPONSE_RESULT_SERVICE_UNAVAILABLE = 2;
+ public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
+ public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
+ public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
+ public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
+ public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
+ public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
+
+ // IAB Helper error codes
+ public static final int IABHELPER_ERROR_BASE = -1000;
+ public static final int IABHELPER_REMOTE_EXCEPTION = -1001;
+ public static final int IABHELPER_BAD_RESPONSE = -1002;
+ public static final int IABHELPER_VERIFICATION_FAILED = -1003;
+ public static final int IABHELPER_SEND_INTENT_FAILED = -1004;
+ public static final int IABHELPER_USER_CANCELLED = -1005;
+ public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006;
+ public static final int IABHELPER_MISSING_TOKEN = -1007;
+ public static final int IABHELPER_UNKNOWN_ERROR = -1008;
+ public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009;
+ public static final int IABHELPER_INVALID_CONSUMPTION = -1010;
+ public static final int IABHELPER_SUBSCRIPTION_UPDATE_NOT_AVAILABLE = -1011;
+
+ // Keys for the responses from InAppBillingService
+ public static final String RESPONSE_CODE = "RESPONSE_CODE";
+ public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST";
+ public static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
+ public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA";
+ public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE";
+ public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST";
+ public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST";
+ public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST";
+ public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
+
+ // Item types
+ public static final String ITEM_TYPE_INAPP = "inapp";
+ public static final String ITEM_TYPE_SUBS = "subs";
+
+ // some fields on the getSkuDetails response bundle
+ public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST";
+ public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST";
+
+ /**
+ * Creates an instance. After creation, it will not yet be ready to use. You must perform
+ * setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not
+ * block and is safe to call from a UI thread.
+ *
+ * @param ctx Your application or Activity context. Needed to bind to the in-app billing service.
+ * @param base64PublicKey Your application's public key, encoded in base64.
+ * This is used for verification of purchase signatures. You can find your app's base64-encoded
+ * public key in your application's page on Google Play Developer Console. Note that this
+ * is NOT your "developer public key".
+ */
+ public IabHelper(Context ctx, String base64PublicKey) {
+ mContext = ctx.getApplicationContext();
+ mSignatureBase64 = base64PublicKey;
+ logDebug("IAB helper created.");
+ }
+
+ /**
+ * Enables or disable debug logging through LogCat.
+ */
+ public void enableDebugLogging(boolean enable, String tag) {
+ checkNotDisposed();
+ mDebugLog = enable;
+ mDebugTag = tag;
+ }
+
+ public void enableDebugLogging(boolean enable) {
+ checkNotDisposed();
+ mDebugLog = enable;
+ }
+
+ /**
+ * Callback for setup process. This listener's {@link #onIabSetupFinished} method is called
+ * when the setup process is complete.
+ */
+ public interface OnIabSetupFinishedListener {
+ /**
+ * Called to notify that setup is complete.
+ *
+ * @param result The result of the setup process.
+ */
+ void onIabSetupFinished(IabResult result);
+ }
+
+ /**
+ * Starts the setup process. This will start up the setup process asynchronously.
+ * You will be notified through the listener when the setup process is complete.
+ * This method is safe to call from a UI thread.
+ *
+ * @param listener The listener to notify when the setup process is complete.
+ */
+ public void startSetup(final OnIabSetupFinishedListener listener) {
+ // If already set up, can't do it again.
+ checkNotDisposed();
+ if (mSetupDone) throw new IllegalStateException("IAB helper is already set up.");
+
+ // Connection to IAB service
+ logDebug("Starting in-app billing setup.");
+ mServiceConn = new ServiceConnection() {
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ logDebug("Billing service disconnected.");
+ mService = null;
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (mDisposed) return;
+ logDebug("Billing service connected.");
+ mService = IInAppBillingService.Stub.asInterface(service);
+ String packageName = mContext.getPackageName();
+ try {
+ logDebug("Checking for in-app billing 3 support.");
+
+ // check for in-app billing v3 support
+ int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ if (listener != null) listener.onIabSetupFinished(new IabResult(response,
+ "Error checking for billing v3 support."));
+
+ // if in-app purchases aren't supported, neither are subscriptions
+ mSubscriptionsSupported = false;
+ mSubscriptionUpdateSupported = false;
+ return;
+ } else {
+ logDebug("In-app billing version 3 supported for " + packageName);
+ }
+
+ // Check for v5 subscriptions support. This is needed for
+ // getBuyIntentToReplaceSku which allows for subscription update
+ response = mService.isBillingSupported(5, packageName, ITEM_TYPE_SUBS);
+ if (response == BILLING_RESPONSE_RESULT_OK) {
+ logDebug("Subscription re-signup AVAILABLE.");
+ mSubscriptionUpdateSupported = true;
+ } else {
+ logDebug("Subscription re-signup not available.");
+ mSubscriptionUpdateSupported = false;
+ }
+
+ if (mSubscriptionUpdateSupported) {
+ mSubscriptionsSupported = true;
+ } else {
+ // check for v3 subscriptions support
+ response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);
+ if (response == BILLING_RESPONSE_RESULT_OK) {
+ logDebug("Subscriptions AVAILABLE.");
+ mSubscriptionsSupported = true;
+ } else {
+ logDebug("Subscriptions NOT AVAILABLE. Response: " + response);
+ mSubscriptionsSupported = false;
+ mSubscriptionUpdateSupported = false;
+ }
+ }
+
+ mSetupDone = true;
+ }
+ catch (RemoteException e) {
+ if (listener != null) {
+ listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION,
+ "RemoteException while setting up in-app billing."));
+ }
+ e.printStackTrace();
+ return;
+ }
+
+ if (listener != null) {
+ listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
+ }
+ }
+ };
+
+ Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
+ serviceIntent.setPackage("com.android.vending");
+ List intentServices = mContext.getPackageManager().queryIntentServices(serviceIntent, 0);
+ if (intentServices != null && !intentServices.isEmpty()) {
+ // service available to handle that Intent
+ mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
+ }
+ else {
+ // no service available to handle that Intent
+ if (listener != null) {
+ listener.onIabSetupFinished(
+ new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,
+ "Billing service unavailable on device."));
+ }
+ }
+ }
+
+ /**
+ * Dispose of object, releasing resources. It's very important to call this
+ * method when you are done with this object. It will release any resources
+ * used by it such as service connections. Naturally, once the object is
+ * disposed of, it can't be used again.
+ */
+ public void dispose() throws IabAsyncInProgressException {
+ synchronized (mAsyncInProgressLock) {
+ if (mAsyncInProgress) {
+ throw new IabAsyncInProgressException("Can't dispose because an async operation " +
+ "(" + mAsyncOperation + ") is in progress.");
+ }
+ }
+ logDebug("Disposing.");
+ mSetupDone = false;
+ if (mServiceConn != null) {
+ logDebug("Unbinding from service.");
+ if (mContext != null) mContext.unbindService(mServiceConn);
+ }
+ mDisposed = true;
+ mContext = null;
+ mServiceConn = null;
+ mService = null;
+ mPurchaseListener = null;
+ }
+
+ /**
+ * Disposes of object, releasing resources. If there is an in-progress async operation, this
+ * method will queue the dispose to occur after the operation has finished.
+ */
+ public void disposeWhenFinished() {
+ synchronized (mAsyncInProgressLock) {
+ if (mAsyncInProgress) {
+ logDebug("Will dispose after async operation finishes.");
+ mDisposeAfterAsync = true;
+ } else {
+ try {
+ dispose();
+ } catch (IabAsyncInProgressException e) {
+ // Should never be thrown, because we call dispose() only after checking that
+ // there's not already an async operation in progress.
+ }
+ }
+ }
+ }
+
+ private void checkNotDisposed() {
+ if (mDisposed) throw new IllegalStateException("IabHelper was disposed of, so it cannot be used.");
+ }
+
+ /** Returns whether subscriptions are supported. */
+ public boolean subscriptionsSupported() {
+ checkNotDisposed();
+ return mSubscriptionsSupported;
+ }
+
+
+ /**
+ * Callback that notifies when a purchase is finished.
+ */
+ public interface OnIabPurchaseFinishedListener {
+ /**
+ * Called to notify that an in-app purchase finished. If the purchase was successful,
+ * then the sku parameter specifies which item was purchased. If the purchase failed,
+ * the sku and extraData parameters may or may not be null, depending on how far the purchase
+ * process went.
+ *
+ * @param result The result of the purchase.
+ * @param info The purchase information (null if purchase failed)
+ */
+ void onIabPurchaseFinished(IabResult result, Purchase info);
+ }
+
+ // The listener registered on launchPurchaseFlow, which we have to call back when
+ // the purchase finishes
+ OnIabPurchaseFinishedListener mPurchaseListener;
+
+ public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener)
+ throws IabAsyncInProgressException {
+ launchPurchaseFlow(act, sku, requestCode, listener, "");
+ }
+
+ public void launchPurchaseFlow(Activity act, String sku, int requestCode,
+ OnIabPurchaseFinishedListener listener, String extraData)
+ throws IabAsyncInProgressException {
+ launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, null, requestCode, listener, extraData);
+ }
+
+ public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
+ OnIabPurchaseFinishedListener listener) throws IabAsyncInProgressException {
+ launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, "");
+ }
+
+ public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
+ OnIabPurchaseFinishedListener listener, String extraData)
+ throws IabAsyncInProgressException {
+ launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, null, requestCode, listener, extraData);
+ }
+
+ /**
+ * Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase,
+ * which will involve bringing up the Google Play screen. The calling activity will be paused
+ * while the user interacts with Google Play, and the result will be delivered via the
+ * activity's {@link android.app.Activity#onActivityResult} method, at which point you must call
+ * this object's {@link #handleActivityResult} method to continue the purchase flow. This method
+ * MUST be called from the UI thread of the Activity.
+ *
+ * @param act The calling activity.
+ * @param sku The sku of the item to purchase.
+ * @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or
+ * ITEM_TYPE_SUBS)
+ * @param oldSkus A list of SKUs which the new SKU is replacing or null if there are none
+ * @param requestCode A request code (to differentiate from other responses -- as in
+ * {@link android.app.Activity#startActivityForResult}).
+ * @param listener The listener to notify when the purchase process finishes
+ * @param extraData Extra data (developer payload), which will be returned with the purchase
+ * data when the purchase completes. This extra data will be permanently bound to that
+ * purchase and will always be returned when the purchase is queried.
+ */
+ public void launchPurchaseFlow(Activity act, String sku, String itemType, List oldSkus,
+ int requestCode, OnIabPurchaseFinishedListener listener, String extraData)
+ throws IabAsyncInProgressException {
+ checkNotDisposed();
+ checkSetupDone("launchPurchaseFlow");
+ flagStartAsync("launchPurchaseFlow");
+ IabResult result;
+
+ if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) {
+ IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE,
+ "Subscriptions are not available.");
+ flagEndAsync();
+ if (listener != null) listener.onIabPurchaseFinished(r, null);
+ return;
+ }
+
+ try {
+ logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);
+ Bundle buyIntentBundle;
+ if (oldSkus == null || oldSkus.isEmpty()) {
+ // Purchasing a new item or subscription re-signup
+ buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType,
+ extraData);
+ } else {
+ // Subscription upgrade/downgrade
+ if (!mSubscriptionUpdateSupported) {
+ IabResult r = new IabResult(IABHELPER_SUBSCRIPTION_UPDATE_NOT_AVAILABLE,
+ "Subscription updates are not available.");
+ flagEndAsync();
+ if (listener != null) listener.onIabPurchaseFinished(r, null);
+ return;
+ }
+ buyIntentBundle = mService.getBuyIntentToReplaceSkus(5, mContext.getPackageName(),
+ oldSkus, sku, itemType, extraData);
+ }
+ int response = getResponseCodeFromBundle(buyIntentBundle);
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ logError("Unable to buy item, Error response: " + getResponseDesc(response));
+ flagEndAsync();
+ result = new IabResult(response, "Unable to buy item");
+ if (listener != null) listener.onIabPurchaseFinished(result, null);
+ return;
+ }
+
+ PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
+ logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);
+ mRequestCode = requestCode;
+ mPurchaseListener = listener;
+ mPurchasingItemType = itemType;
+ act.startIntentSenderForResult(pendingIntent.getIntentSender(),
+ requestCode, new Intent(),
+ Integer.valueOf(0), Integer.valueOf(0),
+ Integer.valueOf(0));
+ }
+ catch (SendIntentException e) {
+ logError("SendIntentException while launching purchase flow for sku " + sku);
+ e.printStackTrace();
+ flagEndAsync();
+
+ result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent.");
+ if (listener != null) listener.onIabPurchaseFinished(result, null);
+ }
+ catch (RemoteException e) {
+ logError("RemoteException while launching purchase flow for sku " + sku);
+ e.printStackTrace();
+ flagEndAsync();
+
+ result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow");
+ if (listener != null) listener.onIabPurchaseFinished(result, null);
+ }
+ }
+
+ /**
+ * Handles an activity result that's part of the purchase flow in in-app billing. If you
+ * are calling {@link #launchPurchaseFlow}, then you must call this method from your
+ * Activity's {@link android.app.Activity@onActivityResult} method. This method
+ * MUST be called from the UI thread of the Activity.
+ *
+ * @param requestCode The requestCode as you received it.
+ * @param resultCode The resultCode as you received it.
+ * @param data The data (Intent) as you received it.
+ * @return Returns true if the result was related to a purchase flow and was handled;
+ * false if the result was not related to a purchase, in which case you should
+ * handle it normally.
+ */
+ public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
+ IabResult result;
+ if (requestCode != mRequestCode) return false;
+
+ checkNotDisposed();
+ checkSetupDone("handleActivityResult");
+
+ // end of async purchase operation that started on launchPurchaseFlow
+ flagEndAsync();
+
+ if (data == null) {
+ logError("Null data in IAB activity result.");
+ result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ return true;
+ }
+
+ int responseCode = getResponseCodeFromIntent(data);
+ String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);
+ String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);
+
+ if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
+ logDebug("Successful resultcode from purchase activity.");
+ logDebug("Purchase data: " + purchaseData);
+ logDebug("Data signature: " + dataSignature);
+ logDebug("Extras: " + data.getExtras());
+ logDebug("Expected item type: " + mPurchasingItemType);
+
+ if (purchaseData == null || dataSignature == null) {
+ logError("BUG: either purchaseData or dataSignature is null.");
+ logDebug("Extras: " + data.getExtras().toString());
+ result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ return true;
+ }
+
+ Purchase purchase = null;
+ try {
+ purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature);
+ String sku = purchase.getSku();
+
+ // Verify signature
+ if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {
+ logError("Purchase signature verification FAILED for sku " + sku);
+ result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, purchase);
+ return true;
+ }
+ logDebug("Purchase signature successfully verified.");
+ }
+ catch (JSONException e) {
+ logError("Failed to parse purchase data.");
+ e.printStackTrace();
+ result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data.");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ return true;
+ }
+
+ if (mPurchaseListener != null) {
+ mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);
+ }
+ }
+ else if (resultCode == Activity.RESULT_OK) {
+ // result code was OK, but in-app billing response was not OK.
+ logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode));
+ if (mPurchaseListener != null) {
+ result = new IabResult(responseCode, "Problem purchashing item.");
+ mPurchaseListener.onIabPurchaseFinished(result, null);
+ }
+ }
+ else if (resultCode == Activity.RESULT_CANCELED) {
+ logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode));
+ result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled.");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ }
+ else {
+ logError("Purchase failed. Result code: " + Integer.toString(resultCode)
+ + ". Response: " + getResponseDesc(responseCode));
+ result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response.");
+ if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+ }
+ return true;
+ }
+
+ public Inventory queryInventory() throws IabException {
+ return queryInventory(false, null, null);
+ }
+
+ /**
+ * Queries the inventory. This will query all owned items from the server, as well as
+ * information on additional skus, if specified. This method may block or take long to execute.
+ * Do not call from a UI thread. For that, use the non-blocking version {@link #queryInventoryAsync}.
+ *
+ * @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well
+ * as purchase information.
+ * @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership.
+ * Ignored if null or if querySkuDetails is false.
+ * @param moreSubsSkus additional SUBSCRIPTIONS skus to query information on, regardless of ownership.
+ * Ignored if null or if querySkuDetails is false.
+ * @throws IabException if a problem occurs while refreshing the inventory.
+ */
+ public Inventory queryInventory(boolean querySkuDetails, List moreItemSkus,
+ List moreSubsSkus) throws IabException {
+ checkNotDisposed();
+ checkSetupDone("queryInventory");
+ try {
+ Inventory inv = new Inventory();
+ int r = queryPurchases(inv, ITEM_TYPE_INAPP);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying owned items).");
+ }
+
+ if (querySkuDetails) {
+ r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying prices of items).");
+ }
+ }
+
+ // if subscriptions are supported, then also query for subscriptions
+ if (mSubscriptionsSupported) {
+ r = queryPurchases(inv, ITEM_TYPE_SUBS);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying owned subscriptions).");
+ }
+
+ if (querySkuDetails) {
+ r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreSubsSkus);
+ if (r != BILLING_RESPONSE_RESULT_OK) {
+ throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions).");
+ }
+ }
+ }
+
+ return inv;
+ }
+ catch (RemoteException e) {
+ throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e);
+ }
+ catch (JSONException e) {
+ throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e);
+ }
+ }
+
+ /**
+ * Listener that notifies when an inventory query operation completes.
+ */
+ public interface QueryInventoryFinishedListener {
+ /**
+ * Called to notify that an inventory query operation completed.
+ *
+ * @param result The result of the operation.
+ * @param inv The inventory.
+ */
+ void onQueryInventoryFinished(IabResult result, Inventory inv);
+ }
+
+
+ /**
+ * Asynchronous wrapper for inventory query. This will perform an inventory
+ * query as described in {@link #queryInventory}, but will do so asynchronously
+ * and call back the specified listener upon completion. This method is safe to
+ * call from a UI thread.
+ *
+ * @param querySkuDetails as in {@link #queryInventory}
+ * @param moreItemSkus as in {@link #queryInventory}
+ * @param moreSubsSkus as in {@link #queryInventory}
+ * @param listener The listener to notify when the refresh operation completes.
+ */
+ public void queryInventoryAsync(final boolean querySkuDetails, final List moreItemSkus,
+ final List moreSubsSkus, final QueryInventoryFinishedListener listener)
+ throws IabAsyncInProgressException {
+ final Handler handler = new Handler();
+ checkNotDisposed();
+ checkSetupDone("queryInventory");
+ flagStartAsync("refresh inventory");
+ (new Thread(new Runnable() {
+ public void run() {
+ IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful.");
+ Inventory inv = null;
+ try {
+ inv = queryInventory(querySkuDetails, moreItemSkus, moreSubsSkus);
+ }
+ catch (IabException ex) {
+ result = ex.getResult();
+ }
+
+ flagEndAsync();
+
+ final IabResult result_f = result;
+ final Inventory inv_f = inv;
+ if (!mDisposed && listener != null) {
+ handler.post(new Runnable() {
+ public void run() {
+ listener.onQueryInventoryFinished(result_f, inv_f);
+ }
+ });
+ }
+ }
+ })).start();
+ }
+
+ public void queryInventoryAsync(QueryInventoryFinishedListener listener)
+ throws IabAsyncInProgressException{
+ queryInventoryAsync(false, null, null, listener);
+ }
+
+ /**
+ * Consumes a given in-app product. Consuming can only be done on an item
+ * that's owned, and as a result of consumption, the user will no longer own it.
+ * This method may block or take long to return. Do not call from the UI thread.
+ * For that, see {@link #consumeAsync}.
+ *
+ * @param itemInfo The PurchaseInfo that represents the item to consume.
+ * @throws IabException if there is a problem during consumption.
+ */
+ void consume(Purchase itemInfo) throws IabException {
+ checkNotDisposed();
+ checkSetupDone("consume");
+
+ if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) {
+ throw new IabException(IABHELPER_INVALID_CONSUMPTION,
+ "Items of type '" + itemInfo.mItemType + "' can't be consumed.");
+ }
+
+ try {
+ String token = itemInfo.getToken();
+ String sku = itemInfo.getSku();
+ if (token == null || token.equals("")) {
+ logError("Can't consume "+ sku + ". No token.");
+ throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: "
+ + sku + " " + itemInfo);
+ }
+
+ logDebug("Consuming sku: " + sku + ", token: " + token);
+ int response = mService.consumePurchase(3, mContext.getPackageName(), token);
+ if (response == BILLING_RESPONSE_RESULT_OK) {
+ logDebug("Successfully consumed sku: " + sku);
+ }
+ else {
+ logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response));
+ throw new IabException(response, "Error consuming sku " + sku);
+ }
+ }
+ catch (RemoteException e) {
+ throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e);
+ }
+ }
+
+ /**
+ * Callback that notifies when a consumption operation finishes.
+ */
+ public interface OnConsumeFinishedListener {
+ /**
+ * Called to notify that a consumption has finished.
+ *
+ * @param purchase The purchase that was (or was to be) consumed.
+ * @param result The result of the consumption operation.
+ */
+ void onConsumeFinished(Purchase purchase, IabResult result);
+ }
+
+ /**
+ * Callback that notifies when a multi-item consumption operation finishes.
+ */
+ public interface OnConsumeMultiFinishedListener {
+ /**
+ * Called to notify that a consumption of multiple items has finished.
+ *
+ * @param purchases The purchases that were (or were to be) consumed.
+ * @param results The results of each consumption operation, corresponding to each
+ * sku.
+ */
+ void onConsumeMultiFinished(List purchases, List results);
+ }
+
+ /**
+ * Asynchronous wrapper to item consumption. Works like {@link #consume}, but
+ * performs the consumption in the background and notifies completion through
+ * the provided listener. This method is safe to call from a UI thread.
+ *
+ * @param purchase The purchase to be consumed.
+ * @param listener The listener to notify when the consumption operation finishes.
+ */
+ public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener)
+ throws IabAsyncInProgressException {
+ checkNotDisposed();
+ checkSetupDone("consume");
+ List purchases = new ArrayList();
+ purchases.add(purchase);
+ consumeAsyncInternal(purchases, listener, null);
+ }
+
+ /**
+ * Same as {@link #consumeAsync}, but for multiple items at once.
+ * @param purchases The list of PurchaseInfo objects representing the purchases to consume.
+ * @param listener The listener to notify when the consumption operation finishes.
+ */
+ public void consumeAsync(List purchases, OnConsumeMultiFinishedListener listener)
+ throws IabAsyncInProgressException {
+ checkNotDisposed();
+ checkSetupDone("consume");
+ consumeAsyncInternal(purchases, null, listener);
+ }
+
+ /**
+ * Returns a human-readable description for the given response code.
+ *
+ * @param code The response code
+ * @return A human-readable string explaining the result code.
+ * It also includes the result code numerically.
+ */
+ public static String getResponseDesc(int code) {
+ String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" +
+ "3:Billing Unavailable/4:Item unavailable/" +
+ "5:Developer Error/6:Error/7:Item Already Owned/" +
+ "8:Item not owned").split("/");
+ String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" +
+ "-1002:Bad response received/" +
+ "-1003:Purchase signature verification failed/" +
+ "-1004:Send intent failed/" +
+ "-1005:User cancelled/" +
+ "-1006:Unknown purchase response/" +
+ "-1007:Missing token/" +
+ "-1008:Unknown error/" +
+ "-1009:Subscriptions not available/" +
+ "-1010:Invalid consumption attempt").split("/");
+
+ if (code <= IABHELPER_ERROR_BASE) {
+ int index = IABHELPER_ERROR_BASE - code;
+ if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index];
+ else return String.valueOf(code) + ":Unknown IAB Helper Error";
+ }
+ else if (code < 0 || code >= iab_msgs.length)
+ return String.valueOf(code) + ":Unknown";
+ else
+ return iab_msgs[code];
+ }
+
+
+ // Checks that setup was done; if not, throws an exception.
+ void checkSetupDone(String operation) {
+ if (!mSetupDone) {
+ logError("Illegal state for operation (" + operation + "): IAB helper is not set up.");
+ throw new IllegalStateException("IAB helper is not set up. Can't perform operation: " + operation);
+ }
+ }
+
+ // Workaround to bug where sometimes response codes come as Long instead of Integer
+ int getResponseCodeFromBundle(Bundle b) {
+ Object o = b.get(RESPONSE_CODE);
+ if (o == null) {
+ logDebug("Bundle with null response code, assuming OK (known issue)");
+ return BILLING_RESPONSE_RESULT_OK;
+ }
+ else if (o instanceof Integer) return ((Integer)o).intValue();
+ else if (o instanceof Long) return (int)((Long)o).longValue();
+ else {
+ logError("Unexpected type for bundle response code.");
+ logError(o.getClass().getName());
+ throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName());
+ }
+ }
+
+ // Workaround to bug where sometimes response codes come as Long instead of Integer
+ int getResponseCodeFromIntent(Intent i) {
+ Object o = i.getExtras().get(RESPONSE_CODE);
+ if (o == null) {
+ logError("Intent with no response code, assuming OK (known issue)");
+ return BILLING_RESPONSE_RESULT_OK;
+ }
+ else if (o instanceof Integer) return ((Integer)o).intValue();
+ else if (o instanceof Long) return (int)((Long)o).longValue();
+ else {
+ logError("Unexpected type for intent response code.");
+ logError(o.getClass().getName());
+ throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName());
+ }
+ }
+
+ void flagStartAsync(String operation) throws IabAsyncInProgressException {
+ synchronized (mAsyncInProgressLock) {
+ if (mAsyncInProgress) {
+ throw new IabAsyncInProgressException("Can't start async operation (" +
+ operation + ") because another async operation (" + mAsyncOperation +
+ ") is in progress.");
+ }
+ mAsyncOperation = operation;
+ mAsyncInProgress = true;
+ logDebug("Starting async operation: " + operation);
+ }
+ }
+
+ void flagEndAsync() {
+ synchronized (mAsyncInProgressLock) {
+ logDebug("Ending async operation: " + mAsyncOperation);
+ mAsyncOperation = "";
+ mAsyncInProgress = false;
+ if (mDisposeAfterAsync) {
+ try {
+ dispose();
+ } catch (IabAsyncInProgressException e) {
+ // Should not be thrown, because we reset mAsyncInProgress immediately before
+ // calling dispose().
+ }
+ }
+ }
+ }
+
+ /**
+ * Exception thrown when the requested operation cannot be started because an async operation
+ * is still in progress.
+ */
+ public static class IabAsyncInProgressException extends Exception {
+ public IabAsyncInProgressException(String message) {
+ super(message);
+ }
+ }
+
+ int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {
+ // Query purchases
+ logDebug("Querying owned items, item type: " + itemType);
+ logDebug("Package name: " + mContext.getPackageName());
+ boolean verificationFailed = false;
+ String continueToken = null;
+
+ do {
+ logDebug("Calling getPurchases with continuation token: " + continueToken);
+ Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(),
+ itemType, continueToken);
+
+ int response = getResponseCodeFromBundle(ownedItems);
+ logDebug("Owned items response: " + String.valueOf(response));
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ logDebug("getPurchases() failed: " + getResponseDesc(response));
+ return response;
+ }
+ if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST)
+ || !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST)
+ || !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) {
+ logError("Bundle returned from getPurchases() doesn't contain required fields.");
+ return IABHELPER_BAD_RESPONSE;
+ }
+
+ ArrayList ownedSkus = ownedItems.getStringArrayList(
+ RESPONSE_INAPP_ITEM_LIST);
+ ArrayList purchaseDataList = ownedItems.getStringArrayList(
+ RESPONSE_INAPP_PURCHASE_DATA_LIST);
+ ArrayList signatureList = ownedItems.getStringArrayList(
+ RESPONSE_INAPP_SIGNATURE_LIST);
+
+ for (int i = 0; i < purchaseDataList.size(); ++i) {
+ String purchaseData = purchaseDataList.get(i);
+ String signature = signatureList.get(i);
+ String sku = ownedSkus.get(i);
+ if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) {
+ logDebug("Sku is owned: " + sku);
+ Purchase purchase = new Purchase(itemType, purchaseData, signature);
+
+ if (TextUtils.isEmpty(purchase.getToken())) {
+ logWarn("BUG: empty/null token!");
+ logDebug("Purchase data: " + purchaseData);
+ }
+
+ // Record ownership and token
+ inv.addPurchase(purchase);
+ }
+ else {
+ logWarn("Purchase signature verification **FAILED**. Not adding item.");
+ logDebug(" Purchase data: " + purchaseData);
+ logDebug(" Signature: " + signature);
+ verificationFailed = true;
+ }
+ }
+
+ continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN);
+ logDebug("Continuation token: " + continueToken);
+ } while (!TextUtils.isEmpty(continueToken));
+
+ return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK;
+ }
+
+ int querySkuDetails(String itemType, Inventory inv, List moreSkus)
+ throws RemoteException, JSONException {
+ logDebug("Querying SKU details.");
+ ArrayList skuList = new ArrayList();
+ skuList.addAll(inv.getAllOwnedSkus(itemType));
+ if (moreSkus != null) {
+ for (String sku : moreSkus) {
+ if (!skuList.contains(sku)) {
+ skuList.add(sku);
+ }
+ }
+ }
+
+ if (skuList.size() == 0) {
+ logDebug("queryPrices: nothing to do because there are no SKUs.");
+ return BILLING_RESPONSE_RESULT_OK;
+ }
+
+ // Split the sku list in blocks of no more than 20 elements.
+ ArrayList> packs = new ArrayList>();
+ ArrayList tempList;
+ int n = skuList.size() / 20;
+ int mod = skuList.size() % 20;
+ for (int i = 0; i < n; i++) {
+ tempList = new ArrayList();
+ for (String s : skuList.subList(i * 20, i * 20 + 20)) {
+ tempList.add(s);
+ }
+ packs.add(tempList);
+ }
+ if (mod != 0) {
+ tempList = new ArrayList();
+ for (String s : skuList.subList(n * 20, n * 20 + mod)) {
+ tempList.add(s);
+ }
+ packs.add(tempList);
+ }
+
+ for (ArrayList skuPartList : packs) {
+ Bundle querySkus = new Bundle();
+ querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuPartList);
+ Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(),
+ itemType, querySkus);
+
+ if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) {
+ int response = getResponseCodeFromBundle(skuDetails);
+ if (response != BILLING_RESPONSE_RESULT_OK) {
+ logDebug("getSkuDetails() failed: " + getResponseDesc(response));
+ return response;
+ } else {
+ logError("getSkuDetails() returned a bundle with neither an error nor a detail list.");
+ return IABHELPER_BAD_RESPONSE;
+ }
+ }
+
+ ArrayList responseList = skuDetails.getStringArrayList(
+ RESPONSE_GET_SKU_DETAILS_LIST);
+
+ for (String thisResponse : responseList) {
+ SkuDetails d = new SkuDetails(itemType, thisResponse);
+ logDebug("Got sku details: " + d);
+ inv.addSkuDetails(d);
+ }
+ }
+
+ return BILLING_RESPONSE_RESULT_OK;
+ }
+
+ void consumeAsyncInternal(final List purchases,
+ final OnConsumeFinishedListener singleListener,
+ final OnConsumeMultiFinishedListener multiListener)
+ throws IabAsyncInProgressException {
+ final Handler handler = new Handler();
+ flagStartAsync("consume");
+ (new Thread(new Runnable() {
+ public void run() {
+ final List results = new ArrayList();
+ for (Purchase purchase : purchases) {
+ try {
+ consume(purchase);
+ results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku()));
+ }
+ catch (IabException ex) {
+ results.add(ex.getResult());
+ }
+ }
+
+ flagEndAsync();
+ if (!mDisposed && singleListener != null) {
+ handler.post(new Runnable() {
+ public void run() {
+ singleListener.onConsumeFinished(purchases.get(0), results.get(0));
+ }
+ });
+ }
+ if (!mDisposed && multiListener != null) {
+ handler.post(new Runnable() {
+ public void run() {
+ multiListener.onConsumeMultiFinished(purchases, results);
+ }
+ });
+ }
+ }
+ })).start();
+ }
+
+ void logDebug(String msg) {
+ if (mDebugLog) Log.d(mDebugTag, msg);
+ }
+
+ void logError(String msg) {
+ Log.e(mDebugTag, "In-app billing error: " + msg);
+ }
+
+ void logWarn(String msg) {
+ Log.w(mDebugTag, "In-app billing warning: " + msg);
+ }
+}
diff --git a/Android/src/emu/project64/inAppPurchase/IabResult.java b/Android/src/emu/project64/inAppPurchase/IabResult.java
new file mode 100644
index 000000000..c59df03f9
--- /dev/null
+++ b/Android/src/emu/project64/inAppPurchase/IabResult.java
@@ -0,0 +1,45 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package emu.project64.inAppPurchase;
+
+/**
+ * Represents the result of an in-app billing operation.
+ * A result is composed of a response code (an integer) and possibly a
+ * message (String). You can get those by calling
+ * {@link #getResponse} and {@link #getMessage()}, respectively. You
+ * can also inquire whether a result is a success or a failure by
+ * calling {@link #isSuccess()} and {@link #isFailure()}.
+ */
+public class IabResult {
+ int mResponse;
+ String mMessage;
+
+ public IabResult(int response, String message) {
+ mResponse = response;
+ if (message == null || message.trim().length() == 0) {
+ mMessage = IabHelper.getResponseDesc(response);
+ }
+ else {
+ mMessage = message + " (response: " + IabHelper.getResponseDesc(response) + ")";
+ }
+ }
+ public int getResponse() { return mResponse; }
+ public String getMessage() { return mMessage; }
+ public boolean isSuccess() { return mResponse == IabHelper.BILLING_RESPONSE_RESULT_OK; }
+ public boolean isFailure() { return !isSuccess(); }
+ public String toString() { return "IabResult: " + getMessage(); }
+}
+
diff --git a/Android/src/emu/project64/inAppPurchase/Inventory.java b/Android/src/emu/project64/inAppPurchase/Inventory.java
new file mode 100644
index 000000000..ccd1c5d8e
--- /dev/null
+++ b/Android/src/emu/project64/inAppPurchase/Inventory.java
@@ -0,0 +1,91 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package emu.project64.inAppPurchase;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents a block of information about in-app items.
+ * An Inventory is returned by such methods as {@link IabHelper#queryInventory}.
+ */
+public class Inventory {
+ Map mSkuMap = new HashMap();
+ Map mPurchaseMap = new HashMap();
+
+ Inventory() { }
+
+ /** Returns the listing details for an in-app product. */
+ public SkuDetails getSkuDetails(String sku) {
+ return mSkuMap.get(sku);
+ }
+
+ /** Returns purchase information for a given product, or null if there is no purchase. */
+ public Purchase getPurchase(String sku) {
+ return mPurchaseMap.get(sku);
+ }
+
+ /** Returns whether or not there exists a purchase of the given product. */
+ public boolean hasPurchase(String sku) {
+ return mPurchaseMap.containsKey(sku);
+ }
+
+ /** Return whether or not details about the given product are available. */
+ public boolean hasDetails(String sku) {
+ return mSkuMap.containsKey(sku);
+ }
+
+ /**
+ * Erase a purchase (locally) from the inventory, given its product ID. This just
+ * modifies the Inventory object locally and has no effect on the server! This is
+ * useful when you have an existing Inventory object which you know to be up to date,
+ * and you have just consumed an item successfully, which means that erasing its
+ * purchase data from the Inventory you already have is quicker than querying for
+ * a new Inventory.
+ */
+ public void erasePurchase(String sku) {
+ if (mPurchaseMap.containsKey(sku)) mPurchaseMap.remove(sku);
+ }
+
+ /** Returns a list of all owned product IDs. */
+ List getAllOwnedSkus() {
+ return new ArrayList(mPurchaseMap.keySet());
+ }
+
+ /** Returns a list of all owned product IDs of a given type */
+ List getAllOwnedSkus(String itemType) {
+ List result = new ArrayList();
+ for (Purchase p : mPurchaseMap.values()) {
+ if (p.getItemType().equals(itemType)) result.add(p.getSku());
+ }
+ return result;
+ }
+
+ /** Returns a list of all purchases. */
+ List getAllPurchases() {
+ return new ArrayList(mPurchaseMap.values());
+ }
+
+ void addSkuDetails(SkuDetails d) {
+ mSkuMap.put(d.getSku(), d);
+ }
+
+ void addPurchase(Purchase p) {
+ mPurchaseMap.put(p.getSku(), p);
+ }
+}
diff --git a/Android/src/emu/project64/inAppPurchase/Purchase.java b/Android/src/emu/project64/inAppPurchase/Purchase.java
new file mode 100644
index 000000000..62a087779
--- /dev/null
+++ b/Android/src/emu/project64/inAppPurchase/Purchase.java
@@ -0,0 +1,66 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package emu.project64.inAppPurchase;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Represents an in-app billing purchase.
+ */
+public class Purchase {
+ String mItemType; // ITEM_TYPE_INAPP or ITEM_TYPE_SUBS
+ String mOrderId;
+ String mPackageName;
+ String mSku;
+ long mPurchaseTime;
+ int mPurchaseState;
+ String mDeveloperPayload;
+ String mToken;
+ String mOriginalJson;
+ String mSignature;
+ boolean mIsAutoRenewing;
+
+ public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException {
+ mItemType = itemType;
+ mOriginalJson = jsonPurchaseInfo;
+ JSONObject o = new JSONObject(mOriginalJson);
+ mOrderId = o.optString("orderId");
+ mPackageName = o.optString("packageName");
+ mSku = o.optString("productId");
+ mPurchaseTime = o.optLong("purchaseTime");
+ mPurchaseState = o.optInt("purchaseState");
+ mDeveloperPayload = o.optString("developerPayload");
+ mToken = o.optString("token", o.optString("purchaseToken"));
+ mIsAutoRenewing = o.optBoolean("autoRenewing");
+ mSignature = signature;
+ }
+
+ public String getItemType() { return mItemType; }
+ public String getOrderId() { return mOrderId; }
+ public String getPackageName() { return mPackageName; }
+ public String getSku() { return mSku; }
+ public long getPurchaseTime() { return mPurchaseTime; }
+ public int getPurchaseState() { return mPurchaseState; }
+ public String getDeveloperPayload() { return mDeveloperPayload; }
+ public String getToken() { return mToken; }
+ public String getOriginalJson() { return mOriginalJson; }
+ public String getSignature() { return mSignature; }
+ public boolean isAutoRenewing() { return mIsAutoRenewing; }
+
+ @Override
+ public String toString() { return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson; }
+}
diff --git a/Android/src/emu/project64/inAppPurchase/Security.java b/Android/src/emu/project64/inAppPurchase/Security.java
new file mode 100644
index 000000000..5e7c4b476
--- /dev/null
+++ b/Android/src/emu/project64/inAppPurchase/Security.java
@@ -0,0 +1,121 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package emu.project64.inAppPurchase;
+
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Log;
+
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+
+/**
+ * Security-related methods. For a secure implementation, all of this code
+ * should be implemented on a server that communicates with the
+ * application on the device. For the sake of simplicity and clarity of this
+ * example, this code is included here and is executed on the device. If you
+ * must verify the purchases on the phone, you should obfuscate this code to
+ * make it harder for an attacker to replace the code with stubs that treat all
+ * purchases as verified.
+ */
+public class Security {
+ private static final String TAG = "IABUtil/Security";
+
+ private static final String KEY_FACTORY_ALGORITHM = "RSA";
+ private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
+
+ /**
+ * Verifies that the data was signed with the given signature, and returns
+ * the verified purchase. The data is in JSON format and signed
+ * with a private key. The data also contains the {@link PurchaseState}
+ * and product ID of the purchase.
+ * @param base64PublicKey the base64-encoded public key to use for verifying.
+ * @param signedData the signed JSON string (signed, not encrypted)
+ * @param signature the signature for the data, signed with the private key
+ */
+ public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
+ if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
+ TextUtils.isEmpty(signature)) {
+ Log.e(TAG, "Purchase verification failed: missing data.");
+ return false;
+ }
+
+ PublicKey key = Security.generatePublicKey(base64PublicKey);
+ return Security.verify(key, signedData, signature);
+ }
+
+ /**
+ * Generates a PublicKey instance from a string containing the
+ * Base64-encoded public key.
+ *
+ * @param encodedPublicKey Base64-encoded public key
+ * @throws IllegalArgumentException if encodedPublicKey is invalid
+ */
+ public static PublicKey generatePublicKey(String encodedPublicKey) {
+ try {
+ byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT);
+ KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
+ return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ } catch (InvalidKeySpecException e) {
+ Log.e(TAG, "Invalid key specification.");
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Verifies that the signature from the server matches the computed
+ * signature on the data. Returns true if the data is correctly signed.
+ *
+ * @param publicKey public key associated with the developer account
+ * @param signedData signed data from server
+ * @param signature server signature
+ * @return true if the data and signature match
+ */
+ public static boolean verify(PublicKey publicKey, String signedData, String signature) {
+ byte[] signatureBytes;
+ try {
+ signatureBytes = Base64.decode(signature, Base64.DEFAULT);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Base64 decoding failed.");
+ return false;
+ }
+ try {
+ Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
+ sig.initVerify(publicKey);
+ sig.update(signedData.getBytes());
+ if (!sig.verify(signatureBytes)) {
+ Log.e(TAG, "Signature verification failed.");
+ return false;
+ }
+ return true;
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(TAG, "NoSuchAlgorithmException.");
+ } catch (InvalidKeyException e) {
+ Log.e(TAG, "Invalid key specification.");
+ } catch (SignatureException e) {
+ Log.e(TAG, "Signature exception.");
+ }
+ return false;
+ }
+}
diff --git a/Android/src/emu/project64/inAppPurchase/SkuDetails.java b/Android/src/emu/project64/inAppPurchase/SkuDetails.java
new file mode 100644
index 000000000..f9da64201
--- /dev/null
+++ b/Android/src/emu/project64/inAppPurchase/SkuDetails.java
@@ -0,0 +1,64 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package emu.project64.inAppPurchase;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Represents an in-app product's listing details.
+ */
+public class SkuDetails {
+ private final String mItemType;
+ private final String mSku;
+ private final String mType;
+ private final String mPrice;
+ private final long mPriceAmountMicros;
+ private final String mPriceCurrencyCode;
+ private final String mTitle;
+ private final String mDescription;
+ private final String mJson;
+
+ public SkuDetails(String jsonSkuDetails) throws JSONException {
+ this(IabHelper.ITEM_TYPE_INAPP, jsonSkuDetails);
+ }
+
+ public SkuDetails(String itemType, String jsonSkuDetails) throws JSONException {
+ mItemType = itemType;
+ mJson = jsonSkuDetails;
+ JSONObject o = new JSONObject(mJson);
+ mSku = o.optString("productId");
+ mType = o.optString("type");
+ mPrice = o.optString("price");
+ mPriceAmountMicros = o.optLong("price_amount_micros");
+ mPriceCurrencyCode = o.optString("price_currency_code");
+ mTitle = o.optString("title");
+ mDescription = o.optString("description");
+ }
+
+ public String getSku() { return mSku; }
+ public String getType() { return mType; }
+ public String getPrice() { return mPrice; }
+ public long getPriceAmountMicros() { return mPriceAmountMicros; }
+ public String getPriceCurrencyCode() { return mPriceCurrencyCode; }
+ public String getTitle() { return mTitle; }
+ public String getDescription() { return mDescription; }
+
+ @Override
+ public String toString() {
+ return "SkuDetails:" + mJson;
+ }
+}
diff --git a/Android/src/emu/project64/input/AbstractController.java b/Android/src/emu/project64/input/AbstractController.java
new file mode 100644
index 000000000..74233a0d8
--- /dev/null
+++ b/Android/src/emu/project64/input/AbstractController.java
@@ -0,0 +1,174 @@
+/****************************************************************************
+* *
+* Project 64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.input;
+
+import java.util.ArrayList;
+
+import emu.project64.jni.NativeInput;
+
+/**
+ * The abstract base class for implementing all N64 controllers.
+ *
+ * Subclasses should implement the following pattern:
+ *
+ * - Register a listener to the upstream input (e.g. touch, keyboard, mouse, joystick, etc.).
+ * - Translate the input data into N64 controller button/axis states, and set the values of the
+ * protected fields mState.buttons and mState.axisFraction* accordingly.
+ * - Call the protected method notifyChanged().
+ *
+ * This abstract class will call the emulator's native libraries to update game state whenever
+ * notifyChanged() is called. Subclasses should not call any native methods themselves. (If they do,
+ * then this abstract class should be expanded to cover those needs.)
+ *
+ * Note that this class is stateful, in that it remembers controller button/axis state between calls
+ * from the subclass. For best performance, subclasses should only call notifyChanged() when the
+ * input state has actually changed, and should bundle the protected field modifications before
+ * calling notifyChanged(). For example,
+ *
+ *
+ * {@code
+ * buttons[0] = true; notifyChanged(); buttons[1] = false; notifyChanged(); // Inefficient
+ * buttons[0] = true; buttons[1] = false; notifyChanged(); // Better
+ * }
+ *
+ *
+ * @see PeripheralController
+ * @see TouchController
+ */
+public abstract class AbstractController
+{
+ /**
+ * A small class that encapsulates controller state.
+ */
+ protected static class State
+ {
+ /** The pressed state of each controller button. */
+ public boolean[] buttons = new boolean[NUM_N64_BUTTONS];
+
+ /** The fractional value of the analog-x axis, between -1 and 1, inclusive. */
+ public float axisFractionX = 0;
+
+ /** The fractional value of the analog-y axis, between -1 and 1, inclusive. */
+ public float axisFractionY = 0;
+ }
+
+ // Constants must match EButton listing in plugin.h! (input-sdl plug-in)
+
+ /** N64 button: dpad-right. */
+ public static final int DPD_R = 0;
+
+ /** N64 button: dpad-left. */
+ public static final int DPD_L = 1;
+
+ /** N64 button: dpad-down. */
+ public static final int DPD_D = 2;
+
+ /** N64 button: dpad-up. */
+ public static final int DPD_U = 3;
+
+ /** N64 button: start. */
+ public static final int START = 4;
+
+ /** N64 button: trigger-z. */
+ public static final int BTN_Z = 5;
+
+ /** N64 button: b. */
+ public static final int BTN_B = 6;
+
+ /** N64 button: a. */
+ public static final int BTN_A = 7;
+
+ /** N64 button: cpad-right. */
+ public static final int CPD_R = 8;
+
+ /** N64 button: cpad-left. */
+ public static final int CPD_L = 9;
+
+ /** N64 button: cpad-down. */
+ public static final int CPD_D = 10;
+
+ /** N64 button: cpad-up. */
+ public static final int CPD_U = 11;
+
+ /** N64 button: shoulder-r. */
+ public static final int BTN_R = 12;
+
+ /** N64 button: shoulder-l. */
+ public static final int BTN_L = 13;
+
+ /** N64 button: reserved-1. */
+ public static final int BTN_RESERVED1 = 14;
+
+ /** N64 button: reserved-2. */
+ public static final int BTN_RESERVED2 = 15;
+
+ /** Total number of N64 buttons. */
+ public static final int NUM_N64_BUTTONS = 16;
+
+ /** The state of all four player controllers. */
+ private static final ArrayList sStates = new ArrayList();
+
+ /** The state of this controller. */
+ protected State mState;
+
+ /** The player number, between 1 and 4, inclusive. */
+ protected int mPlayerNumber = 1;
+
+ /** The factor by which the axis fractions are scaled before going to the core. */
+ private static final float AXIS_SCALE = 80;
+
+ static
+ {
+ sStates.add( new State() );
+ sStates.add( new State() );
+ sStates.add( new State() );
+ sStates.add( new State() );
+ }
+
+ /**
+ * Instantiates a new abstract controller.
+ */
+ protected AbstractController()
+ {
+ mState = sStates.get( 0 );
+ }
+
+ /**
+ * Notifies the core that the N64 controller state has changed.
+ */
+ protected void notifyChanged()
+ {
+ int axisX = Math.round( AXIS_SCALE * mState.axisFractionX );
+ int axisY = Math.round( AXIS_SCALE * mState.axisFractionY );
+ NativeInput.setState( mPlayerNumber - 1, mState.buttons, axisX, axisY );
+ }
+
+ /**
+ * Gets the player number.
+ *
+ * @return The player number, between 1 and 4, inclusive.
+ */
+ public int getPlayerNumber()
+ {
+ return mPlayerNumber;
+ }
+
+ /**
+ * Sets the player number.
+ *
+ * @param player The new player number, between 1 and 4, inclusive.
+ */
+ public void setPlayerNumber( int player )
+ {
+ mPlayerNumber = player;
+ mState = sStates.get( mPlayerNumber - 1 );
+ }
+}
diff --git a/Android/src/emu/project64/input/TouchController.java b/Android/src/emu/project64/input/TouchController.java
new file mode 100644
index 000000000..86791e98c
--- /dev/null
+++ b/Android/src/emu/project64/input/TouchController.java
@@ -0,0 +1,534 @@
+/****************************************************************************
+* *
+* Project 64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.input;
+
+import java.util.Set;
+
+import emu.project64.AndroidDevice;
+import emu.project64.input.map.TouchMap;
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.graphics.Point;
+import android.os.Vibrator;
+import android.util.FloatMath;
+import android.util.SparseIntArray;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+
+/**
+ * A class for generating N64 controller commands from a touchscreen.
+ */
+@SuppressWarnings("deprecation")
+public class TouchController extends AbstractController implements OnTouchListener
+{
+ public interface OnStateChangedListener
+ {
+ /**
+ * Called after the analog stick values have changed.
+ *
+ * @param axisFractionX The x-axis fraction, between -1 and 1, inclusive.
+ * @param axisFractionY The y-axis fraction, between -1 and 1, inclusive.
+ */
+ public void onAnalogChanged( float axisFractionX, float axisFractionY );
+
+ /**
+ * Called after auto-hold button state changed.
+ *
+ * @param pressed The auto-hold state.
+ * @param index The index of the auto-hold mask.
+ */
+ public void onAutoHold( boolean pressed, int index );
+ }
+
+ public static final int AUTOHOLD_METHOD_DISABLED = 0;
+ public static final int AUTOHOLD_METHOD_LONGPRESS = 1;
+ public static final int AUTOHOLD_METHOD_SLIDEOUT = 2;
+
+ /** The number of milliseconds to wait before auto-holding (long-press method). */
+ private static final int AUTOHOLD_LONGPRESS_TIME = 1000;
+
+ /** The pattern vibration when auto-hold is engaged. */
+ private static final long[] AUTOHOLD_VIBRATE_PATTERN = { 0, 50, 50, 50 };
+
+ /** The number of milliseconds of vibration when pressing a key. */
+ private static final int FEEDBACK_VIBRATE_TIME = 50;
+
+ /** The maximum number of pointers to query. */
+ private static final int MAX_POINTER_IDS = 256;
+
+ /** The state change listener. */
+ private final OnStateChangedListener mListener;
+
+ /** The map from screen coordinates to N64 controls. */
+ private final TouchMap mTouchMap;
+
+ /** The map from pointer ids to N64 controls. */
+ private final SparseIntArray mPointerMap = new SparseIntArray();
+
+ /** The method used for auto-holding buttons. */
+ private final int mAutoHoldMethod;
+
+ /** The set of auto-holdable buttons. */
+ private final Set mAutoHoldables;
+
+ /** Whether touchscreen feedback is enabled. */
+ private final boolean mTouchscreenFeedback;
+
+ /** The touch state of each pointer. True indicates down, false indicates up. */
+ private final boolean[] mTouchState = new boolean[MAX_POINTER_IDS];
+
+ /** The x-coordinate of each pointer, between 0 and (screenwidth-1), inclusive. */
+ private final int[] mPointerX = new int[MAX_POINTER_IDS];
+
+ /** The y-coordinate of each pointer, between 0 and (screenheight-1), inclusive. */
+ private final int[] mPointerY = new int[MAX_POINTER_IDS];
+
+ /** The pressed start time of each pointer. */
+ private final long[] mStartTime = new long[MAX_POINTER_IDS];
+
+ /** The time between press and release of each pointer. */
+ private final long[] mElapsedTime = new long[MAX_POINTER_IDS];
+
+ /**
+ * The identifier of the pointer associated with the analog stick. -1 indicates the stick has
+ * been released.
+ */
+ private int mAnalogPid = -1;
+
+ /** The touch event source to listen to, or 0 to listen to all sources. */
+ private int mSourceFilter = 0;
+
+ private Vibrator mVibrator = null;
+
+ /**
+ * Instantiates a new touch controller.
+ *
+ * @param touchMap The map from touch coordinates to N64 controls.
+ * @param view The view receiving touch event data.
+ * @param listener The listener for controller state changes.
+ * @param vibrator The haptic feedback device. MUST BE NULL if vibrate permission not granted.
+ * @param autoHoldMethod The method for auto-holding buttons.
+ * @param touchscreenFeedback True if haptic feedback should be used.
+ * @param autoHoldableButtons The N64 commands that correspond to auto-holdable buttons.
+ */
+ public TouchController( TouchMap touchMap, View view, OnStateChangedListener listener,
+ Vibrator vibrator, int autoHoldMethod, boolean touchscreenFeedback,
+ Set autoHoldableButtons )
+ {
+ mListener = listener;
+ mTouchMap = touchMap;
+ mVibrator = vibrator;
+ mAutoHoldMethod = autoHoldMethod;
+ mTouchscreenFeedback = touchscreenFeedback;
+ mAutoHoldables = autoHoldableButtons;
+ view.setOnTouchListener( this );
+ }
+
+ /**
+ * Sets the touch event source filter.
+ *
+ * @param source The source to listen to, or 0 to listen to all sources.
+ */
+ public void setSourceFilter( int source )
+ {
+ mSourceFilter = source;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see android.view.View.OnTouchListener#onTouch(android.view.View, android.view.MotionEvent)
+ */
+ @SuppressLint( "ClickableViewAccessibility" )
+ @Override
+ @TargetApi( 9 )
+ public boolean onTouch( View view, MotionEvent event )
+ {
+ // Filter by source, if applicable
+ int source = AndroidDevice.IS_GINGERBREAD ? event.getSource() : 0;
+ if( mSourceFilter != 0 && mSourceFilter != source )
+ return false;
+
+ int action = event.getAction();
+ int actionCode = action & MotionEvent.ACTION_MASK;
+
+ int pid = -1;
+ switch( actionCode )
+ {
+ case MotionEvent.ACTION_POINTER_DOWN:
+ // A non-primary touch has been made
+ pid = event.getPointerId( action >> MotionEvent.ACTION_POINTER_INDEX_SHIFT );
+ mStartTime[pid] = System.currentTimeMillis();
+ mTouchState[pid] = true;
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ // A non-primary touch has been released
+ pid = event.getPointerId( action >> MotionEvent.ACTION_POINTER_INDEX_SHIFT );
+ mElapsedTime[pid] = System.currentTimeMillis() - mStartTime[pid];
+ mTouchState[pid] = false;
+ break;
+ case MotionEvent.ACTION_DOWN:
+ // A touch gesture has started (e.g. analog stick movement)
+ for( int i = 0; i < event.getPointerCount(); i++ )
+ {
+ pid = event.getPointerId( i );
+ mStartTime[pid] = System.currentTimeMillis();
+ mTouchState[pid] = true;
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ // A touch gesture has ended or canceled (e.g. analog stick movement)
+ for( int i = 0; i < event.getPointerCount(); i++ )
+ {
+ pid = event.getPointerId( i );
+ mElapsedTime[pid] = System.currentTimeMillis() - mStartTime[pid];
+ mTouchState[pid] = false;
+ }
+ break;
+ default:
+ break;
+ }
+
+ // Update the coordinates of down pointers and record max PID for speed
+ int maxPid = -1;
+ for( int i = 0; i < event.getPointerCount(); i++ )
+ {
+ pid = event.getPointerId( i );
+ if( pid > maxPid )
+ maxPid = pid;
+ if( mTouchState[pid] )
+ {
+ mPointerX[pid] = (int) event.getX( i );
+ mPointerY[pid] = (int) event.getY( i );
+ }
+ }
+
+ // Process each touch
+ processTouches( mTouchState, mPointerX, mPointerY, mElapsedTime, maxPid );
+
+ return true;
+ }
+
+ /**
+ * Sets the N64 controller state based on where the screen is (multi-) touched. Values outside
+ * the ranges listed below are safe.
+ *
+ * @param touchstate The touch state of each pointer. True indicates down, false indicates up.
+ * @param pointerX The x-coordinate of each pointer, between 0 and (screenwidth-1), inclusive.
+ * @param pointerY The y-coordinate of each pointer, between 0 and (screenheight-1), inclusive.
+ * @param maxPid Maximum ID of the pointers that have changed (speed optimization).
+ */
+ private void processTouches( boolean[] touchstate, int[] pointerX, int[] pointerY,
+ long[] elapsedTime, int maxPid )
+ {
+ boolean analogMoved = false;
+
+ // Process each pointer in sequence
+ for( int pid = 0; pid <= maxPid; pid++ )
+ {
+ // Release analog if its pointer is not touching the screen
+ if( pid == mAnalogPid && !touchstate[pid] )
+ {
+ analogMoved = true;
+ mAnalogPid = -1;
+ mState.axisFractionX = 0;
+ mState.axisFractionY = 0;
+ }
+
+ // Process button inputs
+ if( pid != mAnalogPid )
+ processButtonTouch( touchstate[pid], pointerX[pid], pointerY[pid],
+ elapsedTime[pid], pid );
+
+ // Process analog inputs
+ if( touchstate[pid] && processAnalogTouch( pid, pointerX[pid], pointerY[pid] ) )
+ analogMoved = true;
+ }
+
+ // Call the super method to send the input to the core
+ notifyChanged();
+
+ // Update the skin if the virtual analog stick moved
+ if( analogMoved && mListener != null )
+ mListener.onAnalogChanged( mState.axisFractionX, mState.axisFractionY );
+ }
+
+ /**
+ * Process a touch as if intended for a button. Values outside the ranges listed below are safe.
+ *
+ * @param touched Whether the button is pressed or not.
+ * @param xLocation The x-coordinate of the touch, between 0 and (screenwidth-1), inclusive.
+ * @param yLocation The y-coordinate of the touch, between 0 and (screenheight-1), inclusive.
+ * @param pid The identifier of the touch pointer.
+ */
+ private void processButtonTouch( boolean touched, int xLocation, int yLocation,
+ long timeElapsed, int pid )
+ {
+ // Determine the index of the button that was pressed
+ int index = touched
+ ? mTouchMap.getButtonPress( xLocation, yLocation )
+ : mPointerMap.get( pid, TouchMap.UNMAPPED );
+
+ // Update the pointer map
+ if( !touched )
+ {
+ // Finger lifted off screen, forget what this pointer was touching
+ mPointerMap.delete( pid );
+ }
+ else
+ {
+ // Determine where the finger came from if is was slid
+ int prevIndex = mPointerMap.get( pid, TouchMap.UNMAPPED );
+
+ // Finger touched somewhere on screen, remember what this pointer is touching
+ mPointerMap.put( pid, index );
+
+ if( prevIndex != index )
+ {
+ // Finger slid from somewhere else, act accordingly
+ // There are three possibilities:
+ // - old button --> new button
+ // - nothing --> new button
+ // - old button --> nothing
+
+ // Reset this pointer's start time
+ mStartTime[pid] = System.currentTimeMillis();
+
+ if( prevIndex != TouchMap.UNMAPPED )
+ {
+ // Slid off a valid button
+ if( !isAutoHoldable( prevIndex ) || mAutoHoldMethod == AUTOHOLD_METHOD_DISABLED )
+ {
+ // Slid off a non-auto-hold button
+ setTouchState( prevIndex, false );
+ }
+ else
+ {
+ // Slid off an auto-hold button
+ switch( mAutoHoldMethod )
+ {
+ case AUTOHOLD_METHOD_LONGPRESS:
+ // Using long-press method, release auto-hold button
+ if( mListener != null )
+ mListener.onAutoHold( false, prevIndex );
+ setTouchState( prevIndex, false );
+ break;
+
+ case AUTOHOLD_METHOD_SLIDEOUT:
+ // Using slide-off method, engage auto-hold button
+ if( mVibrator != null )
+ {
+ mVibrator.cancel();
+ mVibrator.vibrate( AUTOHOLD_VIBRATE_PATTERN, -1 );
+ }
+ if( mListener != null )
+ mListener.onAutoHold( true, prevIndex );
+ setTouchState( prevIndex, true );
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if( index != TouchMap.UNMAPPED )
+ {
+ // Finger is on a valid button
+
+ // Provide simple vibration feedback for any valid button when first touched
+ if( touched && mTouchscreenFeedback && mVibrator != null )
+ {
+ boolean firstTouched;
+ if( index < NUM_N64_BUTTONS )
+ {
+ // Single button pressed
+ firstTouched = !mState.buttons[index];
+ }
+ else
+ {
+ // Two d-pad buttons pressed simultaneously
+ switch( index )
+ {
+ case TouchMap.DPD_RU:
+ firstTouched = !( mState.buttons[DPD_R] && mState.buttons[DPD_U] );
+ break;
+ case TouchMap.DPD_RD:
+ firstTouched = !( mState.buttons[DPD_R] && mState.buttons[DPD_D] );
+ break;
+ case TouchMap.DPD_LD:
+ firstTouched = !( mState.buttons[DPD_L] && mState.buttons[DPD_D] );
+ break;
+ case TouchMap.DPD_LU:
+ firstTouched = !( mState.buttons[DPD_L] && mState.buttons[DPD_U] );
+ break;
+ default:
+ firstTouched = false;
+ break;
+ }
+ }
+
+ if( firstTouched )
+ {
+ mVibrator.cancel();
+ mVibrator.vibrate( FEEDBACK_VIBRATE_TIME );
+ }
+ }
+
+ // Set the controller state accordingly
+ if( touched || !isAutoHoldable( index ) || mAutoHoldMethod == AUTOHOLD_METHOD_DISABLED )
+ {
+ // Finger just touched a button (any kind) OR
+ // Finger just lifted off non-auto-holdable button
+ setTouchState( index, touched );
+ // Do not provide auto-hold feedback yet
+ }
+ else
+ {
+ // Finger just lifted off an auto-holdable button
+ switch( mAutoHoldMethod )
+ {
+ case AUTOHOLD_METHOD_SLIDEOUT:
+ // Release auto-hold button if using slide-off method
+ if( mListener != null )
+ mListener.onAutoHold( false, index );
+ setTouchState( index, false );
+ break;
+
+ case AUTOHOLD_METHOD_LONGPRESS:
+ if( timeElapsed < AUTOHOLD_LONGPRESS_TIME )
+ {
+ // Release auto-hold if short-pressed
+ if( mListener != null )
+ mListener.onAutoHold( false, index );
+ setTouchState( index, false );
+ }
+ else
+ {
+ // Engage auto-hold if long-pressed
+ if( mVibrator != null )
+ {
+ mVibrator.cancel();
+ mVibrator.vibrate( AUTOHOLD_VIBRATE_PATTERN, -1 );
+ }
+ if( mListener != null )
+ mListener.onAutoHold( true, index );
+ setTouchState( index, true );
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Checks if the button mapped to an N64 command is auto-holdable.
+ *
+ * @param commandIndex The index to the N64 command.
+ *
+ * @return True if the button mapped to the command is auto-holdable.
+ */
+ private boolean isAutoHoldable( int commandIndex )
+ {
+ return mAutoHoldables != null && mAutoHoldables.contains( commandIndex );
+ }
+
+ /**
+ * Sets the state of a button, and handles the D-Pad diagonals.
+ *
+ * @param index Which button is affected.
+ * @param touched Whether the button is pressed or not.
+ */
+ private void setTouchState( int index, boolean touched )
+ {
+ // Set the button state
+ if( index < AbstractController.NUM_N64_BUTTONS )
+ {
+ // A single button was pressed
+ mState.buttons[index] = touched;
+ }
+ else
+ {
+ // Two d-pad buttons pressed simultaneously
+ switch( index )
+ {
+ case TouchMap.DPD_RU:
+ mState.buttons[DPD_R] = touched;
+ mState.buttons[DPD_U] = touched;
+ break;
+ case TouchMap.DPD_RD:
+ mState.buttons[DPD_R] = touched;
+ mState.buttons[DPD_D] = touched;
+ break;
+ case TouchMap.DPD_LD:
+ mState.buttons[DPD_L] = touched;
+ mState.buttons[DPD_D] = touched;
+ break;
+ case TouchMap.DPD_LU:
+ mState.buttons[DPD_L] = touched;
+ mState.buttons[DPD_U] = touched;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ /**
+ * Process a touch as if intended for the analog stick. Values outside the ranges listed below
+ * are safe.
+ *
+ * @param pointerId The pointer identifier.
+ * @param xLocation The x-coordinate of the touch, between 0 and (screenwidth-1), inclusive.
+ * @param yLocation The y-coordinate of the touch, between 0 and (screenheight-1), inclusive.
+ *
+ * @return True, if the analog state changed.
+ */
+ private boolean processAnalogTouch( int pointerId, int xLocation, int yLocation )
+ {
+ // Get the cartesian displacement of the analog stick
+ Point point = mTouchMap.getAnalogDisplacement( xLocation, yLocation );
+
+ // Compute the pythagorean displacement of the stick
+ int dX = point.x;
+ int dY = point.y;
+ float displacement = FloatMath.sqrt( ( dX * dX ) + ( dY * dY ) );
+
+ // "Capture" the analog control
+ if( mTouchMap.isInCaptureRange( displacement ) )
+ mAnalogPid = pointerId;
+
+ if( pointerId == mAnalogPid )
+ {
+ // User is controlling the analog stick
+
+ // Limit range of motion to an octagon (like the real N64 controller)
+ point = mTouchMap.getConstrainedDisplacement( dX, dY );
+ dX = point.x;
+ dY = point.y;
+ displacement = FloatMath.sqrt( ( dX * dX ) + ( dY * dY ) );
+
+ // Fraction of full-throttle, between 0 and 1, inclusive
+ float p = mTouchMap.getAnalogStrength( displacement );
+
+ // Store the axis values in the super fields (screen y is inverted)
+ mState.axisFractionX = p * dX / displacement;
+ mState.axisFractionY = -p * dY / displacement;
+
+ // Analog state changed
+ return true;
+ }
+
+ // Analog state did not change
+ return false;
+ }
+}
diff --git a/Android/src/emu/project64/input/map/AxisMap.java b/Android/src/emu/project64/input/map/AxisMap.java
new file mode 100644
index 000000000..5a071796a
--- /dev/null
+++ b/Android/src/emu/project64/input/map/AxisMap.java
@@ -0,0 +1,203 @@
+/****************************************************************************
+* *
+* Project64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.input.map;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import android.annotation.TargetApi;
+import android.text.TextUtils;
+import android.util.SparseArray;
+import android.view.InputDevice;
+import android.view.InputDevice.MotionRange;
+import android.view.MotionEvent;
+
+@TargetApi( 9 )
+public class AxisMap extends SerializableMap
+{
+ public static final int AXIS_CLASS_UNKNOWN = 0;
+ public static final int AXIS_CLASS_IGNORED = 1;
+ public static final int AXIS_CLASS_STICK = 2;
+ public static final int AXIS_CLASS_TRIGGER = 3;
+ public static final int AXIS_CLASS_N64_USB_STICK = 102;
+
+ private static final int SIGNATURE_HASH_XBOX360 = 449832952;
+ private static final int SIGNATURE_HASH_XBOX360_WIRELESS = -412618953;
+ private static final int SIGNATURE_HASH_PS3 = -528816963;
+ private static final int SIGNATURE_HASH_LOGITECH_WINGMAN_RUMBLEPAD = 1247256123;
+ private static final int SIGNATURE_HASH_MOGA_PRO = -1933523749;
+ private static final int SIGNATURE_HASH_OUYA = 699487739;
+ private static final int SIGNATURE_HASH_AMAZON_FIRE = 2050752785;
+
+ private static final SparseArray sAllMaps = new SparseArray();
+ private final String mSignature;
+ private final String mSignatureName;
+
+ public static AxisMap getMap( InputDevice device )
+ {
+ if( device == null )
+ return null;
+
+ int id = device.hashCode();
+ AxisMap map = sAllMaps.get( id );
+ if( map == null )
+ {
+ // Add an entry to the map if not found
+ map = new AxisMap( device );
+ sAllMaps.put( id, map );
+ }
+ return map;
+ }
+
+ @TargetApi( 12 )
+ public AxisMap( InputDevice device )
+ {
+ // Auto-classify the axes
+ List motionRanges = device.getMotionRanges();
+ List axisCodes = new ArrayList();
+ for( MotionRange motionRange : motionRanges )
+ {
+ if( motionRange.getSource() == InputDevice.SOURCE_JOYSTICK )
+ {
+ int axisCode = motionRange.getAxis();
+ int axisClass = detectClass( motionRange );
+ setClass( axisCode, axisClass );
+ axisCodes.add( axisCode );
+ }
+ }
+
+ // Construct the signature based on the available axes
+ Collections.sort( axisCodes );
+ mSignature = TextUtils.join( ",", axisCodes );
+ String signatureName = "Default";
+ String deviceName = device.getName();
+
+ // Use the signature to override faulty auto-classifications
+ switch( mSignature.hashCode() )
+ {
+ case SIGNATURE_HASH_XBOX360:
+ // Resting value is -1 on the analog triggers; fix that
+ if( deviceName.contains( "Sony Computer Entertainment Wireless Controller" ) )
+ {
+ // Note that the PS4 controller uses the same axes but uses different ones for
+ // the triggers, so we have to differentiate them.
+ setClass( MotionEvent.AXIS_RX, AXIS_CLASS_TRIGGER );
+ setClass( MotionEvent.AXIS_RY, AXIS_CLASS_TRIGGER );
+ signatureName = "PS4 compatible";
+ }
+ else
+ {
+ setClass( MotionEvent.AXIS_Z, AXIS_CLASS_TRIGGER );
+ setClass( MotionEvent.AXIS_RZ, AXIS_CLASS_TRIGGER );
+ signatureName = "Xbox 360 compatible";
+ }
+ break;
+
+ case SIGNATURE_HASH_XBOX360_WIRELESS:
+ // Resting value is -1 on the analog triggers; fix that
+ setClass( MotionEvent.AXIS_Z, AXIS_CLASS_TRIGGER );
+ setClass( MotionEvent.AXIS_RZ, AXIS_CLASS_TRIGGER );
+ signatureName = "Xbox 360 wireless";
+ break;
+
+ case SIGNATURE_HASH_PS3:
+ // Ignore pressure sensitive buttons (buggy on Android)
+ setClass( MotionEvent.AXIS_GENERIC_1, AXIS_CLASS_IGNORED );
+ setClass( MotionEvent.AXIS_GENERIC_2, AXIS_CLASS_IGNORED );
+ setClass( MotionEvent.AXIS_GENERIC_3, AXIS_CLASS_IGNORED );
+ setClass( MotionEvent.AXIS_GENERIC_4, AXIS_CLASS_IGNORED );
+ setClass( MotionEvent.AXIS_GENERIC_5, AXIS_CLASS_IGNORED );
+ setClass( MotionEvent.AXIS_GENERIC_6, AXIS_CLASS_IGNORED );
+ setClass( MotionEvent.AXIS_GENERIC_7, AXIS_CLASS_IGNORED );
+ setClass( MotionEvent.AXIS_GENERIC_8, AXIS_CLASS_IGNORED );
+ signatureName = "PS3 compatible";
+ break;
+
+ case SIGNATURE_HASH_LOGITECH_WINGMAN_RUMBLEPAD:
+ // Bug in controller firmware cross-wires throttle and right stick up/down
+ setClass( MotionEvent.AXIS_THROTTLE, AXIS_CLASS_STICK );
+ signatureName = "Logitech Wingman Rumblepad";
+ break;
+
+ case SIGNATURE_HASH_MOGA_PRO:
+ // Ignore two spurious axes for MOGA Pro in HID (B) mode (as of MOGA Pivot v1.15)
+ // http://www.paulscode.com/forum/index.php?topic=581.msg10094#msg10094
+ setClass( MotionEvent.AXIS_GENERIC_1, AXIS_CLASS_IGNORED );
+ setClass( MotionEvent.AXIS_GENERIC_2, AXIS_CLASS_IGNORED );
+ signatureName = "Moga Pro (HID mode)";
+ break;
+
+ case SIGNATURE_HASH_OUYA:
+ // Ignore phantom triggers
+ setClass( MotionEvent.AXIS_GENERIC_1, AXIS_CLASS_IGNORED );
+ setClass( MotionEvent.AXIS_GENERIC_2, AXIS_CLASS_IGNORED );
+ setClass( MotionEvent.AXIS_GENERIC_3, AXIS_CLASS_IGNORED );
+ setClass( MotionEvent.AXIS_GENERIC_4, AXIS_CLASS_IGNORED );
+ signatureName = "OUYA controller";
+ break;
+
+ case SIGNATURE_HASH_AMAZON_FIRE:
+ // Ignore floating generic axis
+ setClass( MotionEvent.AXIS_GENERIC_1, AXIS_CLASS_IGNORED );
+ signatureName = "Amazon Fire Game Controller";
+ break;
+ }
+
+ // Check if the controller is an N64/USB adapter, to compensate for range of motion
+ if( deviceName.contains( "raphnet.net GC/N64_USB" ) ||
+ deviceName.contains( "raphnet.net GC/N64 to USB, v2" ) ||
+ deviceName.contains( "HuiJia USB GamePad" ) ) // double space is not a typo
+ {
+ setClass( MotionEvent.AXIS_X, AXIS_CLASS_N64_USB_STICK );
+ setClass( MotionEvent.AXIS_Y, AXIS_CLASS_N64_USB_STICK );
+ signatureName = "N64 USB adapter";
+ }
+
+ mSignatureName = signatureName;
+ }
+
+ public void setClass( int axisCode, int axisClass )
+ {
+ if( axisClass == AXIS_CLASS_UNKNOWN )
+ mMap.delete( axisCode );
+ else
+ mMap.put( axisCode, axisClass );
+ }
+
+ public int getClass( int axisCode )
+ {
+ return mMap.get( axisCode );
+ }
+
+ public String getSignature()
+ {
+ return mSignature;
+ }
+
+ public String getSignatureName()
+ {
+ return mSignatureName;
+ }
+
+ @TargetApi( 12 )
+ private static int detectClass( MotionRange motionRange )
+ {
+ if( motionRange != null )
+ {
+ if( motionRange.getMin() == -1 )
+ return AXIS_CLASS_STICK;
+ else if( motionRange.getMin() == 0 )
+ return AXIS_CLASS_TRIGGER;
+ }
+ return AXIS_CLASS_UNKNOWN;
+ }
+}
diff --git a/Android/src/emu/project64/input/map/SerializableMap.java b/Android/src/emu/project64/input/map/SerializableMap.java
new file mode 100644
index 000000000..3ee42d111
--- /dev/null
+++ b/Android/src/emu/project64/input/map/SerializableMap.java
@@ -0,0 +1,96 @@
+/****************************************************************************
+* *
+* Project64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.input.map;
+
+import android.util.SparseIntArray;
+
+public class SerializableMap
+{
+ /** Storage for the map. */
+ protected final SparseIntArray mMap = new SparseIntArray();
+
+ /**
+ * Instantiates a new map.
+ */
+ public SerializableMap()
+ {
+ }
+
+ /**
+ * Instantiates a new map from a serialization.
+ *
+ * @param serializedMap The serialization of the map.
+ */
+ public SerializableMap( String serializedMap )
+ {
+ this();
+ deserialize( serializedMap );
+ }
+
+ /**
+ * Removes all entries from the map.
+ */
+ public void unmapAll()
+ {
+ mMap.clear();
+ }
+
+ /**
+ * Serializes the map data to a string.
+ *
+ * @return The string representation of the map data.
+ */
+ public String serialize()
+ {
+ // Serialize the map data to a multi-delimited string
+ String result = "";
+ for( int i = 0; i < mMap.size(); i++ )
+ {
+ // Putting the value first makes the string a bit more human readable IMO
+ result += mMap.valueAt( i ) + ":" + mMap.keyAt( i ) + ",";
+ }
+ return result;
+ }
+
+ /**
+ * Deserializes the map data from a string.
+ *
+ * @param s The string representation of the map data.
+ */
+ public void deserialize( String s )
+ {
+ // Reset the map
+ unmapAll();
+
+ // Parse the new map data from the multi-delimited string
+ if( s != null )
+ {
+ // Read the input mappings
+ String[] pairs = s.split( "," );
+ for( String pair : pairs )
+ {
+ String[] elements = pair.split( ":" );
+ if( elements.length == 2 )
+ {
+ try
+ {
+ int value = Integer.parseInt( elements[0] );
+ int key = Integer.parseInt( elements[1] );
+ mMap.put( key, value );
+ }
+ catch( NumberFormatException ignored )
+ {
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Android/src/emu/project64/input/map/TouchMap.java b/Android/src/emu/project64/input/map/TouchMap.java
new file mode 100644
index 000000000..8f2066a63
--- /dev/null
+++ b/Android/src/emu/project64/input/map/TouchMap.java
@@ -0,0 +1,591 @@
+/****************************************************************************
+* *
+* Project64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.input.map;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import emu.project64.input.AbstractController;
+import emu.project64.input.TouchController;
+import emu.project64.persistent.ConfigFile;
+import emu.project64.persistent.ConfigFile.ConfigSection;
+import emu.project64.profile.Profile;
+import emu.project64.util.Image;
+import emu.project64.util.Utility;
+import android.annotation.SuppressLint;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.util.FloatMath;
+import android.util.Log;
+import android.util.SparseArray;
+
+/**
+ * A class for mapping digitizer coordinates to N64 buttons/axes.
+ *
+ * @see VisibleTouchMap
+ * @see TouchController
+ */
+@SuppressLint("FloatMath")
+@SuppressWarnings("deprecation")
+public class TouchMap
+{
+ /** Map flag: Touch location is not mapped. */
+ public static final int UNMAPPED = -1;
+
+ /** Map offset: N64 pseudo-buttons. */
+ public static final int OFFSET_EXTRAS = AbstractController.NUM_N64_BUTTONS;
+
+ /** N64 pseudo-button: dpad-right-up. */
+ public static final int DPD_RU = OFFSET_EXTRAS;
+
+ /** N64 pseudo-button: dpad-right-down. */
+ public static final int DPD_RD = OFFSET_EXTRAS + 1;
+
+ /** N64 pseudo-button: dpad-left-down. */
+ public static final int DPD_LD = OFFSET_EXTRAS + 2;
+
+ /** N64 pseudo-button: dpad-left-up. */
+ public static final int DPD_LU = OFFSET_EXTRAS + 3;
+
+ /** Total number of N64 (pseudo-)buttons. */
+ public static final int NUM_N64_PSEUDOBUTTONS = OFFSET_EXTRAS + 4;
+
+ /** Folder containing the images. */
+ protected String skinFolder;
+
+ /** Scaling factor to apply to images. */
+ protected float scale = 1.0f;
+
+ /** Button images. */
+ protected ArrayList buttonImages;
+
+ /** Button masks. */
+ private final ArrayList buttonMasks;
+
+ /** X-coordinates of the buttons, in percent. */
+ private final ArrayList buttonX;
+
+ /** Y-coordinates of the buttons, in percent. */
+ private final ArrayList buttonY;
+
+ /** names of the buttons. */
+ private final ArrayList buttonNames;
+
+ /** Analog background image (fixed). */
+ protected Image analogBackImage;
+
+ /** Analog foreground image (movable). */
+ protected Image analogForeImage;
+
+ /** X-coordinate of the analog background, in percent. */
+ private int analogBackX;
+
+ /** Y-coordinate of the analog background, in percent. */
+ private int analogBackY;
+
+ /** Deadzone of the analog stick, in pixels. */
+ private int analogDeadzone;
+
+ /** Maximum displacement of the analog stick, in pixels. */
+ protected int analogMaximum;
+
+ /** Extra region beyond maximum in which the analog stick can be captured, in pixels. */
+ private int analogPadding;
+
+ /** The resources of the associated activity. */
+ protected final Resources mResources;
+
+ /** Map from N64 (pseudo-)button to mask color. */
+ private final int[] mN64ToColor;
+
+ /** The map from strings in the skin.ini file to N64 button indices. */
+ public static final HashMap MASK_KEYS = new HashMap();
+
+ /** The map from N64 button indices to asset name prefixes in the skin folder. */
+ public static final SparseArray ASSET_NAMES = new SparseArray();
+
+ /** The error in RGB (256x256x256) space that we tolerate when matching mask colors. */
+ private static final int MATCH_TOLERANCE = 10;
+
+ static
+ {
+ // Define the map from skin.ini keys to N64 button indices
+ // @formatter:off
+ MASK_KEYS.put( "Dr", AbstractController.DPD_R );
+ MASK_KEYS.put( "Dl", AbstractController.DPD_L );
+ MASK_KEYS.put( "Dd", AbstractController.DPD_D );
+ MASK_KEYS.put( "Du", AbstractController.DPD_U );
+ MASK_KEYS.put( "S", AbstractController.START );
+ MASK_KEYS.put( "Z", AbstractController.BTN_Z );
+ MASK_KEYS.put( "B", AbstractController.BTN_B );
+ MASK_KEYS.put( "A", AbstractController.BTN_A );
+ MASK_KEYS.put( "Cr", AbstractController.CPD_R );
+ MASK_KEYS.put( "Cl", AbstractController.CPD_L );
+ MASK_KEYS.put( "Cd", AbstractController.CPD_D );
+ MASK_KEYS.put( "Cu", AbstractController.CPD_U );
+ MASK_KEYS.put( "R", AbstractController.BTN_R );
+ MASK_KEYS.put( "L", AbstractController.BTN_L );
+ MASK_KEYS.put( "Dru", DPD_RU );
+ MASK_KEYS.put( "Drd", DPD_RD );
+ MASK_KEYS.put( "Dld", DPD_LD );
+ MASK_KEYS.put( "Dlu", DPD_LU );
+ // @formatter:on
+
+ // Define the map from N64 button indices to profile key prefixes
+ ASSET_NAMES.put( AbstractController.DPD_R, "dpad" );
+ ASSET_NAMES.put( AbstractController.DPD_L, "dpad" );
+ ASSET_NAMES.put( AbstractController.DPD_D, "dpad" );
+ ASSET_NAMES.put( AbstractController.DPD_U, "dpad" );
+ ASSET_NAMES.put( AbstractController.START, "buttonS" );
+ ASSET_NAMES.put( AbstractController.BTN_Z, "buttonZ" );
+ ASSET_NAMES.put( AbstractController.BTN_B, "groupAB" );
+ ASSET_NAMES.put( AbstractController.BTN_A, "groupAB" );
+ ASSET_NAMES.put( AbstractController.CPD_R, "groupC" );
+ ASSET_NAMES.put( AbstractController.CPD_L, "groupC" );
+ ASSET_NAMES.put( AbstractController.CPD_D, "groupC" );
+ ASSET_NAMES.put( AbstractController.CPD_U, "groupC" );
+ ASSET_NAMES.put( AbstractController.BTN_R, "buttonR" );
+ ASSET_NAMES.put( AbstractController.BTN_L, "buttonL" );
+ ASSET_NAMES.put( DPD_LU, "dpad" );
+ ASSET_NAMES.put( DPD_LD, "dpad" );
+ ASSET_NAMES.put( DPD_RD, "dpad" );
+ ASSET_NAMES.put( DPD_RU, "dpad" );
+ }
+
+ /**
+ * Instantiates a new touch map.
+ *
+ * @param resources The resources of the activity associated with this touch map.
+ */
+ public TouchMap( Resources resources )
+ {
+ mResources = resources;
+ mN64ToColor = new int[NUM_N64_PSEUDOBUTTONS];
+ buttonImages = new ArrayList();
+ buttonMasks = new ArrayList();
+ buttonX = new ArrayList();
+ buttonY = new ArrayList();
+ buttonNames = new ArrayList();
+ }
+
+ /**
+ * Clears the map data.
+ */
+ public void clear()
+ {
+ buttonImages.clear();
+ buttonMasks.clear();
+ buttonX.clear();
+ buttonY.clear();
+ buttonNames.clear();
+ analogBackImage = null;
+ analogForeImage = null;
+ analogBackX = analogBackY = 0;
+ analogPadding = 32;
+ analogDeadzone = 2;
+ analogMaximum = 360;
+ for( int i = 0; i < mN64ToColor.length; i++ )
+ mN64ToColor[i] = -1;
+ }
+
+ /**
+ * Recomputes the map data for a given digitizer size.
+ *
+ * @param w The width of the digitizer, in pixels.
+ * @param h The height of the digitizer, in pixels.
+ */
+ public void resize( int w, int h )
+ {
+ // Recompute button locations
+ for( int i = 0; i < buttonImages.size(); i++ )
+ {
+ buttonImages.get( i ).setScale( scale );
+ buttonImages.get( i ).fitPercent( buttonX.get( i ), buttonY.get( i ), w, h );
+ buttonMasks.get( i ).setScale( scale );
+ buttonMasks.get( i ).fitPercent( buttonX.get( i ), buttonY.get( i ), w, h );
+ }
+
+ // Recompute analog background location
+ if( analogBackImage != null )
+ {
+ analogBackImage.setScale( scale );
+ analogBackImage.fitPercent( analogBackX, analogBackY, w, h );
+ }
+ }
+
+ /**
+ * Gets the N64 button mapped to a given touch location.
+ *
+ * @param xLocation The x-coordinate of the touch, in pixels.
+ * @param yLocation The y-coordinate of the touch, in pixels.
+ *
+ * @return The N64 button the location is mapped to, or UNMAPPED.
+ *
+ * @see TouchMap#UNMAPPED
+ */
+ public int getButtonPress( int xLocation, int yLocation )
+ {
+ // Search through every button mask to see if the corresponding button was touched
+ for( Image mask : buttonMasks )
+ {
+ if( mask != null )
+ {
+ int left = mask.x;
+ int right = left + (int) ( mask.width * mask.scale );
+ int bottom = mask.y;
+ int top = bottom + (int) ( mask.height * mask.scale );
+
+ // See if the touch falls in the vicinity of the button (conservative test)
+ if( xLocation >= left && xLocation < right && yLocation >= bottom
+ && yLocation < top )
+ {
+ // Get the mask color at this location
+ int c = mask.image.getPixel( (int) ( ( xLocation - mask.x ) / scale ), (int) ( ( yLocation - mask.y ) / scale ) );
+
+ // Ignore the alpha component if any
+ int rgb = c & 0x00ffffff;
+
+ // Ignore black and get the N64 button associated with this color
+ if( rgb > 0 )
+ return getButtonFromColor( rgb );
+ }
+ }
+ }
+ return UNMAPPED;
+ }
+
+ /**
+ * Gets the frame for the N64 button with a given asset name
+ *
+ * @param assetName The asset name for the button
+ *
+ * @return The frame for the N64 button with the given asset name
+ *
+ */
+ public Rect getButtonFrame( String assetName )
+ {
+ for( int i = 0; i < buttonNames.size(); i++ )
+ {
+ if ( buttonNames.get( i ).equals( assetName ) )
+ return new Rect( buttonMasks.get( i ).drawRect );
+ }
+ return new Rect(0, 0, 0, 0);
+ }
+
+ /**
+ * Gets the N64 button mapped to a given mask color.
+ *
+ * @param color The mask color.
+ *
+ * @return The N64 button the color is mapped to, or UNMAPPED.
+ */
+ private int getButtonFromColor( int color )
+ {
+ // Find the N64 button whose mask matches the given color. Because we scale the mask images,
+ // the mask boundaries can get softened. Therefore we tolerate a bit of error in the match.
+ int closestMatch = UNMAPPED;
+ int matchDif = MATCH_TOLERANCE * MATCH_TOLERANCE;
+
+ // Get the RGB values of the given color
+ int r = ( color & 0xFF0000 ) >> 16;
+ int g = ( color & 0x00FF00 ) >> 8;
+ int b = ( color & 0x0000FF );
+
+ // Find the mask color with the smallest squared error
+ for( int i = 0; i < mN64ToColor.length; i++ )
+ {
+ int color2 = mN64ToColor[i];
+
+ // Compute squared error in RGB space
+ int difR = r - ( ( color2 & 0xFF0000 ) >> 16 );
+ int difG = g - ( ( color2 & 0x00FF00 ) >> 8 );
+ int difB = b - ( ( color2 & 0x0000FF ) );
+ int dif = difR * difR + difG * difG + difB * difB;
+
+ if( dif < matchDif )
+ {
+ closestMatch = i;
+ matchDif = dif;
+ }
+ }
+ return closestMatch;
+ }
+
+ /**
+ * Gets the N64 analog stick displacement.
+ *
+ * @param xLocation The x-coordinate of the touch, in pixels.
+ * @param yLocation The y-coordinate of the touch, in pixels.
+ *
+ * @return The analog displacement, in pixels.
+ */
+ public Point getAnalogDisplacement( int xLocation, int yLocation )
+ {
+ if( analogBackImage == null )
+ return new Point( 0, 0 );
+
+ // Distance from center along x-axis
+ int dX = xLocation - ( analogBackImage.x + (int) ( analogBackImage.hWidth * scale ) );
+
+ // Distance from center along y-axis
+ int dY = yLocation - ( analogBackImage.y + (int) ( analogBackImage.hHeight * scale ) );
+
+ return new Point( dX, dY );
+ }
+
+ /**
+ * Gets the N64 analog stick's frame.
+ *
+ * @return The analog stick's frame.
+ */
+ public Rect getAnalogFrame()
+ {
+ if( analogBackImage != null )
+ return new Rect( analogBackImage.drawRect );
+ return new Rect(0, 0, 0, 0);
+ }
+
+ /**
+ * Gets the N64 analog stick displacement, constrained to an octagon.
+ *
+ * @param dX The x-displacement of the stick, in pixels.
+ * @param dY The y-displacement of the stick, in pixels.
+ *
+ * @return The constrained analog displacement, in pixels.
+ */
+ public Point getConstrainedDisplacement( int dX, int dY )
+ {
+ final float dC = (int) ( analogMaximum * scale );
+ final float dA = dC * FloatMath.sqrt( 0.5f );
+ final float signX = (dX < 0) ? -1 : 1;
+ final float signY = (dY < 0) ? -1 : 1;
+
+ Point crossPt = new Point();
+ crossPt.x = dX;
+ crossPt.y = dY;
+
+ if( ( signX * dX ) > ( signY * dY ) )
+ segsCross( 0, 0, dX, dY, signX * dC, 0, signX * dA, signY * dA, crossPt );
+ else
+ segsCross( 0, 0, dX, dY, 0, signY * dC, signX * dA, signY * dA, crossPt );
+
+ return crossPt;
+ }
+
+ /**
+ * Gets the analog strength, accounting for deadzone and motion limits.
+ *
+ * @param displacement The Pythagorean displacement of the analog stick, in pixels.
+ *
+ * @return The analog strength, between 0 and 1, inclusive.
+ */
+ public float getAnalogStrength( float displacement )
+ {
+ displacement /= scale;
+ float p = ( displacement - analogDeadzone ) / ( analogMaximum - analogDeadzone );
+ return Utility.clamp( p, 0.0f, 1.0f );
+ }
+
+ /**
+ * Checks if a touch is within capture range of the analog stick.
+ *
+ * @param displacement The displacement of the touch with respect to analog center, in pixels.
+ *
+ * @return True, if the touch is in capture range of the stick.
+ */
+ public boolean isInCaptureRange( float displacement )
+ {
+ displacement /= scale;
+ return ( displacement >= analogDeadzone ) && ( displacement < analogMaximum + analogPadding );
+ }
+
+ /**
+ * Loads all touch map data from the filesystem.
+ *
+ * @param skinDir The directory containing the skin.ini and image files.
+ * @param profile The name of the layout profile.
+ * @param animated True to load the analog assets in two parts for animation.
+ */
+ public void load( String skinDir, Profile profile, boolean animated )
+ {
+ // Clear any old assets and map data
+ clear();
+
+ // Load the configuration files
+ skinFolder = skinDir;
+ ConfigFile skin_ini = new ConfigFile( skinFolder + "/skin.ini" );
+
+ // Look up the mask colors
+ loadMaskColors( skin_ini );
+
+ // Loop through all the configuration sections
+ loadAllAssets( profile, animated );
+ }
+
+ /**
+ * Loads the mask colors from a configuration file.
+ *
+ * @param skin_ini The configuration file containing mask info.
+ */
+ private void loadMaskColors( ConfigFile skin_ini )
+ {
+ ConfigSection section = skin_ini.get( "MASK_COLOR" );
+ if( section != null )
+ {
+ // Loop through the key-value pairs
+ for( String key : section.keySet() )
+ {
+ // Assign the map colors to the appropriate N64 button
+ String val = section.get( key );
+ Integer index = MASK_KEYS.get( key );
+ if( index != null )
+ {
+ try
+ {
+ mN64ToColor[index] = Integer.parseInt( val, 16 );
+ }
+ catch( NumberFormatException ex )
+ {
+ mN64ToColor[index] = -1;
+ Log.w( "TouchMap", "Invalid mask color '" + val + "' in " + skinFolder + "/skin.ini" );
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Loads all assets and properties specified in a profile.
+ *
+ * @param profile The touchscreen/touchpad profile.
+ * @param animated True to load the analog assets in two parts for animation.
+ */
+ protected void loadAllAssets( Profile profile, boolean animated )
+ {
+ if( profile != null )
+ {
+ loadAnalog( profile, animated );
+ loadButton( profile, "dpad" );
+ loadButton( profile, "groupAB" );
+ loadButton( profile, "groupC" );
+ loadButton( profile, "buttonL" );
+ loadButton( profile, "buttonR" );
+ loadButton( profile, "buttonZ" );
+ loadButton( profile, "buttonS" );
+ }
+ }
+
+ /**
+ * Loads analog assets and properties from the filesystem.
+ *
+ * @param profile The touchscreen/touchpad profile containing the analog properties.
+ * @param animated True to load the assets in two parts for animation.
+ */
+ protected void loadAnalog( Profile profile, boolean animated )
+ {
+ int x = profile.getInt( "analog-x", -1 );
+ int y = profile.getInt( "analog-y", -1 );
+
+ if( x >= 0 && y >= 0 )
+ {
+ // Position (percentages of the digitizer dimensions)
+ analogBackX = x;
+ analogBackY = y;
+
+ // The images (used by touchscreens) are in PNG image format.
+ if( animated )
+ {
+ analogBackImage = new Image( mResources, skinFolder + "/analog-back.png" );
+ analogForeImage = new Image( mResources, skinFolder + "/analog-fore.png" );
+ }
+ else
+ {
+ analogBackImage = new Image( mResources, skinFolder + "/analog.png" );
+ }
+
+ // Sensitivity (percentages of the radius, i.e. half the image width)
+ analogDeadzone = (int) ( analogBackImage.hWidth * ( profile.getFloat( "analog-min", 1 ) / 100.0f ) );
+ analogMaximum = (int) ( analogBackImage.hWidth * ( profile.getFloat( "analog-max", 55 ) / 100.0f ) );
+ analogPadding = (int) ( analogBackImage.hWidth * ( profile.getFloat( "analog-buff", 55 ) / 100.0f ) );
+ }
+ }
+
+ /**
+ * Loads button assets and properties from the filesystem.
+ *
+ * @param profile The touchscreen/touchpad profile containing the button properties.
+ * @param name The name of the button/group to load.
+ */
+ protected void loadButton( Profile profile, String name )
+ {
+ int x = profile.getInt( name + "-x", -1 );
+ int y = profile.getInt( name + "-y", -1 );
+
+ if( x >= 0 && y >= 0 )
+ {
+ // Position (percentages of the digitizer dimensions)
+ buttonX.add( x );
+ buttonY.add( y );
+ buttonNames.add( name );
+
+ // Load the displayed and mask images
+ buttonImages.add( new Image( mResources, skinFolder + "/" + name + ".png" ) );
+ buttonMasks.add( new Image( mResources, skinFolder + "/" + name + "-mask.png" ) );
+ }
+ }
+
+ /**
+ * Determines if the two specified line segments intersect with each other, and calculates where
+ * the intersection occurs if they do.
+ *
+ * @param seg1pt1_x X-coordinate for the first end of the first line segment.
+ * @param seg1pt1_y Y-coordinate for the first end of the first line segment.
+ * @param seg1pt2_x X-coordinate for the second end of the first line segment.
+ * @param seg1pt2_y Y-coordinate for the second end of the first line segment.
+ * @param seg2pt1_x X-coordinate for the first end of the second line segment.
+ * @param seg2pt1_y Y-coordinate for the first end of the second line segment.
+ * @param seg2pt2_x X-coordinate for the second end of the second line segment.
+ * @param seg2pt2_y Y-coordinate for the second end of the second line segment.
+ * @param crossPt Changed to the point of intersection if there is one, otherwise unchanged.
+ *
+ * @return True if the two line segments intersect.
+ */
+ private static boolean segsCross( float seg1pt1_x, float seg1pt1_y, float seg1pt2_x,
+ float seg1pt2_y, float seg2pt1_x, float seg2pt1_y, float seg2pt2_x, float seg2pt2_y,
+ Point crossPt )
+ {
+ float vec1_x = seg1pt2_x - seg1pt1_x;
+ float vec1_y = seg1pt2_y - seg1pt1_y;
+
+ float vec2_x = seg2pt2_x - seg2pt1_x;
+ float vec2_y = seg2pt2_y - seg2pt1_y;
+
+ float div = ( -vec2_x * vec1_y + vec1_x * vec2_y );
+
+ // Segments don't cross
+ if( div == 0 )
+ return false;
+
+ float s = ( -vec1_y * ( seg1pt1_x - seg2pt1_x ) + vec1_x * ( seg1pt1_y - seg2pt1_y ) ) / div;
+ float t = ( vec2_x * ( seg1pt1_y - seg2pt1_y ) - vec2_y * ( seg1pt1_x - seg2pt1_x ) ) / div;
+
+ if( s >= 0 && s < 1 && t >= 0 && t <= 1 )
+ {
+ // Segments cross, point of intersection stored in 'crossPt'
+ crossPt.x = (int) ( seg1pt1_x + ( t * vec1_x ) );
+ crossPt.y = (int) ( seg1pt1_y + ( t * vec1_y ) );
+ return true;
+ }
+
+ // Segments don't cross
+ return false;
+ }
+}
diff --git a/Android/src/emu/project64/input/map/VisibleTouchMap.java b/Android/src/emu/project64/input/map/VisibleTouchMap.java
new file mode 100644
index 000000000..4a9df40fc
--- /dev/null
+++ b/Android/src/emu/project64/input/map/VisibleTouchMap.java
@@ -0,0 +1,564 @@
+/****************************************************************************
+* *
+* Project64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.input.map;
+
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import emu.project64.game.GameOverlay;
+import emu.project64.persistent.ConfigFile;
+import emu.project64.profile.Profile;
+import emu.project64.util.Image;
+import emu.project64.util.SafeMethods;
+import emu.project64.util.Utility;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.util.DisplayMetrics;
+import android.util.Log;
+
+/**
+ * A kind of touch map that can be drawn on a canvas.
+ *
+ * @see TouchMap
+ * @see GameOverlay
+ */
+public class VisibleTouchMap extends TouchMap
+{
+ /** FPS frame image. */
+ private Image mFpsFrame;
+
+ /** X-coordinate of the FPS frame, in percent. */
+ private int mFpsFrameX;
+
+ /** Y-coordinate of the FPS frame, in percent. */
+ private int mFpsFrameY;
+
+ /** X-coordinate of the FPS text centroid, in percent. */
+ private int mFpsTextX;
+
+ /** Y-coordinate of the FPS text centroid, in percent. */
+ private int mFpsTextY;
+
+ /** The current FPS value. */
+ private int mFpsValue;
+
+ /** The minimum size of the FPS indicator in pixels. */
+ private float mFpsMinPixels;
+
+ /** The minimum size to scale the FPS indicator. */
+ private float mFpsMinScale;
+
+ /** True if the FPS indicator should be drawn. */
+ private boolean mFpsEnabled;
+
+ /** The factor to scale images by. */
+ private float mScalingFactor = 1.0f;
+
+ /** Touchscreen opacity. */
+ private int mTouchscreenTransparency;
+
+ /** Reference screen width in pixels (if provided in skin.ini). */
+ private int mReferenceWidth = 0;
+
+ /** Reference screen height in pixels (if provided in skin.ini). */
+ private int mReferenceHeight = 0;
+
+ /** The last width passed to {@link #resize(int, int, DisplayMetrics)}. */
+ private int cacheWidth = 0;
+
+ /** The last height passed to {@link #resize(int, int, DisplayMetrics)}. */
+ private int cacheHeight = 0;
+
+ /** The last height passed to {@link #resize(int, int, DisplayMetrics)}. */
+ private DisplayMetrics cacheMetrics;
+
+ /** The set of images representing the FPS string. */
+ private final CopyOnWriteArrayList mFpsDigits;
+
+ /** The set of images representing the numerals 0, 1, 2, ..., 9. */
+ private final Image[] mNumerals;
+
+ /** Auto-hold overlay images. */
+ public final Image[] autoHoldImages;
+
+ /** X-coordinates of the AutoHold mask, in percent. */
+ private final int[] autoHoldX;
+
+ /** Y-coordinates of the AutoHold mask, in percent. */
+ private final int[] autoHoldY;
+
+ /**
+ * Instantiates a new visible touch map.
+ *
+ * @param resources The resources of the activity associated with this touch map.
+ */
+ public VisibleTouchMap( Resources resources )
+ {
+ super( resources );
+ mFpsDigits = new CopyOnWriteArrayList();
+ mNumerals = new Image[10];
+ autoHoldImages = new Image[NUM_N64_PSEUDOBUTTONS];
+ autoHoldX = new int[NUM_N64_PSEUDOBUTTONS];
+ autoHoldY = new int[NUM_N64_PSEUDOBUTTONS];
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see emu.project64.input.map.TouchMap#clear()
+ */
+ @Override
+ public void clear()
+ {
+ super.clear();
+ mFpsFrame = null;
+ mFpsFrameX = mFpsFrameY = 0;
+ mFpsTextX = mFpsTextY = 50;
+ mFpsValue = 0;
+ mFpsDigits.clear();
+ for( int i = 0; i < mNumerals.length; i++ )
+ mNumerals[i] = null;
+ for( int i = 0; i < autoHoldImages.length; i++ )
+ autoHoldImages[i] = null;
+ for( int i = 0; i < autoHoldX.length; i++ )
+ autoHoldX[i] = 0;
+ for( int i = 0; i < autoHoldY.length; i++ )
+ autoHoldY[i] = 0;
+ }
+
+ /**
+ * Recomputes the map data for a given digitizer size, and
+ * recalculates the scaling factor.
+ *
+ * @param w The width of the digitizer, in pixels.
+ * @param h The height of the digitizer, in pixels.
+ * @param metrics Metrics about the display (for use in scaling).
+ */
+ public void resize( int w, int h, DisplayMetrics metrics )
+ {
+ // Cache the width and height in case we need to reload assets
+ cacheWidth = w;
+ cacheHeight = h;
+ cacheMetrics = metrics;
+ scale = 1.0f;
+
+ if( metrics != null )
+ {
+ // Scale buttons to match the skin designer's proportions
+ float scaleW = 1f;
+ float scaleH = 1f;
+ if( mReferenceWidth > 0 )
+ scaleW = Math.max( metrics.widthPixels, metrics.heightPixels ) / (float) mReferenceWidth;
+ if( mReferenceHeight > 0 )
+ scaleH = Math.min( metrics.widthPixels, metrics.heightPixels ) / (float) mReferenceHeight;
+ scale = Math.min( scaleW, scaleH );
+ }
+ // Apply the global scaling factor (derived from user prefs)
+ scale *= mScalingFactor;
+
+ resize( w, h );
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see emu.project64.input.map.TouchMap#resize(int, int)
+ */
+ @Override
+ public void resize( int w, int h )
+ {
+ super.resize( w, h );
+
+ // Compute analog foreground location (centered)
+ if( analogBackImage != null && analogForeImage != null )
+ {
+ int cX = analogBackImage.x + (int) ( analogBackImage.hWidth * scale );
+ int cY = analogBackImage.y + (int) ( analogBackImage.hHeight * scale );
+ analogForeImage.setScale( scale );
+ analogForeImage.fitCenter( cX, cY, analogBackImage.x, analogBackImage.y,
+ (int) ( analogBackImage.width * scale ), (int) ( analogBackImage.height * scale ) );
+ }
+
+ // Compute auto-hold overlay locations
+ for( int i = 0; i < autoHoldImages.length; i++ )
+ {
+ if( autoHoldImages[i] != null )
+ {
+ autoHoldImages[i].setScale( scale );
+ autoHoldImages[i].fitPercent( autoHoldX[i], autoHoldY[i], w, h );
+ }
+ }
+
+ // Compute FPS frame location
+ float fpsScale = scale;
+ if( mFpsMinScale > scale )
+ fpsScale = mFpsMinScale;
+ if( mFpsFrame != null )
+ {
+ mFpsFrame.setScale( fpsScale );
+ mFpsFrame.fitPercent( mFpsFrameX, mFpsFrameY, w, h );
+ }
+ for( int i = 0; i < mNumerals.length; i++ )
+ {
+ if( mNumerals[i] != null )
+ mNumerals[i].setScale( fpsScale );
+ }
+
+ // Compute the FPS digit locations
+ refreshFpsImages();
+ refreshFpsPositions();
+ }
+
+ /**
+ * Draws the buttons.
+ *
+ * @param canvas The canvas on which to draw.
+ */
+ public void drawButtons( Canvas canvas )
+ {
+ // Draw the buttons onto the canvas
+ for( Image button : buttonImages )
+ {
+ button.draw( canvas );
+ }
+ }
+
+ /**
+ * Draws the AutoHold mask.
+ *
+ * @param canvas The canvas on which to draw.
+ */
+ public void drawAutoHold( Canvas canvas )
+ {
+ // Draw the AutoHold mask onto the canvas
+ for( Image autoHoldImage : autoHoldImages )
+ {
+ if( autoHoldImage != null )
+ {
+ autoHoldImage.draw( canvas );
+ }
+ }
+ }
+
+ /**
+ * Draws the analog stick.
+ *
+ * @param canvas The canvas on which to draw.
+ */
+ public void drawAnalog( Canvas canvas )
+ {
+ // Draw the background image
+ if( analogBackImage != null )
+ {
+ analogBackImage.draw( canvas );
+ }
+
+ // Draw the movable foreground (the stick)
+ if( analogForeImage != null )
+ {
+ analogForeImage.draw( canvas );
+ }
+ }
+
+ /**
+ * Draws the FPS indicator.
+ *
+ * @param canvas The canvas on which to draw.
+ */
+ public void drawFps( Canvas canvas )
+ {
+ if( canvas == null )
+ return;
+
+ // Redraw the FPS indicator
+ if( mFpsFrame != null )
+ mFpsFrame.draw( canvas );
+
+ // Draw each digit of the FPS number
+ for( Image digit : mFpsDigits )
+ digit.draw( canvas );
+ }
+
+ /**
+ * Updates the analog stick assets to reflect a new position.
+ *
+ * @param axisFractionX The x-axis fraction, between -1 and 1, inclusive.
+ * @param axisFractionY The y-axis fraction, between -1 and 1, inclusive.
+ *
+ * @return True if the analog assets changed.
+ */
+ public boolean updateAnalog( float axisFractionX, float axisFractionY )
+ {
+ if( analogForeImage != null && analogBackImage != null )
+ {
+ // Get the location of stick center
+ int hX = (int) ( ( analogBackImage.hWidth + ( axisFractionX * analogMaximum ) ) * scale );
+ int hY = (int) ( ( analogBackImage.hHeight - ( axisFractionY * analogMaximum ) ) * scale );
+
+ // Use other values if invalid
+ if( hX < 0 )
+ hX = (int) ( analogBackImage.hWidth * scale );
+ if( hY < 0 )
+ hY = (int) ( analogBackImage.hHeight * scale );
+
+ // Update the position of the stick
+ int cX = analogBackImage.x + hX;
+ int cY = analogBackImage.y + hY;
+ analogForeImage.fitCenter( cX, cY, analogBackImage.x, analogBackImage.y,
+ (int) ( analogBackImage.width * scale ), (int) ( analogBackImage.height * scale ) );
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Updates the FPS indicator assets to reflect a new value.
+ *
+ * @param fps The new FPS value.
+ *
+ * @return True if the FPS assets changed.
+ */
+ public boolean updateFps( int fps )
+ {
+ // Clamp to positive, four digits max [0 - 9999]
+ fps = Utility.clamp( fps, 0, 9999 );
+
+ // Quick return if user has disabled FPS or it hasn't changed
+ if( !mFpsEnabled || mFpsValue == fps )
+ return false;
+
+ // Store the new value
+ mFpsValue = fps;
+
+ // Refresh the FPS digits
+ refreshFpsImages();
+ refreshFpsPositions();
+
+ return true;
+ }
+
+ /**
+ * Updates the auto-hold assets to reflect a new value.
+ *
+ * @param pressed The new autohold state value.
+ * @param index The index of the auto-hold mask.
+ *
+ * @return True if the autohold assets changed.
+ */
+ public boolean updateAutoHold( boolean pressed, int index )
+ {
+ if( autoHoldImages[index] != null )
+ {
+ if( pressed )
+ autoHoldImages[index].setAlpha( mTouchscreenTransparency );
+ else
+ autoHoldImages[index].setAlpha( 0 );
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Refreshes the images used to draw the FPS string.
+ */
+ private void refreshFpsImages()
+ {
+ // Refresh the list of FPS digits
+ String fpsString = Integer.toString( mFpsValue );
+ mFpsDigits.clear();
+ for( int i = 0; i < 4; i++ )
+ {
+ // Create a new sequence of numeral images
+ if( i < fpsString.length() )
+ {
+ int numeral = SafeMethods.toInt( fpsString.substring( i, i + 1 ), -1 );
+ if( numeral > -1 && numeral < 10 )
+ {
+ // Clone the numeral from the font images and move to next digit
+ mFpsDigits.add( new Image( mResources, mNumerals[numeral] ) );
+ }
+ }
+ }
+ }
+
+ /**
+ * Refreshes the positions of the FPS images.
+ */
+ private void refreshFpsPositions()
+ {
+ // Compute the centroid of the FPS text
+ int x = 0;
+ int y = 0;
+ if( mFpsFrame != null )
+ {
+ x = mFpsFrame.x + (int) ( ( mFpsFrame.width * mFpsFrame.scale ) * ( mFpsTextX / 100f ) );
+ y = mFpsFrame.y + (int) ( ( mFpsFrame.height * mFpsFrame.scale ) * ( mFpsTextY / 100f ) );
+ }
+
+ // Compute the width of the FPS text
+ int totalWidth = 0;
+ for( Image digit : mFpsDigits )
+ totalWidth += (int) ( digit.width * digit.scale );
+
+ // Compute the starting position of the FPS text
+ x -= (int) ( totalWidth / 2f );
+
+ // Compute the position of each digit
+ for( Image digit : mFpsDigits )
+ {
+ digit.setPos( x, y - (int) ( digit.hHeight * digit.scale ) );
+ x += (int) ( digit.width * digit.scale );
+ }
+ }
+
+ /**
+ * Loads all touch map data from the filesystem.
+ *
+ * @param skinDir The directory containing the skin.ini and image files.
+ * @param profile The name of the touchscreen profile.
+ * @param animated True to load the analog assets in two parts for animation.
+ * @param fpsEnabled True to display the FPS indicator.
+ * @param scale The factor to scale images by.
+ * @param alpha The opacity of the visible elements.
+ */
+ public void load( String skinDir, Profile profile, boolean animated, boolean fpsEnabled, float scale, int alpha )
+ {
+ mFpsEnabled = fpsEnabled;
+ mScalingFactor = scale;
+ mTouchscreenTransparency = alpha;
+
+ super.load( skinDir, profile, animated );
+ ConfigFile skin_ini = new ConfigFile( skinFolder + "/skin.ini" );
+ mReferenceWidth = SafeMethods.toInt( skin_ini.get( "INFO", "referenceScreenWidth" ), 0 );
+ mReferenceHeight = SafeMethods.toInt( skin_ini.get( "INFO", "referenceScreenHeight" ), 0 );
+ mFpsTextX = SafeMethods.toInt( skin_ini.get( "INFO", "fps-numx" ), 50 );
+ mFpsTextY = SafeMethods.toInt( skin_ini.get( "INFO", "fps-numy" ), 50 );
+ mFpsMinPixels = SafeMethods.toInt( skin_ini.get( "INFO", "fps-minPixels" ), 0 );
+
+ // Scale the assets to the last screensize used
+ resize( cacheWidth, cacheHeight, cacheMetrics );
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * emu.project64.input.map.TouchMap#loadAllAssets(emu.project64
+ * .profile.Profile, boolean)
+ */
+ @Override
+ protected void loadAllAssets( Profile profile, boolean animated )
+ {
+ super.loadAllAssets( profile, animated );
+
+ // Set the transparency of the images
+ for( Image buttonImage : buttonImages )
+ {
+ buttonImage.setAlpha( mTouchscreenTransparency );
+ }
+ if( analogBackImage != null )
+ {
+ analogBackImage.setAlpha( mTouchscreenTransparency );
+ }
+ if( analogForeImage != null )
+ {
+ analogForeImage.setAlpha( mTouchscreenTransparency );
+ }
+
+ // Load the FPS and autohold images
+ if( profile != null )
+ {
+ loadFpsIndicator( profile );
+ loadAutoHoldImages( profile, "groupAB-holdA" );
+ loadAutoHoldImages( profile, "groupAB-holdB" );
+ loadAutoHoldImages( profile, "groupC-holdCu" );
+ loadAutoHoldImages( profile, "groupC-holdCd" );
+ loadAutoHoldImages( profile, "groupC-holdCl" );
+ loadAutoHoldImages( profile, "groupC-holdCr" );
+ loadAutoHoldImages( profile, "buttonL-holdL" );
+ loadAutoHoldImages( profile, "buttonR-holdR" );
+ loadAutoHoldImages( profile, "buttonZ-holdZ" );
+ loadAutoHoldImages( profile, "buttonS-holdS" );
+ }
+ }
+
+ /**
+ * Loads FPS indicator assets and properties from the filesystem.
+ *
+ * @param profile The touchscreen profile containing the FPS properties.
+ */
+ private void loadFpsIndicator( Profile profile )
+ {
+ int x = profile.getInt( "fps-x", -1 );
+ int y = profile.getInt( "fps-y", -1 );
+
+ if( x >= 0 && y >= 0 )
+ {
+ // Position (percentages of the screen dimensions)
+ mFpsFrameX = x;
+ mFpsFrameY = y;
+
+ // Load frame image
+ mFpsFrame = new Image( mResources, skinFolder + "/fps.png" );
+
+ // Minimum factor the FPS indicator can be scaled by
+ mFpsMinScale = mFpsMinPixels / (float) mFpsFrame.width;
+
+ // Load numeral images
+ String filename = "";
+ try
+ {
+ // Make sure we can load them (they might not even exist)
+ for( int i = 0; i < mNumerals.length; i++ )
+ {
+ filename = skinFolder + "/fps-" + i + ".png";
+ mNumerals[i] = new Image( mResources, filename );
+ }
+ }
+ catch( Exception e )
+ {
+ // Problem, let the user know
+ Log.e( "VisibleTouchMap", "Problem loading fps numeral '" + filename
+ + "', error message: " + e.getMessage() );
+ }
+ }
+ }
+
+ /**
+ * Loads auto-hold assets and properties from the filesystem.
+ *
+ * @param profile The touchscreen profile containing the auto-hold properties.
+ * @param name The name of the image to load.
+ */
+ private void loadAutoHoldImages( Profile profile, String name )
+ {
+ if ( !name.contains("-hold") )
+ return;
+
+ String[] fields = name.split( "-hold" );
+ String group = fields[0];
+ String hold = fields[1];
+
+ int x = profile.getInt( group + "-x", -1 );
+ int y = profile.getInt( group + "-y", -1 );
+ Integer index = MASK_KEYS.get( hold );
+
+ if( x >= 0 && y >= 0 && index != null )
+ {
+ // Position (percentages of the digitizer dimensions)
+ autoHoldX[index] = x;
+ autoHoldY[index] = y;
+
+ // The drawable image is in PNG image format.
+ autoHoldImages[index] = new Image( mResources, skinFolder + "/" + name + ".png" );
+ autoHoldImages[index].setAlpha( 0 );
+ }
+ }
+}
diff --git a/Android/src/emu/project64/jni/NativeConstants.java b/Android/src/emu/project64/jni/NativeConstants.java
new file mode 100644
index 000000000..e20da1250
--- /dev/null
+++ b/Android/src/emu/project64/jni/NativeConstants.java
@@ -0,0 +1,21 @@
+/****************************************************************************
+* *
+* Project 64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.jni;
+
+public class NativeConstants
+{
+ // @formatter:off
+ public static final int EMULATOR_STATE_UNKNOWN = 0;
+ public static final int EMULATOR_STATE_IDLE = 1;
+ public static final int EMULATOR_STATE_RUNNING = 2;
+ public static final int EMULATOR_STATE_PAUSED = 3;
+ // @formatter:on
+}
diff --git a/Android/src/emu/project64/jni/NativeExports.java b/Android/src/emu/project64/jni/NativeExports.java
new file mode 100644
index 000000000..73d28c005
--- /dev/null
+++ b/Android/src/emu/project64/jni/NativeExports.java
@@ -0,0 +1,49 @@
+/****************************************************************************
+* *
+* Project 64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.jni;
+
+import android.app.Activity;
+import emu.project64.game.GameSurface;
+
+public class NativeExports
+{
+ static
+ {
+ System.loadLibrary( "Project64-bridge" );
+ }
+
+ public static native void appInit (String BaseDir );
+ public static native void StopEmulation();
+ public static native void StartEmulation();
+ public static native void CloseSystem();
+ public static native void SettingsSaveBool(int type, boolean value);
+ public static native void SettingsSaveDword(int type, int value);
+ public static native void SettingsSaveString(int type, String value);
+ public static native boolean SettingsLoadBool(int type);
+ public static native int SettingsLoadDword(int type);
+ public static native String SettingsLoadString(int type);
+ public static native boolean IsSettingSet(int type);
+ public static native void LoadGame(String FileLoc);
+ public static native void StartGame(Activity activity, GameSurface.GLThread thread);
+ public static native void LoadRomList();
+ public static native void RefreshRomDir(String RomDir, boolean Recursive);
+ public static native void ExternalEvent(int Type);
+
+ public static native void onSurfaceCreated();
+ public static native void onSurfaceChanged(int width, int height);
+ public static native void onDrawFrame();
+
+ public static native void UISettingsSaveBool(int Type, boolean Value);
+ public static native void UISettingsSaveDword(int Type, int Value);
+
+ public static native boolean UISettingsLoadBool(int Type);
+ public static native int UISettingsLoadDword(int Type);
+}
diff --git a/Android/src/emu/project64/jni/NativeInput.java b/Android/src/emu/project64/jni/NativeInput.java
new file mode 100644
index 000000000..04b1884fc
--- /dev/null
+++ b/Android/src/emu/project64/jni/NativeInput.java
@@ -0,0 +1,36 @@
+/****************************************************************************
+* *
+* Project 64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.jni;
+
+/**
+ * Calls made between the native input-android library and Java. Any function names changed here
+ * should also be changed in the corresponding C code, and vice versa.
+ *
+ * @see /Source/Android/PluginInput/Main.cpp
+ * @see CoreInterface
+ */
+public class NativeInput
+{
+ static
+ {
+ System.loadLibrary( "Project64-input-android" );
+ }
+
+ /**
+ * Set the button/axis state of a controller.
+ *
+ * @param controllerNum Controller index, in the range [0,3].
+ * @param buttons The pressed state of the buttons.
+ * @param axisX The analog value of the x-axis, in the range [-80,80].
+ * @param axisY The analog value of the y-axis, in the range [-80,80].
+ */
+ public static native void setState( int controllerNum, boolean[] buttons, int axisX, int axisY );
+}
diff --git a/Android/src/emu/project64/jni/NativeXperiaTouchpad.java b/Android/src/emu/project64/jni/NativeXperiaTouchpad.java
new file mode 100644
index 000000000..a9bd219b1
--- /dev/null
+++ b/Android/src/emu/project64/jni/NativeXperiaTouchpad.java
@@ -0,0 +1,27 @@
+/****************************************************************************
+* *
+* Project 64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.jni;
+
+import android.content.Context;
+import android.view.View;
+
+public class NativeXperiaTouchpad extends View
+{
+ /**
+ * Instantiates a new native input source.
+ *
+ * @param context The context associated with the input events.
+ */
+ public NativeXperiaTouchpad( Context context )
+ {
+ super( context );
+ }
+}
diff --git a/Android/src/emu/project64/jni/SettingsID.java b/Android/src/emu/project64/jni/SettingsID.java
new file mode 100644
index 000000000..79bafdfff
--- /dev/null
+++ b/Android/src/emu/project64/jni/SettingsID.java
@@ -0,0 +1,315 @@
+/****************************************************************************
+* *
+* Project 64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.jni;
+
+public enum SettingsID
+{
+ //Default values
+ Default_None,
+ Default_Constant,
+
+ //Command Settings
+ Cmd_BaseDirectory,
+ Cmd_RomFile,
+ Cmd_ShowHelp,
+
+ //Support Files
+ SupportFile_Settings,
+ SupportFile_SettingsDefault,
+ SupportFile_RomDatabase,
+ SupportFile_RomDatabaseDefault,
+ SupportFile_Glide64RDB,
+ SupportFile_Glide64RDBDefault,
+ SupportFile_Cheats,
+ SupportFile_CheatsDefault,
+ SupportFile_Notes,
+ SupportFile_NotesDefault,
+ SupportFile_ExtInfo,
+ SupportFile_ExtInfoDefault,
+
+ //Settings
+ Setting_ApplicationName,
+ Setting_UseFromRegistry,
+ Setting_RdbEditor,
+ Setting_CN64TimeCritical,
+ Setting_AutoStart,
+ Setting_CheckEmuRunning,
+ Setting_EraseGameDefaults,
+
+ Setting_AutoZipInstantSave,
+ Setting_RememberCheats,
+ Setting_UniqueSaveDir,
+ Setting_LanguageDir,
+ Setting_LanguageDirDefault,
+ Setting_CurrentLanguage,
+ Setting_EnableDisk,
+
+ //RDB Settings
+ Rdb_GoodName,
+ Rdb_SaveChip,
+ Rdb_CpuType,
+ Rdb_RDRamSize,
+ Rdb_CounterFactor,
+ Rdb_UseTlb,
+ Rdb_DelayDP,
+ Rdb_DelaySi,
+ Rdb_32Bit,
+ Rdb_FastSP,
+ Rdb_FixedAudio,
+ Rdb_SyncViaAudio,
+ Rdb_RspAudioSignal,
+ Rdb_TLB_VAddrStart,
+ Rdb_TLB_VAddrLen,
+ Rdb_TLB_PAddrStart,
+ Rdb_UseHleGfx,
+ Rdb_UseHleAudio,
+ Rdb_LoadRomToMemory,
+ Rdb_ScreenHertz,
+ Rdb_FuncLookupMode,
+ Rdb_RegCache,
+ Rdb_BlockLinking,
+ Rdb_SMM_StoreInstruc,
+ Rdb_SMM_Cache,
+ Rdb_SMM_PIDMA,
+ Rdb_SMM_TLB,
+ Rdb_SMM_Protect,
+ Rdb_SMM_ValidFunc,
+ Rdb_GameCheatFix,
+ Rdb_GameCheatFixPlugin,
+ Rdb_ViRefreshRate,
+ Rdb_AiCountPerBytes,
+ Rdb_AudioResetOnLoad,
+ Rdb_AllowROMWrites,
+ Rdb_CRC_Recalc,
+
+ //Individual Game Settings
+ Game_IniKey,
+ Game_File,
+ Game_UniqueSaveDir,
+ Game_GameName,
+ Game_GoodName,
+ Game_TempLoaded,
+ Game_SystemType,
+ Game_EditPlugin_Gfx,
+ Game_EditPlugin_Audio,
+ Game_EditPlugin_Contr,
+ Game_EditPlugin_RSP,
+ Game_Plugin_Gfx,
+ Game_Plugin_Audio,
+ Game_Plugin_Controller,
+ Game_Plugin_RSP,
+ Game_SaveChip,
+ Game_CpuType,
+ Game_LastSaveSlot,
+ Game_FixedAudio,
+ Game_SyncViaAudio,
+ Game_32Bit,
+ Game_SMM_Cache,
+ Game_SMM_Protect,
+ Game_SMM_ValidFunc,
+ Game_SMM_PIDMA,
+ Game_SMM_TLB,
+ Game_SMM_StoreInstruc,
+ Game_CurrentSaveState,
+ Game_LastSaveTime,
+ Game_RDRamSize,
+ Game_CounterFactor,
+ Game_UseTlb,
+ Game_DelayDP,
+ Game_DelaySI,
+ Game_FastSP,
+ Game_FuncLookupMode,
+ Game_RegCache,
+ Game_BlockLinking,
+ Game_ScreenHertz,
+ Game_RspAudioSignal,
+ Game_UseHleGfx,
+ Game_UseHleAudio,
+ Game_LoadRomToMemory,
+ Game_ViRefreshRate,
+ Game_AiCountPerBytes,
+ Game_AudioResetOnLoad,
+ Game_AllowROMWrites,
+ Game_CRC_Recalc,
+ Game_Transferpak_ROM,
+ Game_Transferpak_Sav,
+
+ // General Game running info
+ GameRunning_LoadingInProgress,
+ GameRunning_CPU_Running,
+ GameRunning_CPU_Paused,
+ GameRunning_CPU_PausedType,
+ GameRunning_InstantSaveFile,
+ GameRunning_LimitFPS,
+ GameRunning_ScreenHertz,
+ GameRunning_InReset,
+
+ //User Interface
+ UserInterface_BasicMode,
+ UserInterface_ShowCPUPer,
+ UserInterface_DisplayFrameRate,
+ UserInterface_FrameDisplayType,
+
+ //Directory Info
+ Directory_Plugin,
+ Directory_PluginInitial,
+ Directory_PluginSelected,
+ Directory_PluginUseSelected,
+ Directory_PluginSync,
+ Directory_SnapShot,
+ Directory_SnapShotInitial,
+ Directory_SnapShotSelected,
+ Directory_SnapShotUseSelected,
+ Directory_NativeSave,
+ Directory_NativeSaveInitial,
+ Directory_NativeSaveSelected,
+ Directory_NativeSaveUseSelected,
+ Directory_InstantSave,
+ Directory_InstantSaveInitial,
+ Directory_InstantSaveSelected,
+ Directory_InstantSaveUseSelected,
+ Directory_Texture,
+ Directory_TextureInitial,
+ Directory_TextureSelected,
+ Directory_TextureUseSelected,
+ Directory_Log,
+ Directory_LogInitial,
+ Directory_LogSelected,
+ Directory_LogUseSelected,
+
+ //Rom List
+ RomList_RomListCache,
+ RomList_RomListCacheDefault,
+ RomList_GameDir,
+ RomList_GameDirInitial,
+ RomList_GameDirSelected,
+ RomList_GameDirUseSelected,
+ RomList_GameDirRecursive,
+ RomList_7zipCache,
+ RomList_7zipCacheDefault,
+
+ //File Info
+ File_DiskIPLPath,
+
+ //Debugger
+ Debugger_Enabled,
+ Debugger_ShowTLBMisses,
+ Debugger_ShowUnhandledMemory,
+ Debugger_ShowPifErrors,
+ Debugger_ShowDivByZero,
+ Debugger_GenerateLogFiles,
+ Debugger_ProfileCode,
+ Debugger_DisableGameFixes,
+ Debugger_AppLogLevel,
+ Debugger_AppLogFlush,
+ Debugger_ShowDListAListCount,
+ Debugger_ShowRecompMemSize,
+ Debugger_DebugLanguage,
+
+ //Trace
+ Debugger_TraceMD5,
+ Debugger_TraceThread,
+ Debugger_TracePath,
+ Debugger_TraceSettings,
+ Debugger_TraceUnknown,
+ Debugger_TraceAppInit,
+ Debugger_TraceAppCleanup,
+ Debugger_TraceN64System,
+ Debugger_TracePlugins,
+ Debugger_TraceGFXPlugin,
+ Debugger_TraceAudioPlugin,
+ Debugger_TraceControllerPlugin,
+ Debugger_TraceRSPPlugin,
+ Debugger_TraceRSP,
+ Debugger_TraceAudio,
+ Debugger_TraceRegisterCache,
+ Debugger_TraceRecompiler,
+ Debugger_TraceTLB,
+ Debugger_TraceProtectedMEM,
+ Debugger_TraceUserInterface,
+ Debugger_TraceRomList,
+ Debugger_TraceExceptionHandler,
+
+ //Plugins
+ Plugin_RSP_Current,
+ Plugin_RSP_CurVer,
+ Plugin_GFX_Current,
+ Plugin_GFX_CurVer,
+ Plugin_AUDIO_Current,
+ Plugin_AUDIO_CurVer,
+ Plugin_CONT_Current,
+ Plugin_CONT_CurVer,
+ Plugin_UseHleGfx,
+ Plugin_UseHleAudio,
+
+ Logging_GenerateLog,
+ Logging_LogRDRamRegisters,
+ Logging_LogSPRegisters,
+ Logging_LogDPCRegisters,
+ Logging_LogDPSRegisters,
+ Logging_LogMIPSInterface,
+ Logging_LogVideoInterface,
+ Logging_LogAudioInterface,
+ Logging_LogPerInterface,
+ Logging_LogRDRAMInterface,
+ Logging_LogSerialInterface,
+ Logging_LogPRDMAOperations,
+ Logging_LogPRDirectMemLoads,
+ Logging_LogPRDMAMemLoads,
+ Logging_LogPRDirectMemStores,
+ Logging_LogPRDMAMemStores,
+ Logging_LogControllerPak,
+ Logging_LogCP0changes,
+ Logging_LogCP0reads,
+ Logging_LogTLB,
+ Logging_LogExceptions,
+ Logging_NoInterrupts,
+ Logging_LogCache,
+ Logging_LogRomHeader,
+ Logging_LogUnknown,
+
+ //Cheats
+ Cheat_Entry,
+ Cheat_Active,
+ Cheat_Extension,
+ Cheat_Notes,
+ Cheat_Options,
+ Cheat_Range,
+ Cheat_RangeNotes,
+
+ /*, FirstUISettings, LastUISettings = FirstUISettings + MaxPluginSetting,
+ FirstRSPDefaultSet, LastRSPDefaultSet = FirstRSPDefaultSet + MaxPluginSetting,
+ FirstRSPSettings, LastRSPSettings = FirstRSPSettings + MaxPluginSetting,
+ FirstGfxDefaultSet, LastGfxDefaultSet = FirstGfxDefaultSet + MaxPluginSetting,
+ FirstGfxSettings, LastGfxSettings = FirstGfxSettings + MaxPluginSetting,
+ FirstAudioDefaultSet, LastAudioDefaultSet = FirstAudioDefaultSet + MaxPluginSetting,
+ FirstAudioSettings, LastAudioSettings = FirstAudioSettings + MaxPluginSetting,
+ FirstCtrlDefaultSet, LastCtrlDefaultSet = FirstCtrlDefaultSet + MaxPluginSetting,
+ FirstCtrlSettings, LastCtrlSettings = FirstCtrlSettings + MaxPluginSetting,
+ ;*/
+ ;
+ private int value;
+
+ public int getValue()
+ {
+ return this.value;
+ }
+ private static final class StaticFields
+ {
+ public static int Counter = 0;
+ }
+
+ private SettingsID()
+ {
+ this.value = StaticFields.Counter;
+ StaticFields.Counter += 1;
+ }
+}
\ No newline at end of file
diff --git a/Android/src/emu/project64/jni/SystemEvent.java b/Android/src/emu/project64/jni/SystemEvent.java
new file mode 100644
index 000000000..4beadf271
--- /dev/null
+++ b/Android/src/emu/project64/jni/SystemEvent.java
@@ -0,0 +1,71 @@
+/****************************************************************************
+* *
+* Project 64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.jni;
+
+public enum SystemEvent
+{
+ SysEvent_ExecuteInterrupt,
+ SysEvent_GSButtonPressed,
+ SysEvent_ResetCPU_Soft,
+ SysEvent_ResetCPU_SoftDone,
+ SysEvent_ResetCPU_Hard,
+ SysEvent_CloseCPU,
+ SysEvent_PauseCPU_FromMenu,
+ SysEvent_PauseCPU_AppLostActive,
+ SysEvent_PauseCPU_AppLostActiveDelay,
+ SysEvent_PauseCPU_AppLostFocus,
+ SysEvent_PauseCPU_SaveGame,
+ SysEvent_PauseCPU_LoadGame,
+ SysEvent_PauseCPU_DumpMemory,
+ SysEvent_PauseCPU_SearchMemory,
+ SysEvent_PauseCPU_Settings,
+ SysEvent_PauseCPU_Cheats,
+ SysEvent_ResumeCPU_FromMenu,
+ SysEvent_ResumeCPU_AppGainedActive,
+ SysEvent_ResumeCPU_AppGainedFocus,
+ SysEvent_ResumeCPU_SaveGame,
+ SysEvent_ResumeCPU_LoadGame,
+ SysEvent_ResumeCPU_DumpMemory,
+ SysEvent_ResumeCPU_SearchMemory,
+ SysEvent_ResumeCPU_Settings,
+ SysEvent_ResumeCPU_Cheats,
+ SysEvent_ChangingFullScreen,
+ SysEvent_ChangePlugins,
+ SysEvent_SaveMachineState,
+ SysEvent_LoadMachineState,
+ SysEvent_Interrupt_SP,
+ SysEvent_Interrupt_SI,
+ SysEvent_Interrupt_AI,
+ SysEvent_Interrupt_VI,
+ SysEvent_Interrupt_PI,
+ SysEvent_Interrupt_DP,
+ SysEvent_Profile_StartStop,
+ SysEvent_Profile_ResetLogs,
+ SysEvent_Profile_GenerateLogs
+ ;
+
+ private int value;
+
+ public int getValue()
+ {
+ return this.value;
+ }
+ private static final class StaticFields
+ {
+ public static int Counter = 0;
+ }
+
+ private SystemEvent()
+ {
+ this.value = StaticFields.Counter;
+ StaticFields.Counter += 1;
+ }
+}
diff --git a/Android/src/emu/project64/jni/UISettingID.java b/Android/src/emu/project64/jni/UISettingID.java
new file mode 100644
index 000000000..209ad6e5b
--- /dev/null
+++ b/Android/src/emu/project64/jni/UISettingID.java
@@ -0,0 +1,35 @@
+/****************************************************************************
+* *
+* Project 64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.jni;
+
+public enum UISettingID
+{
+ Asserts_Version,
+ Screen_Orientation,
+ ;
+
+ private int value;
+
+ public int getValue()
+ {
+ return this.value;
+ }
+ private static final class StaticFields
+ {
+ public static int Counter = 0;
+ }
+
+ private UISettingID()
+ {
+ this.value = StaticFields.Counter;
+ StaticFields.Counter += 1;
+ }
+}
diff --git a/Android/src/emu/project64/persistent/ConfigFile.java b/Android/src/emu/project64/persistent/ConfigFile.java
new file mode 100644
index 000000000..a72d185bb
--- /dev/null
+++ b/Android/src/emu/project64/persistent/ConfigFile.java
@@ -0,0 +1,539 @@
+/****************************************************************************
+* *
+* Project64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.persistent;
+
+import java.io.BufferedReader;
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.Set;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+public class ConfigFile
+{
+ /** The name we use for the untitled section (preamble) of the config file. */
+ public static final String SECTIONLESS_NAME = "[]";
+
+ /** Name of the config file. */
+ private final String mFilename;
+
+ /** Sections mapped by title for easy lookup, with insertion order retained. */
+ private final LinkedHashMap mConfigMap;
+
+ /**
+ * Reads the entire config file, and saves the data to internal collections for manipulation.
+ *
+ * @param filename The config file to read from.
+ */
+ public ConfigFile( String filename )
+ {
+ mFilename = filename;
+ mConfigMap = new LinkedHashMap();
+ reload();
+ }
+
+ /**
+ * Looks up a config section by its title.
+ *
+ * @param sectionTitle Title of the section containing the parameter.
+ *
+ * @return A ConfigSection containing parameters, or null if not found.
+ */
+ public ConfigSection get( String sectionTitle )
+ {
+ return mConfigMap.get( sectionTitle );
+ }
+
+ /**
+ * Looks up the specified parameter under the specified section title.
+ *
+ * @param sectionTitle Title of the section containing the parameter.
+ * @param parameter Name of the parameter.
+ *
+ * @return The value of the specified parameter, or null if not found.
+ */
+ public String get( String sectionTitle, String parameter )
+ {
+ ConfigSection section = mConfigMap.get( sectionTitle );
+
+ // The specified section doesn't exist or is empty.. quit
+ if( section == null || section.parameters == null )
+ return null;
+
+ ConfigParameter confParam = section.parameters.get( parameter );
+
+ // The specified parameter doesn't exist.. quit
+ if( confParam == null )
+ return null;
+
+ // Got it
+ return confParam.value;
+ }
+
+ /**
+ * Assigns the specified value to the specified parameter under the specified section.
+ *
+ * @param sectionTitle The title of the section to contain the parameter.
+ * @param parameter The name of the parameter.
+ * @param value The value to give the parameter.
+ */
+ public void put( String sectionTitle, String parameter, String value )
+ {
+ ConfigSection section = mConfigMap.get( sectionTitle );
+ if( section == null )
+ {
+ // Add a new section
+ section = new ConfigSection( sectionTitle );
+ mConfigMap.put( sectionTitle, section );
+ }
+ section.put( parameter, value );
+ }
+
+ /**
+ * Erases any previously loaded data.
+ */
+ public void clear()
+ {
+ mConfigMap.clear();
+ }
+
+ /**
+ * Re-loads the entire config file, overwriting any unsaved changes, and saves the data in
+ * 'configMap'.
+ *
+ * @return True if successful.
+ * @see #save()
+ */
+ public boolean reload()
+ {
+ // Make sure a file was actually specified
+ if( TextUtils.isEmpty( mFilename ) )
+ return false;
+
+ // Free any previously loaded data
+ clear();
+
+ FileInputStream fstream;
+ try
+ {
+ fstream = new FileInputStream( mFilename );
+ }
+ catch( FileNotFoundException fnfe )
+ {
+ // File not found... we can't continue
+ return false;
+ }
+
+ DataInputStream in = new DataInputStream( fstream );
+ BufferedReader br = new BufferedReader( new InputStreamReader( in ) );
+
+ String sectionName = SECTIONLESS_NAME;
+ ConfigSection section = new ConfigSection( sectionName, br ); // Read the 'sectionless'
+ // section
+ mConfigMap.put( sectionName, section ); // Save the data to 'configMap'
+
+ // Loop through reading the remaining sections
+ while( !TextUtils.isEmpty( section.nextName ) )
+ {
+ // Get the next section name
+ sectionName = section.nextName;
+
+ // Load the next section
+ section = new ConfigSection( sectionName, br );
+ mConfigMap.put( sectionName, section ); // Save the data to 'configMap'
+ }
+
+ try
+ {
+ // Finished. Close the file.
+ in.close();
+ br.close();
+ }
+ catch( IOException ioe )
+ {
+ // (Don't care)
+ }
+
+ // Success
+ return true;
+ }
+
+ /**
+ * Saves the data from 'configMap' back to the config file.
+ *
+ * @return True if successful. False otherwise.
+ * @see #reload()
+ */
+ public boolean save()
+ {
+ // No filename was specified.
+ if( TextUtils.isEmpty( mFilename ) )
+ {
+ Log.e( "ConfigFile", "Filename not specified in method save()" );
+ return false; // Quit
+ }
+
+ // Ensure parent directories exist before writing file
+ new File( mFilename ).getParentFile().mkdirs();
+
+ // Write data to file
+ FileWriter fw = null;
+ try
+ {
+ fw = new FileWriter( mFilename );
+
+ // Loop through the sections
+ for( ConfigSection section : mConfigMap.values() )
+ {
+ if( section != null )
+ section.save( fw );
+ }
+ }
+ catch( IOException ioe )
+ {
+ Log.e( "ConfigFile", "IOException creating file " + mFilename + ", error message: "
+ + ioe.getMessage() );
+ return false; // Some problem creating the file.. quit
+ }
+ finally
+ {
+ if( fw != null )
+ {
+ try
+ {
+ fw.close();
+ }
+ catch( IOException ignored )
+ {
+ }
+ }
+ }
+
+ // Success
+ return true;
+ }
+
+ /**
+ * Returns a handle to the configMap keyset.
+ *
+ * @return keyset containing all the config section titles.
+ */
+ public Set keySet()
+ {
+ return mConfigMap.keySet();
+ }
+
+ /**
+ * The ConfigSection class reads all the parameters in the next section of the config file.
+ * Saves the name of the next section (or null if end of file or error). Can also be used to add
+ * a new section to an existing configuration.
+ */
+ public static class ConfigSection
+ {
+ public String name; // Section name
+ private HashMap parameters; // Parameters sorted by name for easy
+ // lookup
+ private LinkedList lines; // All the lines in this section, including comments
+
+ // Name of the next section, or null if there are no sections left to read in the file:
+ private String nextName = null;
+
+ /**
+ * Constructor: Creates an empty config section
+ *
+ * @param sectionName The section title.
+ */
+ public ConfigSection( String sectionName )
+ {
+ parameters = new HashMap();
+ lines = new LinkedList();
+
+ if( !TextUtils.isEmpty( sectionName ) && !sectionName.equals( SECTIONLESS_NAME ) )
+ lines.add( new ConfigLine( ConfigLine.LINE_SECTION, "[" + sectionName + "]\n", null ) );
+
+ name = sectionName;
+ }
+
+ /**
+ * Constructor: Reads the next section of the config file, and saves it in 'parameters'.
+ *
+ * @param sectionName The section title.
+ * @param br The config file to read from.
+ */
+ public ConfigSection( String sectionName, BufferedReader br )
+ {
+ String fullLine, strLine, p, v;
+ ConfigParameter confParam;
+ int x, y;
+
+ parameters = new HashMap();
+ lines = new LinkedList();
+
+ if( !TextUtils.isEmpty( sectionName ) && !sectionName.equals( SECTIONLESS_NAME ) )
+ lines.add( new ConfigLine( ConfigLine.LINE_SECTION, "[" + sectionName + "]\n", null ) );
+
+ name = sectionName;
+
+ // No file to read from. Quit.
+ if( br == null )
+ return;
+
+ try
+ {
+ while( ( fullLine = br.readLine() ) != null )
+ {
+ strLine = fullLine.trim();
+ if( ( strLine.length() < 1 )
+ || ( strLine.substring( 0, 1 ).equals( "#" ) )
+ || ( strLine.substring( 0, 1 ).equals( ";" ) )
+ || ( ( strLine.length() > 1 ) && ( strLine.substring( 0, 2 )
+ .equals( "//" ) ) ) )
+
+ { // A comment or blank line.
+ lines.add( new ConfigLine( ConfigLine.LINE_GARBAGE, fullLine + "\n", null ) );
+ }
+ else if( strLine.contains( "=" ) )
+ {
+ // This should be a "parameter=value" pair:
+ x = strLine.indexOf( '=' );
+
+ if( x < 1 )
+ return; // This shouldn't happen (bad syntax). Quit.
+
+ if( x < ( strLine.length() - 1 ) )
+ {
+ p = strLine.substring( 0, x ).trim();
+ if( p.length() < 1 )
+ return; // This shouldn't happen (bad syntax). Quit.
+
+ v = strLine.substring( x + 1, strLine.length() ).trim();
+ // v = v.replace( "\"", "" ); // I'm doing this later, so I can save
+ // back without losing them
+
+ if( v.length() > 0 )
+ {
+ // Save the parameter=value pair
+ confParam = parameters.get( p );
+ if( confParam != null )
+ {
+ confParam.value = v;
+ }
+ else
+ {
+ confParam = new ConfigParameter( p, v );
+ lines.add( new ConfigLine( ConfigLine.LINE_PARAM, fullLine
+ + "\n", confParam ) );
+ parameters.put( p, confParam ); // Save the pair.
+ }
+ }
+ } // It's ok to have an empty assignment (such as "param=")
+ }
+ else if( strLine.contains( "[" ) )
+ {
+ // This should be the beginning of the next section
+ if( ( strLine.length() < 3 ) || ( !strLine.contains( "]" ) ) )
+ return; // This shouldn't happen (bad syntax). Quit.
+
+ x = strLine.indexOf( '[' );
+ y = strLine.indexOf( ']' );
+
+ if( ( y <= x + 1 ) || ( x == -1 ) || ( y == -1 ) )
+ return; // This shouldn't happen (bad syntax). Quit.
+
+ p = strLine.substring( x + 1, y ).trim();
+
+ // Save the name of the next section.
+ nextName = p;
+
+ // Done reading parameters. Return.
+ return;
+ }
+ else
+ {
+ // This shouldn't happen (bad syntax). Quit.
+ return;
+ }
+ }
+ }
+ catch( IOException ioe )
+ {
+ // (Don't care)
+ }
+
+ // Reached end of file or error.. either way, just quit
+ return;
+ }
+
+ /**
+ * Returns a handle to the parameter keyset.
+ *
+ * @return keyset containing all the parameters.
+ */
+ public Set keySet()
+ {
+ return parameters.keySet();
+ }
+
+ /**
+ * Returns the value of the specified parameter.
+ *
+ * @param parameter Name of the parameter.
+ *
+ * @return Parameter's value, or null if not found.
+ */
+ public String get( String parameter )
+ {
+ // Error: no parameters, or parameter was null
+ if( parameters == null || TextUtils.isEmpty( parameter ) )
+ return null;
+
+ ConfigParameter confParam = parameters.get( parameter );
+
+ // Parameter not found
+ if( confParam == null )
+ return null;
+
+ // Got it
+ return confParam.value;
+ }
+
+ /**
+ * Adds the specified parameter to this config section, updates the value if it already
+ * exists, or removes the parameter.
+ *
+ * @param parameter The name of the parameter.
+ * @param value The parameter's value, or null to remove.
+ */
+ public void put( String parameter, String value )
+ {
+ ConfigParameter confParam = parameters.get( parameter );
+ if( confParam == null ) // New parameter
+ {
+ if( !TextUtils.isEmpty( value ) )
+ {
+ confParam = new ConfigParameter( parameter, value );
+ lines.add( new ConfigLine( ConfigLine.LINE_PARAM, parameter + "=" + value
+ + "\n", confParam ) );
+ parameters.put( parameter, confParam );
+ }
+ }
+ else
+ {
+ // Change the parameter's value
+ confParam.value = value;
+ }
+ }
+
+ /**
+ * Writes the entire section to file.
+ *
+ * @param fw File to write to.
+ *
+ * @throws IOException if a writing error occurs.
+ */
+ public void save( FileWriter fw ) throws IOException
+ {
+ for( ConfigLine line : lines )
+ {
+ if( line != null )
+ line.save( fw );
+ }
+ }
+ }
+
+ /**
+ * The ConfigLine class stores each line of the config file (including comments).
+ */
+ private static class ConfigLine
+ {
+ public static final int LINE_GARBAGE = 0; // Comment, whitespace, or blank line
+ public static final int LINE_SECTION = 1; // Section title
+ public static final int LINE_PARAM = 2; // Parameter=value pair
+
+ public int lineType = 0; // LINE_GARBAGE, LINE_SECTION, or LINE_PARAM.
+ public String strLine = ""; // Actual line from the config file.
+ public ConfigParameter confParam = null; // Null unless this line has a parameter.
+
+ /**
+ * Constructor: Saves the relevant information about the line.
+ *
+ * @param type The type of line.
+ * @param line The line itself.
+ * @param param Config parameters pertaining to the line.
+ */
+ public ConfigLine( int type, String line, ConfigParameter param )
+ {
+ lineType = type;
+ strLine = line;
+ confParam = param;
+ }
+
+ /**
+ * Saves the ConfigLine.
+ *
+ * @param fw The file to save the ConfigLine to.
+ *
+ * @throws IOException If a writing error occurs.
+ */
+ public void save( FileWriter fw ) throws IOException
+ {
+ int x;
+ if( lineType == LINE_PARAM )
+ {
+ if( !strLine.contains( "=" ) || confParam == null )
+ return; // This shouldn't happen
+
+ x = strLine.indexOf( '=' );
+
+ if( x < 1 )
+ return; // This shouldn't happen either
+
+ if( x < strLine.length() )
+ fw.write( strLine.substring( 0, x + 1 ) + confParam.value + "\n" );
+ }
+ else
+ {
+ fw.write( strLine );
+ }
+ }
+ }
+
+ /**
+ * The ConfigParameter class associates a parameter with its value.
+ */
+ private static class ConfigParameter
+ {
+ @SuppressWarnings( "unused" )
+ public String parameter;
+ public String value;
+
+ /**
+ * Constructor: Associate the parameter and value
+ *
+ * @param parameter The name of the parameter.
+ * @param value The value of the parameter.
+ */
+ public ConfigParameter( String parameter, String value )
+ {
+ this.parameter = parameter;
+ this.value = value;
+ }
+ }
+}
diff --git a/Android/src/emu/project64/persistent/GlobalPrefsActivity.java b/Android/src/emu/project64/persistent/GlobalPrefsActivity.java
new file mode 100644
index 000000000..674bc3733
--- /dev/null
+++ b/Android/src/emu/project64/persistent/GlobalPrefsActivity.java
@@ -0,0 +1,70 @@
+/****************************************************************************
+* *
+* Project64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.persistent;
+
+import emu.project64.R;
+
+import emu.project64.compat.AppCompatPreferenceActivity;
+import android.annotation.TargetApi;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+
+public class GlobalPrefsActivity extends AppCompatPreferenceActivity implements OnPreferenceClickListener,
+ OnSharedPreferenceChangeListener
+{
+ private static final String NAVIGATION_MODE = "navigationMode";
+
+ @SuppressWarnings( "deprecation" )
+ @Override
+ protected void onCreate( Bundle savedInstanceState )
+ {
+ super.onCreate( savedInstanceState );
+
+ // Load user preference menu structure from XML and update view
+ addPreferencesFromResource( R.xml.preferences_global );
+
+ }
+
+
+ @Override
+ public void onSharedPreferenceChanged( SharedPreferences sharedPreferences, String key )
+ {
+ if( key.equals( NAVIGATION_MODE ) )
+ {
+ // Sometimes one preference change affects the hierarchy or layout of the views. In this
+ // case it's easier just to restart the activity than try to figure out what to fix.
+ finish();
+ startActivity(getIntent());
+ }
+ else
+ {
+ // Just refresh the preference screens in place
+ refreshViews();
+ }
+ }
+
+ @TargetApi( 9 )
+ private void refreshViews()
+ {
+ // Refresh the preferences object
+
+ }
+
+ @Override
+ public boolean onPreferenceClick( Preference preference )
+ {
+ // Let Android handle all other preference clicks
+ return false;
+ }
+}
diff --git a/Android/src/emu/project64/preference/PathPreference.java b/Android/src/emu/project64/preference/PathPreference.java
new file mode 100644
index 000000000..e4a8e7220
--- /dev/null
+++ b/Android/src/emu/project64/preference/PathPreference.java
@@ -0,0 +1,307 @@
+/****************************************************************************
+* *
+* Project 64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.preference;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import emu.project64.R;
+
+import emu.project64.AndroidDevice;
+import emu.project64.dialog.Prompt;
+import emu.project64.util.FileUtil;
+import android.app.AlertDialog.Builder;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.TypedArray;
+import android.os.Environment;
+import android.os.Parcelable;
+import android.preference.DialogPreference;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.widget.ArrayAdapter;
+
+/**
+ * A {@link DialogPreference} that is specifically for choosing a directory path or file on a device.
+ */
+public class PathPreference extends DialogPreference
+{
+ /** The user must select a directory. No files will be shown in the list. */
+ public static final int SELECTION_MODE_DIRECTORY = 0;
+
+ /** The user must select a file. The dialog will only close when a file is selected. */
+ public static final int SELECTION_MODE_FILE = 1;
+
+ /** The user may select a file or a directory. The Ok button must be used. */
+ public static final int SELECTION_MODE_ANY = 2;
+
+ private static final String STORAGE_DIR = Environment.getExternalStorageDirectory().getAbsolutePath();
+
+ private final boolean mUseDefaultSummary;
+ private int mSelectionMode = SELECTION_MODE_ANY;
+ private boolean mDoReclick = false;
+ private final List mNames = new ArrayList();
+ private final List mPaths = new ArrayList();
+ private String mNewValue;
+ private String mValue;
+
+ /**
+ * Constructor
+ *
+ * @param context The {@link Context} that this PathPreference is being used in.
+ * @param attrs A collection of attributes, as found associated with a tag in an XML document.
+ */
+ public PathPreference( Context context, AttributeSet attrs )
+ {
+ super( context, attrs );
+
+ mUseDefaultSummary = TextUtils.isEmpty( getSummary() );
+
+ // Get the selection mode from the XML file, if provided
+ TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.PathPreference );
+ mSelectionMode = a.getInteger( R.styleable.PathPreference_selectionMode, SELECTION_MODE_ANY );
+ a.recycle();
+ }
+
+ /**
+ * Sets the path that PathPrefence will use.
+ *
+ * @param value The path that this PathPreference instance will use.
+ */
+ public void setValue( String value )
+ {
+ mValue = validate( value );
+ if( shouldPersist() )
+ persistString( mValue );
+
+ // Summary always reflects the true/persisted value, does not track the temporary/new value
+ if( mUseDefaultSummary )
+ setSummary( mSelectionMode == SELECTION_MODE_FILE ? new File( mValue ).getName() : mValue );
+
+ // Reset the dialog info
+ populate( mValue );
+ }
+
+ /**
+ * Sets the specific selection mode to use.
+ *
+ * @param value The selection mode to use.
+ * 0 = Directories can only be used as a choice.
+ * 1 = Files can only be used as a choice.
+ * 2 = Directories and files can be used as a choice.
+ */
+ public void setSelectionMode( int value )
+ {
+ mSelectionMode = value;
+ }
+
+ /**
+ * Gets the path value being used.
+ *
+ * @return The path value being used by this PathPreference.
+ */
+ public String getValue()
+ {
+ return mValue;
+ }
+
+ /**
+ * Gets the current selection mode being used.
+ *
+ * @return The current selection mode being used by this PathPreference.
+ */
+ public int getSelectionMode()
+ {
+ return mSelectionMode;
+ }
+
+ @Override
+ protected Object onGetDefaultValue( TypedArray a, int index )
+ {
+ return a.getString( index );
+ }
+
+ @Override
+ protected void onSetInitialValue( boolean restorePersistedValue, Object defaultValue )
+ {
+ setValue( restorePersistedValue ? getPersistedString( mValue ) : (String) defaultValue );
+ }
+
+ @Override
+ protected void onPrepareDialogBuilder( Builder builder )
+ {
+ super.onPrepareDialogBuilder( builder );
+
+ // Add the list entries
+ if( AndroidDevice.IS_HONEYCOMB )
+ {
+ // Holo theme has folder icons and "Parent folder" text
+ ArrayAdapter adapter = Prompt.createFilenameAdapter( getContext(), mPaths, mNames );
+ builder.setAdapter( adapter, this );
+ }
+ else
+ {
+ // Basic theme uses bold text for folders and ".." for the parent
+ CharSequence[] items = mNames.toArray( new CharSequence[mNames.size()] );
+ builder.setItems( items, this );
+ }
+
+ // Remove the Ok button when user must choose a file
+ if( mSelectionMode == SELECTION_MODE_FILE )
+ builder.setPositiveButton( null, null );
+ }
+
+ @Override
+ public void onClick( DialogInterface dialog, int which )
+ {
+ // If the user clicked a list item...
+ if( which >= 0 && which < mPaths.size() )
+ {
+ mNewValue = mPaths.get( which );
+ File path = new File( mNewValue );
+ if( path.isDirectory() )
+ {
+ // ...navigate into...
+ populate( mNewValue );
+ mDoReclick = true;
+ }
+ else
+ {
+ // ...or close dialog positively
+ which = DialogInterface.BUTTON_POSITIVE;
+ }
+ }
+
+ // Call super last, parameters may have changed above
+ super.onClick( dialog, which );
+ }
+
+ @Override
+ protected void onDialogClosed( boolean positiveResult )
+ {
+ super.onDialogClosed( positiveResult );
+
+ if( positiveResult && callChangeListener( mNewValue ) )
+ {
+ // User clicked Ok: clean the state by persisting value
+ setValue( mNewValue );
+ }
+ else if( mDoReclick )
+ {
+ // User clicked a list item: maintain dirty value and re-open
+ mDoReclick = false;
+ onClick();
+ }
+ else
+ {
+ // User clicked Cancel/Back: clean state by restoring persisted value
+ populate( mValue );
+ }
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState()
+ {
+ final SavedStringState myState = new SavedStringState( super.onSaveInstanceState() );
+ myState.mValue = mNewValue;
+ return myState;
+ }
+
+ @Override
+ protected void onRestoreInstanceState( Parcelable state )
+ {
+ if( state == null || !state.getClass().equals( SavedStringState.class ) )
+ {
+ // Didn't save state for us in onSaveInstanceState
+ super.onRestoreInstanceState( state );
+ return;
+ }
+
+ final SavedStringState myState = (SavedStringState) state;
+ super.onRestoreInstanceState( myState.getSuperState() );
+ populate( myState.mValue );
+
+ // If the dialog is already showing, we must close and reopen to refresh the contents
+ // TODO: Find a less hackish solution, if one exists
+ if( getDialog() != null )
+ {
+ mDoReclick = true;
+ getDialog().dismiss();
+ }
+ }
+
+ // Populates the dialog view with files and folders on the device.
+ private void populate( String path )
+ {
+ // Cache the path to persist on Ok
+ mNewValue = path;
+
+ // Quick exit if null
+ if( path == null )
+ return;
+
+ // If start path is a file, list it and its siblings in the parent directory
+ File startPath = new File( path );
+ if( startPath.isFile() )
+ startPath = startPath.getParentFile();
+
+ // Set the dialog title based on the selection mode
+ switch( mSelectionMode )
+ {
+ case SELECTION_MODE_FILE:
+ // If selecting only files, set title to parent directory name
+ setDialogTitle( startPath.getPath() );
+ break;
+ case SELECTION_MODE_DIRECTORY:
+ case SELECTION_MODE_ANY:
+ // Otherwise clarify the directory that will be selected if user clicks Ok
+ setDialogTitle( getContext().getString( R.string.pathPreference_dialogTitle,
+ startPath.getPath() ) );
+ break;
+ }
+
+ // Populate the key-value pairs for the list entries
+ boolean isFilesIncluded = mSelectionMode != SELECTION_MODE_DIRECTORY;
+ FileUtil.populate( startPath, true, true, isFilesIncluded, mNames, mPaths );
+ }
+
+ private static String validate( String value )
+ {
+ if( TextUtils.isEmpty( value ) )
+ {
+ // Use storage directory if value is empty
+ value = STORAGE_DIR;
+ }
+ else
+ {
+ // Non-empty string provided
+ // Prefixes encode additional information:
+ // ! and ~ mean path is relative to storage dir
+ // ! means parent dirs should be created if path does not exist
+ // ~ means storage dir should be used if path does not exist
+ boolean isRelativePath = value.startsWith( "!" ) || value.startsWith( "~" );
+ boolean forceParentDirs = value.startsWith( "!" );
+
+ // Build the absolute path if necessary
+ if( isRelativePath )
+ value = STORAGE_DIR + "/" + value.substring( 1 );
+
+ // Ensure the parent directories exist if requested
+ File file = new File( value );
+ if( forceParentDirs )
+ file.mkdirs();
+ else if( !file.exists() )
+ value = STORAGE_DIR;
+ }
+ return value;
+ }
+}
diff --git a/Android/src/emu/project64/preference/SavedStringState.java b/Android/src/emu/project64/preference/SavedStringState.java
new file mode 100644
index 000000000..34d303785
--- /dev/null
+++ b/Android/src/emu/project64/preference/SavedStringState.java
@@ -0,0 +1,68 @@
+/****************************************************************************
+* *
+* Project 64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.preference;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.preference.Preference;
+import android.preference.Preference.BaseSavedState;
+
+public class SavedStringState extends BaseSavedState
+{
+ String mValue;
+
+ public SavedStringState( Parcel source )
+ {
+ super( source );
+ mValue = source.readString();
+ }
+
+ @Override
+ public void writeToParcel( Parcel dest, int flags )
+ {
+ super.writeToParcel( dest, flags );
+ dest.writeString( mValue );
+ }
+
+ public SavedStringState( Parcelable superState )
+ {
+ super( superState );
+ }
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator()
+ {
+ @Override
+ public SavedStringState createFromParcel( Parcel in )
+ {
+ return new SavedStringState( in );
+ }
+
+ @Override
+ public SavedStringState[] newArray( int size )
+ {
+ return new SavedStringState[size];
+ }
+ };
+
+ public static Parcelable onSaveInstanceState( final Parcelable superState,
+ Preference preference, String value )
+ {
+ if( preference.isPersistent() )
+ {
+ // No need to save instance state since it's persistent
+ return superState;
+ }
+
+ final SavedStringState myState = new SavedStringState( superState );
+ myState.mValue = value;
+ return myState;
+ }
+}
diff --git a/Android/src/emu/project64/profile/Profile.java b/Android/src/emu/project64/profile/Profile.java
new file mode 100644
index 000000000..db7cc49c7
--- /dev/null
+++ b/Android/src/emu/project64/profile/Profile.java
@@ -0,0 +1,233 @@
+/****************************************************************************
+* *
+* Project64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.profile;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import emu.project64.persistent.ConfigFile;
+import emu.project64.persistent.ConfigFile.ConfigSection;
+import emu.project64.util.SafeMethods;
+import android.text.TextUtils;
+
+/**
+ * The base class for configuration profiles. Extend this class to encapsulate groups of settings.
+ */
+public class Profile implements Comparable
+{
+ /** The name of the profile, displayed in the UI and used as a unique identifier. */
+ public final String name;
+
+ /** An optional brief description of the profile. Shown in some locations in the UI. */
+ public final String comment;
+
+ /**
+ * Whether this profile is "built-in" to the app (vs. user defined). Built-in profiles are
+ * read-only and can only be copied. Non-built-in profiles can be copied, renamed, edited,
+ * created, and deleted. Built-in profiles are defined in the assets directory, and for all
+ * intents and purposes are guaranteed to exist. Defaults should always reference built-in
+ * profiles.
+ */
+ public final boolean isBuiltin;
+
+ private static final String KEY_COMMENT = "comment";
+
+ private final HashMap data = new HashMap();
+
+ public static List getProfiles( ConfigFile config, boolean isBuiltin )
+ {
+ List profiles = new ArrayList();
+ for( String name : config.keySet() )
+ if( !ConfigFile.SECTIONLESS_NAME.equals( name ) )
+ profiles.add( new Profile( isBuiltin, config.get( name ) ) );
+ return profiles;
+ }
+
+ /**
+ * Instantiates an empty profile.
+ *
+ * @param isBuiltin true if the profile is built-in; false if the profile is user-defined
+ * @param name the unique name of the profile
+ * @param comment an optional brief description of the profile, shown in some of the UI
+ */
+ public Profile( boolean isBuiltin, String name, String comment )
+ {
+ this.isBuiltin = isBuiltin;
+ this.name = name;
+ this.comment = comment;
+ data.put( KEY_COMMENT, comment );
+ }
+
+ /**
+ * Instantiates a profile from a {@link ConfigSection}.
+ *
+ * @param isBuiltin true if the profile is built-in; false if the profile is user-defined
+ * @param section a back-end datastore for the profile
+ */
+ public Profile( boolean isBuiltin, ConfigSection section )
+ {
+ this.isBuiltin = isBuiltin;
+ this.name = section.name;
+ this.comment = section.get( KEY_COMMENT );
+ for( String key : section.keySet() )
+ this.data.put( key, section.get( key ) );
+ }
+
+ /**
+ * Gets the value mapped to the specified key.
+ *
+ * @param key the data key
+ * @return the value mapped to the key, or null if no mapping for the key exists
+ */
+ public String get( String key )
+ {
+ return data.get( key );
+ }
+
+ /**
+ * Gets the value mapped to the specified key.
+ *
+ * @param key the data key
+ * @param defaultValue the value to use if the key is not mapped
+ * @return the value mapped to the key, or defaultValue
if no mapping exists
+ */
+ public String get( String key, String defaultValue )
+ {
+ String value = data.get( key );
+ return value == null ? defaultValue : value;
+ }
+
+ /**
+ * @see #get(String, String)
+ */
+ public boolean getBoolean( String key, boolean defaultValue )
+ {
+ String value = data.get( key );
+ return SafeMethods.toBoolean( value, defaultValue );
+ }
+
+ /**
+ * @see #get(String, String)
+ */
+ public int getInt( String key, int defaultValue )
+ {
+ String value = data.get( key );
+ return SafeMethods.toInt( value, defaultValue );
+ }
+
+ /**
+ * @see #get(String, String)
+ */
+ public float getFloat( String key, int defaultValue )
+ {
+ String value = data.get( key );
+ return SafeMethods.toFloat( value, defaultValue );
+ }
+
+ /**
+ * Maps the specified value to the specified key.
+ *
+ * @param key the data key
+ * @param value the value to be mapped to the key
+ */
+ public void put( String key, String value )
+ {
+ data.put( key, value );
+ }
+
+ /**
+ * @see #put(String, String)
+ */
+ public void putBoolean( String key, boolean value )
+ {
+ put( key, String.valueOf( value ) );
+ }
+
+ /**
+ * @see #put(String, String)
+ */
+ public void putInt( String key, int value )
+ {
+ put( key, String.valueOf( value ) );
+ }
+
+ /**
+ * @see #put(String, String)
+ */
+ public void putFloat( String key, float value )
+ {
+ put( key, String.valueOf( value ) );
+ }
+
+ /**
+ * Reads key-value pairs from a given config file into the profile. Key-value pairs that already
+ * exist in the profile are overwritten.
+ *
+ * @param config the {@link ConfigFile} to read from
+ * @return true if the config file is non-null and the profile name is non-empty
+ */
+ public boolean readFrom( ConfigFile config )
+ {
+ if( config == null || TextUtils.isEmpty( name ) )
+ return false;
+
+ ConfigSection source = config.get( name );
+ if( source == null )
+ return false;
+
+ for( String key : source.keySet() )
+ data.put( key, source.get( key ) );
+ return true;
+ }
+
+ /**
+ * Writes key-value pairs from the profile into a given config file. Key-value pairs that
+ * already exist in the config file are overwritten.
+ *
+ * @param config the {@link ConfigFile} to write to
+ * @return true if the config file is non-null and the profile name is non-empty
+ */
+ public boolean writeTo( ConfigFile config )
+ {
+ if( config == null || TextUtils.isEmpty( name ) )
+ return false;
+
+ for( String key : data.keySet() )
+ config.put( name, key, data.get( key ) );
+ return true;
+ }
+
+ /**
+ * Copies a profile, changing only its name and comment.
+ *
+ * @param name the name of the copy
+ * @param comment the comment of the copy
+ * @return the copied profile
+ */
+ public Profile copy( String name, String comment )
+ {
+ if( TextUtils.isEmpty( name ) )
+ return null;
+
+ Profile newProfile = new Profile( false, name, comment );
+ for( String key : data.keySet() )
+ if( !KEY_COMMENT.equals( key ) )
+ newProfile.data.put( key, data.get( key ) );
+ return newProfile;
+ }
+
+ @Override
+ public int compareTo( Profile another )
+ {
+ return this.name.compareToIgnoreCase( another.name );
+ }
+}
\ No newline at end of file
diff --git a/Android/src/emu/project64/task/ExtractAssetsTask.java b/Android/src/emu/project64/task/ExtractAssetsTask.java
new file mode 100644
index 000000000..a49b750e6
--- /dev/null
+++ b/Android/src/emu/project64/task/ExtractAssetsTask.java
@@ -0,0 +1,323 @@
+/****************************************************************************
+* *
+* Project64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+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>
+{
+ public interface ExtractAssetsListener
+ {
+ public void onExtractAssetsProgress( String nextFileToExtract );
+ public void onExtractAssetsFinished( List 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 doInBackground( Void... params )
+ {
+ return extractAssets( mSrcPath, mDstPath );
+ }
+
+ @Override
+ protected void onProgressUpdate( String... values )
+ {
+ mListener.onExtractAssetsProgress( values[0] );
+ }
+
+ @Override
+ protected void onPostExecute( List 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 extractAssets( String srcPath, String dstPath )
+ {
+ final List failures = new ArrayList();
+
+ 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 fileParts = new HashMap();
+
+ // 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 combineFileParts( Map filePieces, String dstPath )
+ {
+ List failures = new ArrayList();
+ 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;
+ }
+}
diff --git a/Android/src/emu/project64/util/DeviceUtil.java b/Android/src/emu/project64/util/DeviceUtil.java
new file mode 100644
index 000000000..64cf520a7
--- /dev/null
+++ b/Android/src/emu/project64/util/DeviceUtil.java
@@ -0,0 +1,371 @@
+/****************************************************************************
+* *
+* Project64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.util;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import emu.project64.AndroidDevice;
+import emu.project64.input.map.AxisMap;
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.view.InputDevice;
+import android.view.InputDevice.MotionRange;
+import android.view.MotionEvent;
+import android.view.View;
+
+public final class DeviceUtil
+{
+ /**
+ * Gets the hardware information from /proc/cpuinfo.
+ *
+ * @return The hardware string.
+ */
+ public static String getCpuInfo()
+ {
+ // From http://android-er.blogspot.com/2009/09/read-android-cpu-info.html
+ String result = Utility.executeShellCommand( "/system/bin/cat", "/proc/cpuinfo" );
+
+ // Remove the serial number for privacy
+ Pattern pattern = Pattern.compile( "^serial\\s*?:.*?$", Pattern.CASE_INSENSITIVE
+ | Pattern.MULTILINE );
+ result = pattern.matcher( result ).replaceAll( "Serial : XXXX" );
+
+ // Additional information in android.os.Build may be useful
+ result += "\n";
+ result += "Board: " + Build.BOARD + "\n";
+ result += "Brand: " + Build.BRAND + "\n";
+ result += "Device: " + Build.DEVICE + "\n";
+ result += "Display: " + Build.DISPLAY + "\n";
+ result += "Host: " + Build.HOST + "\n";
+ result += "ID: " + Build.ID + "\n";
+ result += "Manufacturer: " + Build.MANUFACTURER + "\n";
+ result += "Model: " + Build.MODEL + "\n";
+ result += "Product: " + Build.PRODUCT + "\n";
+ return result;
+ }
+
+ public static String getLogCat()
+ {
+ return Utility.executeShellCommand( "logcat", "-d", "-v", "long" );
+ }
+
+ public static void clearLogCat()
+ {
+ Utility.executeShellCommand( "logcat", "-c" );
+ }
+
+ @TargetApi( 12 )
+ public static String getAxisInfo()
+ {
+ StringBuilder builder = new StringBuilder();
+
+ if( AndroidDevice.IS_HONEYCOMB_MR1 )
+ {
+ int[] ids = InputDevice.getDeviceIds();
+ for( int i = 0; i < ids.length; i++ )
+ {
+ InputDevice device = InputDevice.getDevice( ids[i] );
+ AxisMap axisMap = AxisMap.getMap( device );
+ if( !TextUtils.isEmpty( axisMap.getSignature() ) )
+ {
+ builder.append( "Device: " + device.getName() + "\n" );
+ builder.append( "Type: " + axisMap.getSignatureName() + "\n" );
+ builder.append( "Signature: " + axisMap.getSignature() + "\n" );
+ builder.append( "Hash: " + axisMap.getSignature().hashCode() + "\n" );
+
+ List ranges = getPeripheralMotionRanges( device );
+ for( MotionRange range : ranges )
+ {
+ if( range.getSource() == InputDevice.SOURCE_JOYSTICK )
+ {
+ int axisCode = range.getAxis();
+ String axisName = MotionEvent.axisToString( axisCode );
+ String className = getAxisClassName( axisMap.getClass( axisCode ) );
+ builder.append( " " + axisName + ": " + className + "\n" );
+ }
+ }
+ builder.append( "\n" );
+ }
+ }
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ * Gets the peripheral information using the appropriate Android API.
+ *
+ * @return The peripheral info string.
+ */
+ @TargetApi( 16 )
+ public static String getPeripheralInfo()
+ {
+ StringBuilder builder = new StringBuilder();
+
+ if( AndroidDevice.IS_GINGERBREAD )
+ {
+ int[] ids = InputDevice.getDeviceIds();
+ for( int i = 0; i < ids.length; i++ )
+ {
+ InputDevice device = InputDevice.getDevice( ids[i] );
+ if( device != null )
+ {
+ if( 0 < ( device.getSources() & ( InputDevice.SOURCE_CLASS_BUTTON | InputDevice.SOURCE_CLASS_JOYSTICK ) ) )
+ {
+ builder.append( "Device: " + device.getName() + "\n" );
+ builder.append( "Id: " + device.getId() + "\n" );
+ if( AndroidDevice.IS_JELLY_BEAN )
+ {
+ builder.append( "Descriptor: " + device.getDescriptor() + "\n" );
+ if( device.getVibrator().hasVibrator() )
+ builder.append( "Vibrator: true\n" );
+ }
+ builder.append( "Class: " + getSourceClassesString( device.getSources() )
+ + "\n" );
+
+ List ranges = getPeripheralMotionRanges( device );
+ if( ranges.size() > 0 )
+ {
+ builder.append( "Axes: " + ranges.size() + "\n" );
+ for( MotionRange range : ranges )
+ {
+ if( AndroidDevice.IS_HONEYCOMB_MR1 )
+ {
+ String axisName = MotionEvent.axisToString( range.getAxis() );
+ String source = getSourceName( range.getSource() );
+ builder.append( " " + axisName + " (" + source + ")" );
+ }
+ else
+ {
+ builder.append( " Axis" );
+ }
+ builder.append( ": ( " + range.getMin() + " , " + range.getMax()
+ + " )\n" );
+ }
+ }
+ builder.append( "\n" );
+ }
+ }
+ }
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ * Gets the motion ranges of a peripheral using the appropriate Android API.
+ *
+ * @return The motion ranges associated with the peripheral.
+ */
+ @TargetApi( 12 )
+ public static List getPeripheralMotionRanges( InputDevice device )
+ {
+ List ranges;
+ if( AndroidDevice.IS_HONEYCOMB_MR1 )
+ {
+ ranges = device.getMotionRanges();
+ }
+ else if( AndroidDevice.IS_GINGERBREAD )
+ {
+ // Earlier APIs we have to do it the hard way
+ ranges = new ArrayList();
+ boolean finished = false;
+ for( int j = 0; j < 256 && !finished; j++ )
+ {
+ // TODO: Eliminate reliance on try-catch
+ try
+ {
+ if( device.getMotionRange( j ) != null )
+ ranges.add( device.getMotionRange( j ) );
+ }
+ catch( Exception e )
+ {
+ finished = true;
+ }
+ }
+ }
+ else
+ {
+ ranges = new ArrayList();
+ }
+
+ return ranges;
+ }
+
+ /**
+ * Gets the name of an axis class.
+ *
+ * @param axisClass The axis class to get the name of.
+ *
+ * @return The name of the axis class.
+ */
+ public static String getAxisClassName( int axisClass )
+ {
+ switch( axisClass )
+ {
+ case AxisMap.AXIS_CLASS_UNKNOWN:
+ return "Unknown";
+ case AxisMap.AXIS_CLASS_IGNORED:
+ return "Ignored";
+ case AxisMap.AXIS_CLASS_STICK:
+ return "Stick";
+ case AxisMap.AXIS_CLASS_TRIGGER:
+ return "Trigger";
+ default:
+ return "";
+ }
+ }
+
+ /**
+ * Gets the name of an action.
+ *
+ * @param action The action being performed.
+ * @param isMotionEvent Whether or not the action is a motion event.
+ *
+ * @return The name of the action being performed.
+ */
+ public static String getActionName( int action, boolean isMotionEvent )
+ {
+ switch( action )
+ {
+ case MotionEvent.ACTION_DOWN:
+ return "DOWN";
+ case MotionEvent.ACTION_UP:
+ return "UP";
+ case MotionEvent.ACTION_MOVE:
+ return isMotionEvent ? "MOVE" : "MULTIPLE";
+ case MotionEvent.ACTION_CANCEL:
+ return "CANCEL";
+ case MotionEvent.ACTION_OUTSIDE:
+ return "OUTSIDE";
+ case MotionEvent.ACTION_POINTER_DOWN:
+ return "POINTER_DOWN";
+ case MotionEvent.ACTION_POINTER_UP:
+ return "POINTER_UP";
+ case MotionEvent.ACTION_HOVER_MOVE:
+ return "HOVER_MOVE";
+ case MotionEvent.ACTION_SCROLL:
+ return "SCROLL";
+ case MotionEvent.ACTION_HOVER_ENTER:
+ return "HOVER_ENTER";
+ case MotionEvent.ACTION_HOVER_EXIT:
+ return "HOVER_EXIT";
+ default:
+ return "ACTION_" + Integer.toString( action );
+ }
+ }
+
+ /**
+ * Gets the name of the source performing an action.
+ *
+ * @param source A number representing the source.
+ *
+ * @return The name of the source.
+ */
+ public static String getSourceName( int source )
+ {
+ switch( source )
+ {
+ case InputDevice.SOURCE_CLASS_BUTTON:
+ return "BUTTON";
+ case InputDevice.SOURCE_CLASS_POINTER:
+ return "POINTER";
+ case InputDevice.SOURCE_CLASS_TRACKBALL:
+ return "TRACKBALL";
+ case InputDevice.SOURCE_CLASS_POSITION:
+ return "POSITION";
+ case InputDevice.SOURCE_CLASS_JOYSTICK:
+ return "JOYSTICK";
+ case InputDevice.SOURCE_DPAD:
+ return "dpad";
+ case InputDevice.SOURCE_GAMEPAD:
+ return "gamepad";
+ case InputDevice.SOURCE_JOYSTICK:
+ return "joystick";
+ case InputDevice.SOURCE_KEYBOARD:
+ return "keyboard";
+ case InputDevice.SOURCE_MOUSE:
+ return "mouse";
+ case InputDevice.SOURCE_STYLUS:
+ return "stylus";
+ case InputDevice.SOURCE_TOUCHPAD:
+ return "touchpad";
+ case InputDevice.SOURCE_TOUCHSCREEN:
+ return "touchscreen";
+ case InputDevice.SOURCE_TRACKBALL:
+ return "trackball";
+ case InputDevice.SOURCE_UNKNOWN:
+ return "unknown";
+ default:
+ return "source_" + source;
+ }
+ }
+
+ @SuppressLint( "InlinedApi" )
+ public static String getSourcesString( int sources )
+ {
+ List names = new ArrayList();
+ addString( sources, InputDevice.SOURCE_KEYBOARD, names );
+ addString( sources, InputDevice.SOURCE_DPAD, names );
+ addString( sources, InputDevice.SOURCE_GAMEPAD, names );
+ addString( sources, InputDevice.SOURCE_TOUCHSCREEN, names );
+ addString( sources, InputDevice.SOURCE_MOUSE, names );
+ addString( sources, InputDevice.SOURCE_STYLUS, names );
+ addString( sources, InputDevice.SOURCE_TOUCHPAD, names );
+ addString( sources, InputDevice.SOURCE_JOYSTICK, names );
+ return TextUtils.join( ", ", names );
+ }
+
+ @SuppressLint( "InlinedApi" )
+ public static String getSourceClassesString( int sources )
+ {
+ List names = new ArrayList();
+ addString( sources, InputDevice.SOURCE_CLASS_BUTTON, names );
+ addString( sources, InputDevice.SOURCE_CLASS_POINTER, names );
+ addString( sources, InputDevice.SOURCE_CLASS_TRACKBALL, names );
+ addString( sources, InputDevice.SOURCE_CLASS_POSITION, names );
+ addString( sources, InputDevice.SOURCE_CLASS_JOYSTICK, names );
+ return TextUtils.join( ", ", names );
+ }
+
+ private static void addString( int sources, int sourceClass, List strings )
+ {
+ if( ( sources & sourceClass ) > 0 )
+ strings.add( getSourceName( sourceClass ) );
+ }
+
+ /**
+ * Returns display metrics for the specified view.
+ *
+ * @param view An instance of View (must be the child of an Activity).
+ *
+ * @return DisplayMetrics instance, or null if there was a problem.
+ */
+ public static DisplayMetrics getDisplayMetrics( View view )
+ {
+ if( view == null )
+ return null;
+
+ Context context = view.getContext();
+ if( !( context instanceof Activity ) )
+ return null;
+ DisplayMetrics metrics = new DisplayMetrics();
+ ((Activity) context).getWindowManager().getDefaultDisplay().getMetrics( metrics );
+ return metrics;
+ }
+}
diff --git a/Android/src/emu/project64/util/FileUtil.java b/Android/src/emu/project64/util/FileUtil.java
new file mode 100644
index 000000000..384f8e3f9
--- /dev/null
+++ b/Android/src/emu/project64/util/FileUtil.java
@@ -0,0 +1,168 @@
+/****************************************************************************
+* *
+* Project64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.util;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import emu.project64.AndroidDevice;
+import android.text.Html;
+import android.text.TextUtils;
+
+/**
+ * Utility class that provides methods which simplify file I/O tasks.
+ */
+public final class FileUtil
+{
+ public static void populate( File startPath, boolean includeParent, boolean includeDirectories,
+ boolean includeFiles, List outNames, List outPaths )
+ {
+ if( !startPath.exists() )
+ return;
+
+ if( startPath.isFile() )
+ startPath = startPath.getParentFile();
+
+ if( startPath.getParentFile() == null )
+ includeParent = false;
+
+ outNames.clear();
+ outPaths.clear();
+
+ if( includeParent )
+ {
+
+ outNames.add( Html.fromHtml( ".." ) );
+ boolean BaseDir = false;
+ ArrayList StorageDirectories = AndroidDevice.getStorageDirectories();
+ for( String directory : StorageDirectories )
+ {
+ if (TextUtils.equals(startPath.getPath(), directory))
+ {
+ BaseDir = true;
+ break;
+ }
+ }
+
+ outPaths.add( BaseDir ? null : startPath.getParentFile().getPath() );
+ }
+
+ if( includeDirectories )
+ {
+ for( File directory : getContents( startPath, new VisibleDirectoryFilter() ) )
+ {
+ outNames.add( Html.fromHtml( "" + directory.getName() + "" ) );
+ outPaths.add( directory.getPath() );
+ }
+ }
+
+ if( includeFiles )
+ {
+ for( File file : getContents( startPath, new VisibleFileFilter() ) )
+ {
+ outNames.add( Html.fromHtml( file.getName() ) );
+ outPaths.add( file.getPath() );
+ }
+ }
+ }
+
+ public static List getContents( File startPath, FileFilter fileFilter )
+ {
+ // Get a filtered, sorted list of files
+ List results = new ArrayList();
+ File[] files = startPath.listFiles( fileFilter );
+
+ if( files != null )
+ {
+ Collections.addAll( results, files );
+ Collections.sort( results, new FileUtil.FileComparer() );
+ }
+
+ return results;
+ }
+
+ private static class FileComparer implements Comparator
+ {
+ // Compare files first by directory/file then alphabetically (case-insensitive)
+ @Override
+ public int compare( File lhs, File rhs )
+ {
+ if( lhs.isDirectory() && rhs.isFile() )
+ return -1;
+ else if( lhs.isFile() && rhs.isDirectory() )
+ return 1;
+ else
+ return lhs.getName().compareToIgnoreCase( rhs.getName() );
+ }
+ }
+
+ private static class VisibleFileFilter implements FileFilter
+ {
+ // Include only non-hidden files not starting with '.'
+ @Override
+ public boolean accept( File pathname )
+ {
+ return ( pathname != null ) && ( pathname.isFile() ) && ( !pathname.isHidden() )
+ && ( !pathname.getName().startsWith( "." ) );
+ }
+ }
+
+ private static class VisibleDirectoryFilter implements FileFilter
+ {
+ // Include only non-hidden directories not starting with '.'
+ @Override
+ public boolean accept( File pathname )
+ {
+ return ( pathname != null ) && ( pathname.isDirectory() ) && ( !pathname.isHidden() )
+ && ( !pathname.getName().startsWith( "." ) );
+ }
+ }
+
+ /**
+ * Deletes a given folder directory in the form of a {@link File}
+ *
+ * @param folder The folder to delete.
+ *
+ * @return True if the folder was deleted, false otherwise.
+ */
+ public static boolean deleteFolder( File folder )
+ {
+ if( folder.isDirectory() )
+ {
+ String[] children = folder.list();
+ if( children != null )
+ {
+ for( String child : children )
+ {
+ boolean success = deleteFolder( new File( folder, child ) );
+ if( !success )
+ return false;
+ }
+ }
+ }
+
+ return folder.delete();
+ }
+
+ public static String getFileNameFromPath(String path)
+ {
+ if (path == null)
+ {
+ return "";
+ }
+ int index = path.lastIndexOf('/');
+ return index > -1 ? path.substring(index+1) : path;
+ }
+}
diff --git a/Android/src/emu/project64/util/Image.java b/Android/src/emu/project64/util/Image.java
new file mode 100644
index 000000000..42d910a0d
--- /dev/null
+++ b/Android/src/emu/project64/util/Image.java
@@ -0,0 +1,203 @@
+/****************************************************************************
+* *
+* Project64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.util;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+
+/**
+ * The Image class provides a simple interface to common image manipulation methods.
+ */
+public final class Image
+{
+ public final Bitmap image;
+ public final BitmapDrawable drawable;
+ public final int width;
+ public final int height;
+ public final int hWidth;
+ public final int hHeight;
+
+ public float scale = 1.0f;
+ public int x = 0;
+ public int y = 0;
+ public final Rect drawRect = new Rect();
+
+ /**
+ * Constructor: Loads an image file and sets the initial properties.
+ *
+ * @param res
+ * A handle to the app resources.
+ * @param filename
+ * The path to the image file.
+ */
+ public Image( Resources res, String filename )
+ {
+ image = BitmapFactory.decodeFile( filename );
+ drawable = new BitmapDrawable( res, image );
+
+ if( image == null )
+ {
+ width = 0;
+ height = 0;
+ }
+ else
+ {
+ width = image.getWidth();
+ height = image.getHeight();
+ }
+
+ hWidth = (int) ( width / 2.0f );
+ hHeight = (int) ( height / 2.0f );
+ }
+
+ /**
+ * Constructor: Creates a clone copy of a given Image.
+ *
+ * @param res
+ * A handle to the app resources.
+ * @param clone
+ * The Image to make a copy of.
+ */
+ public Image( Resources res, Image clone )
+ {
+ if( clone == null )
+ {
+ image = null;
+ drawable = null;
+ width = 0;
+ height = 0;
+ hWidth = 0;
+ hHeight = 0;
+ }
+ else
+ {
+ image = clone.image;
+ drawable = new BitmapDrawable( res, image );
+ width = clone.width;
+ height = clone.height;
+ hWidth = clone.hWidth;
+ hHeight = clone.hHeight;
+ scale = clone.scale;
+ }
+ }
+
+ /**
+ * Sets the scaling factor of the image.
+ *
+ * @param scale
+ * Factor to scale the image by.
+ */
+ public void setScale( float scale )
+ {
+ this.scale = scale;
+ setPos( x, y ); // Apply the new scaling factor
+ }
+
+ /**
+ * Sets the screen position of the image (in pixels).
+ *
+ * @param x
+ * X-coordinate.
+ * @param y
+ * Y-coordinate.
+ */
+ public void setPos( int x, int y )
+ {
+ this.x = x;
+ this.y = y;
+ drawRect.set( x, y, x + (int) ( width * scale ), y + (int) ( height * scale ) );
+ if( drawable != null )
+ drawable.setBounds( drawRect );
+ }
+
+ /**
+ * Places the image at the specified location in terms of percentage of screen size.
+ *
+ * @param percentX
+ * Percent of screen width to shift the image by.
+ * @param percentY
+ * Percent of screen height to shift the image by.
+ * @param screenW
+ * Horizontal screen dimension (in pixels).
+ * @param screenH
+ * Vertical screen dimension (in pixels).
+ */
+ public void fitPercent( float percentX, float percentY, int screenW, int screenH )
+ {
+ int px = (int) ( ( percentX / 100f ) * ( screenW - width * scale ) );
+ int py = (int) ( ( percentY / 100f ) * ( screenH - height * scale ) );
+ setPos( px, py );
+ }
+
+ /**
+ * Centers the image at the specified coordinates, without going beyond the edges of the
+ * specified rectangle.
+ *
+ * @param centerX
+ * X-coordinate to center the image at.
+ * @param centerY
+ * Y-coordinate to center the image at.
+ * @param rectX
+ * X-coordinate of the bounding rectangle.
+ * @param rectY
+ * Y-coordinate of the bounding rectangle.
+ * @param rectW
+ * Horizontal bounding rectangle dimension (in pixels).
+ * @param rectH
+ * Vertical bounding rectangle dimension (in pixels).
+ */
+ public void fitCenter( int centerX, int centerY, int rectX, int rectY, int rectW, int rectH )
+ {
+ float cx = centerX;
+ float cy = centerY;
+
+ if( cx < rectX + ( hWidth * scale ) )
+ cx = rectX + ( hWidth * scale );
+ if( cy < rectY + ( hHeight * scale ) )
+ cy = rectY + ( hHeight * scale );
+ if( cx + ( hWidth * scale ) > rectX + rectW )
+ cx = rectX + rectW - ( hWidth * scale );
+ if( cy + ( hHeight * scale ) > rectY + rectH )
+ cy = rectY + rectH - ( hHeight * scale );
+
+ int px = (int) ( cx - ( hWidth * scale ) );
+ int py = (int) ( cy - ( hHeight * scale ) );
+ setPos( px, py );
+ }
+
+ /**
+ * Draws the image.
+ *
+ * @param canvas
+ * Canvas to draw the image on.
+ */
+ public void draw( Canvas canvas )
+ {
+ if( drawable != null )
+ drawable.draw( canvas );
+ }
+
+ /**
+ * Sets the alpha value of the image.
+ *
+ * @param alpha
+ * Alpha value.
+ */
+ public void setAlpha( int alpha )
+ {
+ if( drawable != null )
+ drawable.setAlpha( alpha );
+ }
+}
diff --git a/Android/src/emu/project64/util/Notifier.java b/Android/src/emu/project64/util/Notifier.java
new file mode 100644
index 000000000..26a17167a
--- /dev/null
+++ b/Android/src/emu/project64/util/Notifier.java
@@ -0,0 +1,68 @@
+/****************************************************************************
+* *
+* Project64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.util;
+
+import android.app.Activity;
+import android.view.Gravity;
+import android.widget.Toast;
+
+/**
+ * A small class to encapsulate the notification process for Mupen64PlusAE.
+ */
+public final class Notifier
+{
+ private static Toast sToast = null;
+ private static Runnable sToastMessager = null;
+
+ /**
+ * Pop up a temporary message on the device.
+ *
+ * @param activity The activity to display from
+ * @param message The message string to display.
+ */
+ public static void showToast( Activity activity, String message )
+ {
+ if( activity == null )
+ return;
+
+ // Create a messaging task if it doesn't already exist
+ if( sToastMessager == null )
+ {
+ final String ToastMessage = new String(message);
+ final Activity ToastActivity = activity;
+
+ sToastMessager = new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ // Just show the toast message
+ if( sToast != null )
+ sToast.show();
+
+ if( sToast != null )
+ {
+ // Toast exists, just change the text
+ Notifier.sToast.setText( ToastMessage );
+ }
+ else
+ {
+ // Message short in duration, and at the bottom of the screen
+ sToast = Toast.makeText( ToastActivity, ToastMessage, Toast.LENGTH_SHORT );
+ sToast.setGravity( Gravity.BOTTOM, 0, 0 );
+ }
+ sToastMessager = null;
+ }
+ };
+ }
+ activity.runOnUiThread( sToastMessager );
+ }
+}
diff --git a/Android/src/emu/project64/util/SafeMethods.java b/Android/src/emu/project64/util/SafeMethods.java
new file mode 100644
index 000000000..e3a03712e
--- /dev/null
+++ b/Android/src/emu/project64/util/SafeMethods.java
@@ -0,0 +1,176 @@
+/****************************************************************************
+* *
+* Project64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.LinkedList;
+
+import android.text.TextUtils;
+
+/**
+ * A boilerplate class for safely doing things, with simple exception handling.
+ */
+public final class SafeMethods
+{
+ /**
+ * Safely converts a string into a boolean.
+ *
+ * @param val String containing the boolean to convert.
+ * @param fail Value to use if unable to convert val to a boolean.
+ *
+ * @return The converted boolean, or the specified value if unsuccessful.
+ */
+ public static boolean toBoolean( String val, boolean fail )
+ {
+ if( TextUtils.isEmpty( val ) )
+ return fail; // Not a boolean
+
+ try
+ {
+ // Convert to boolean
+ return Boolean.parseBoolean( val );
+ }
+ catch( NumberFormatException nfe )
+ {
+ }
+ // Conversion failed
+ return fail;
+ }
+
+ /**
+ * Safely converts a string into an integer.
+ *
+ * @param val String containing the number to convert.
+ * @param fail Value to use if unable to convert val to an integer.
+ *
+ * @return The converted integer, or the specified value if unsuccessful.
+ */
+ public static int toInt( String val, int fail )
+ {
+ if( TextUtils.isEmpty( val ) )
+ return fail; // Not a number
+
+ try
+ {
+ // Convert to integer
+ return Integer.parseInt( val );
+ }
+ catch( NumberFormatException nfe )
+ {
+ }
+ // Conversion failed
+ return fail;
+ }
+
+ /**
+ * Safely converts a string into a float.
+ *
+ * @param val String containing the number to convert.
+ * @param fail Value to use if unable to convert val to a float.
+ *
+ * @return The converted float, or the specified value if unsuccessful.
+ */
+ public static float toFloat( String val, float fail )
+ {
+ if( TextUtils.isEmpty( val ) )
+ return fail; // Not a number
+
+ try
+ {
+ // Convert to float
+ return Float.parseFloat( val );
+ }
+ catch( NumberFormatException nfe )
+ {
+ }
+ // Conversion failed
+ return fail;
+ }
+
+ /**
+ * Safely sleep.
+ *
+ * @param milliseconds The sleep duration.
+ */
+ public static void sleep( int milliseconds )
+ {
+ try
+ {
+ Thread.sleep( milliseconds );
+ }
+ catch( InterruptedException e )
+ {
+ }
+ }
+
+ /**
+ * Safely wait for a thread to die.
+ *
+ * @param thread Thread to join.
+ * @param milliseconds Time to wait, in milliseconds (0 to wait indefinitely).
+ */
+ public static void join( Thread thread, int milliseconds )
+ {
+ if( thread == null || milliseconds < 0 )
+ return;
+
+ try
+ {
+ thread.join( milliseconds );
+ }
+ catch( InterruptedException e )
+ {
+ }
+ }
+
+ /**
+ * Safely executes a command and its arguments in a separate native process
+ *
+ * @param cmd Array containing the command and its arguments.
+ * @param wait Whether or not to wait for the command to finish executing.
+ *
+ * @return Array containing the output (if any), or null if wait was false.
+ */
+ public static String[] exec( String[] cmd, boolean wait )
+ {
+ try
+ {
+ Process process = Runtime.getRuntime().exec( cmd );
+ if( wait )
+ {
+ process.waitFor();
+ LinkedList output = new LinkedList();
+ BufferedReader buffer = new BufferedReader( new InputStreamReader( process.getInputStream() ) );
+ String line;
+ while( ( line = buffer.readLine() ) != null )
+ {
+ output.add( line );
+ }
+
+ // Done with reading
+ buffer.close();
+
+ if( output.size() > 0 )
+ {
+ return output.toArray( new String[ output.size() ] );
+ }
+ }
+ }
+ catch( IOException ioe )
+ {}
+ catch( InterruptedException ie )
+ {}
+
+ return null;
+ }
+}
diff --git a/Android/src/emu/project64/util/Strings.java b/Android/src/emu/project64/util/Strings.java
new file mode 100644
index 000000000..01c171613
--- /dev/null
+++ b/Android/src/emu/project64/util/Strings.java
@@ -0,0 +1,40 @@
+/****************************************************************************
+* *
+* Project64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.util;
+
+import java.util.List;
+
+public class Strings
+{
+ static public boolean startsWith(String[] array, String text)
+ {
+ for (String item : array)
+ {
+ if (text.startsWith(item))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ static public int containsName(List array, String text)
+ {
+ for (int i = array.size()-1 ; i >= 0 ; --i)
+ {
+ if (array.get(i).endsWith(text))
+ {
+ return i;
+ }
+ }
+ return -1;
+ }
+}
diff --git a/Android/src/emu/project64/util/Utility.java b/Android/src/emu/project64/util/Utility.java
new file mode 100644
index 000000000..d89144cb1
--- /dev/null
+++ b/Android/src/emu/project64/util/Utility.java
@@ -0,0 +1,85 @@
+/****************************************************************************
+* *
+* Project64 - A Nintendo 64 emulator. *
+* http://www.pj64-emu.com/ *
+* Copyright (C) 2012 Project64. All rights reserved. *
+* *
+* License: *
+* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
+* *
+****************************************************************************/
+package emu.project64.util;
+
+import java.io.BufferedReader;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStreamReader;
+/**
+ * Utility class which collects a bunch of commonly used methods into one class.
+ */
+public final class Utility
+{
+
+ /**
+ * Clamps a value to the limit defined by min and max.
+ *
+ * @param val The value to clamp to min and max.
+ * @param min The lowest number val can be equal to.
+ * @param max The largest number val can be equal to.
+ *
+ * @return If the value is lower than min, min is returned.
+ * If the value is higher than max, max is returned.
+ */
+ public static> T clamp( T val, T min, T max )
+ {
+ final T temp;
+
+ // val < max
+ if ( val.compareTo(max) < 0 )
+ temp = val;
+ else
+ temp = max;
+
+ // temp > min
+ if ( temp.compareTo(min) > 0 )
+ return temp;
+ else
+ return min;
+ }
+
+ public static String executeShellCommand(String... args)
+ {
+ try
+ {
+ Process process = Runtime.getRuntime().exec( args );
+ BufferedReader reader = new BufferedReader( new InputStreamReader( process.getInputStream() ) );
+ StringBuilder result = new StringBuilder();
+ String line;
+ while( ( line = reader.readLine() ) != null )
+ {
+ result.append( line + "\n" );
+ }
+ return result.toString();
+ }
+ catch( IOException ignored )
+ {
+ }
+ return "";
+ }
+
+ public static boolean close(Closeable closeable)
+ {
+ if (closeable != null)
+ {
+ try
+ {
+ closeable.close();
+ return true;
+ }
+ catch (IOException e)
+ {
+ }
+ }
+ return false;
+ }
+}