package org.renpy.android; import android.app.Activity; import android.content.Intent; import android.content.pm.ActivityInfo; import android.os.Bundle; import android.os.Environment; import android.view.KeyEvent; import android.view.Window; import android.view.WindowManager; import android.widget.Toast; import android.util.Log; import android.content.pm.PackageManager; import android.content.pm.ApplicationInfo; import java.io.InputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.File; import java.io.IOException; import java.util.Collections; import java.util.Iterator; // Billing import org.renpy.android.Configuration; import org.renpy.android.billing.BillingService.RequestPurchase; import org.renpy.android.billing.BillingService.RestoreTransactions; import org.renpy.android.billing.Consts.PurchaseState; import org.renpy.android.billing.Consts.ResponseCode; import org.renpy.android.billing.PurchaseObserver; import org.renpy.android.billing.BillingService; import org.renpy.android.billing.PurchaseDatabase; import org.renpy.android.billing.Consts; import org.renpy.android.billing.ResponseHandler; import org.renpy.android.billing.Security; import android.os.Handler; import android.database.Cursor; import java.util.List; import java.util.ArrayList; import android.content.SharedPreferences; import android.content.Context; public class PythonActivity extends Activity implements Runnable { private static String TAG = "Python"; // The audio thread for streaming audio... private static AudioThread mAudioThread = null; // The SDLSurfaceView we contain. public static SDLSurfaceView mView = null; public static PythonActivity mActivity = null; public static ApplicationInfo mInfo = null; // Did we launch our thread? private boolean mLaunchedThread = false; private ResourceManager resourceManager; // The path to the directory contaning our external storage. private File externalStorage; // The path to the directory containing the game. private File mPath = null; boolean _isPaused = false; private static final String DB_INITIALIZED = "db_initialized"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Hardware.context = this; Action.context = this; this.mActivity = this; getWindowManager().getDefaultDisplay().getMetrics(Hardware.metrics); resourceManager = new ResourceManager(this); externalStorage = new File(Environment.getExternalStorageDirectory(), getPackageName()); // Figure out the directory where the game is. If the game was // given to us via an intent, then we use the scheme-specific // part of that intent to determine the file to launch. We // also use the android.txt file to determine the orientation. // // Otherwise, we use the public data, if we have it, or the // private data if we do not. if (getIntent() != null && getIntent().getAction() != null && getIntent().getAction().equals("org.renpy.LAUNCH")) { mPath = new File(getIntent().getData().getSchemeSpecificPart()); Project p = Project.scanDirectory(mPath); if (p != null) { if (p.landscape) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); } else { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } } // Let old apps know they started. try { FileWriter f = new FileWriter(new File(mPath, ".launch")); f.write("started"); f.close(); } catch (IOException e) { // pass } } else if (resourceManager.getString("public_version") != null) { mPath = externalStorage; } else { mPath = getFilesDir(); } requestWindowFeature(Window.FEATURE_NO_TITLE); // go to fullscreen mode if requested try { this.mInfo = this.getPackageManager().getApplicationInfo( this.getPackageName(), PackageManager.GET_META_DATA); Log.v("python", "metadata fullscreen is" + this.mInfo.metaData.get("fullscreen")); if ( (Integer)this.mInfo.metaData.get("fullscreen") == 1 ) { getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); } } catch (PackageManager.NameNotFoundException e) { } if ( Configuration.use_billing ) { mBillingHandler = new Handler(); } // Start showing an SDLSurfaceView. mView = new SDLSurfaceView( this, mPath.getAbsolutePath()); Hardware.view = mView; //setContentView(mView); // Force the background window color if asked if ( this.mInfo.metaData.containsKey("android.background_color") ) { getWindow().getDecorView().setBackgroundColor( this.mInfo.metaData.getInt("android.background_color")); } } /** * Show an error using a toast. (Only makes sense from non-UI * threads.) */ public void toastError(final String msg) { final Activity thisActivity = this; runOnUiThread(new Runnable () { public void run() { Toast.makeText(thisActivity, msg, Toast.LENGTH_LONG).show(); } }); // Wait to show the error. synchronized (this) { try { this.wait(1000); } catch (InterruptedException e) { } } } public void recursiveDelete(File f) { if (f.isDirectory()) { for (File r : f.listFiles()) { recursiveDelete(r); } } f.delete(); } /** * This determines if unpacking one the zip files included in * the .apk is necessary. If it is, the zip file is unpacked. */ public void unpackData(final String resource, File target) { // The version of data in memory and on disk. String data_version = resourceManager.getString(resource + "_version"); String disk_version = null; // If no version, no unpacking is necessary. if (data_version == null) { return; } // Check the current disk version, if any. String filesDir = target.getAbsolutePath(); String disk_version_fn = filesDir + "/" + resource + ".version"; try { byte buf[] = new byte[64]; InputStream is = new FileInputStream(disk_version_fn); int len = is.read(buf); disk_version = new String(buf, 0, len); is.close(); } catch (Exception e) { disk_version = ""; } // If the disk data is out of date, extract it and write the // version file. if (! data_version.equals(disk_version)) { Log.v(TAG, "Extracting " + resource + " assets."); recursiveDelete(target); target.mkdirs(); AssetExtract ae = new AssetExtract(this); if (!ae.extractTar(resource + ".mp3", target.getAbsolutePath())) { toastError("Could not extract " + resource + " data."); } try { // Write .nomedia. new File(target, ".nomedia").createNewFile(); // Write version file. FileOutputStream os = new FileOutputStream(disk_version_fn); os.write(data_version.getBytes()); os.close(); } catch (Exception e) { Log.w("python", e); } } } public void run() { unpackData("private", getFilesDir()); unpackData("public", externalStorage); System.loadLibrary("sdl"); System.loadLibrary("sdl_image"); System.loadLibrary("sdl_ttf"); System.loadLibrary("sdl_mixer"); System.loadLibrary("python2.7"); System.loadLibrary("application"); System.loadLibrary("sdl_main"); System.load(getFilesDir() + "/lib/python2.7/lib-dynload/_io.so"); System.load(getFilesDir() + "/lib/python2.7/lib-dynload/unicodedata.so"); try { System.loadLibrary("sqlite3"); System.load(getFilesDir() + "/lib/python2.7/lib-dynload/_sqlite3.so"); } catch(UnsatisfiedLinkError e) { } try { System.load(getFilesDir() + "/lib/python2.7/lib-dynload/_imaging.so"); System.load(getFilesDir() + "/lib/python2.7/lib-dynload/_imagingft.so"); System.load(getFilesDir() + "/lib/python2.7/lib-dynload/_imagingmath.so"); } catch(UnsatisfiedLinkError e) { } if ( mAudioThread == null ) { Log.i("python", "Starting audio thread"); mAudioThread = new AudioThread(this); } runOnUiThread(new Runnable () { public void run() { mView.start(); } }); } @Override protected void onPause() { _isPaused = true; super.onPause(); if (mView != null) { mView.onPause(); } } @Override protected void onResume() { super.onResume(); _isPaused = false; if (!mLaunchedThread) { mLaunchedThread = true; new Thread(this).start(); } if (mView != null) { mView.onResume(); } } public boolean isPaused() { return _isPaused; } @Override public boolean onKeyDown(int keyCode, final KeyEvent event) { //Log.i("python", "key2 " + mView + " " + mView.mStarted); if (mView != null && mView.mStarted && SDLSurfaceView.nativeKey(keyCode, 1, event.getUnicodeChar())) { return true; } else { return super.onKeyDown(keyCode, event); } } @Override public boolean onKeyUp(int keyCode, final KeyEvent event) { //Log.i("python", "key up " + mView + " " + mView.mStarted); if (mView != null && mView.mStarted && SDLSurfaceView.nativeKey(keyCode, 0, event.getUnicodeChar())) { return true; } else { return super.onKeyUp(keyCode, event); } } protected void onDestroy() { if (mPurchaseDatabase != null) { mPurchaseDatabase.close(); } if (mBillingService != null) { mBillingService.unbind(); } if (mView != null) { mView.onDestroy(); } //Log.i(TAG, "on destroy (exit1)"); System.exit(0); } public static void start_service(String serviceTitle, String serviceDescription, String pythonServiceArgument) { Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class); String argument = PythonActivity.mActivity.getFilesDir().getAbsolutePath(); String filesDirectory = PythonActivity.mActivity.mPath.getAbsolutePath(); serviceIntent.putExtra("androidPrivate", argument); serviceIntent.putExtra("androidArgument", filesDirectory); serviceIntent.putExtra("pythonHome", argument); serviceIntent.putExtra("pythonPath", argument + ":" + filesDirectory + "/lib"); serviceIntent.putExtra("serviceTitle", serviceTitle); serviceIntent.putExtra("serviceDescription", serviceDescription); serviceIntent.putExtra("pythonServiceArgument", pythonServiceArgument); PythonActivity.mActivity.startService(serviceIntent); } public static void stop_service() { Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class); PythonActivity.mActivity.stopService(serviceIntent); } //---------------------------------------------------------------------------- // Listener interface for onNewIntent // public interface NewIntentListener { void onNewIntent(Intent intent); } private List newIntentListeners = null; public void registerNewIntentListener(NewIntentListener listener) { if ( this.newIntentListeners == null ) this.newIntentListeners = Collections.synchronizedList(new ArrayList()); this.newIntentListeners.add(listener); } public void unregisterNewIntentListener(NewIntentListener listener) { if ( this.newIntentListeners == null ) return; this.newIntentListeners.remove(listener); } @Override protected void onNewIntent(Intent intent) { if ( this.newIntentListeners == null ) return; if ( this.mView != null ) this.mView.onResume(); synchronized ( this.newIntentListeners ) { Iterator iterator = this.newIntentListeners.iterator(); while ( iterator.hasNext() ) { (iterator.next()).onNewIntent(intent); } } } //---------------------------------------------------------------------------- // Listener interface for onActivityResult // public interface ActivityResultListener { void onActivityResult(int requestCode, int resultCode, Intent data); } private List activityResultListeners = null; public void registerActivityResultListener(ActivityResultListener listener) { if ( this.activityResultListeners == null ) this.activityResultListeners = Collections.synchronizedList(new ArrayList()); this.activityResultListeners.add(listener); } public void unregisterActivityResultListener(ActivityResultListener listener) { if ( this.activityResultListeners == null ) return; this.activityResultListeners.remove(listener); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { if ( this.activityResultListeners == null ) return; if ( this.mView != null ) this.mView.onResume(); synchronized ( this.activityResultListeners ) { Iterator iterator = this.activityResultListeners.iterator(); while ( iterator.hasNext() ) (iterator.next()).onActivityResult(requestCode, resultCode, intent); } } //---------------------------------------------------------------------------- // Billing // class PythonPurchaseObserver extends PurchaseObserver { public PythonPurchaseObserver(Handler handler) { super(PythonActivity.this, handler); } @Override public void onBillingSupported(boolean supported, String type) { if (Consts.DEBUG) { Log.i(TAG, "supported: " + supported); } String sup = "1"; if ( !supported ) sup = "0"; if (type == null) type = Consts.ITEM_TYPE_INAPP; // add notification for python message queue mActivity.mBillingQueue.add("billingSupported|" + type + "|" + sup); // for managed items, restore the database if ( type == Consts.ITEM_TYPE_INAPP && supported ) { restoreDatabase(); } } @Override public void onPurchaseStateChange(PurchaseState purchaseState, String itemId, int quantity, long purchaseTime, String developerPayload) { mActivity.mBillingQueue.add( "purchaseStateChange|" + itemId + "|" + purchaseState.toString()); } @Override public void onRequestPurchaseResponse(RequestPurchase request, ResponseCode responseCode) { mActivity.mBillingQueue.add( "requestPurchaseResponse|" + request.mProductId + "|" + responseCode.toString()); } @Override public void onRestoreTransactionsResponse(RestoreTransactions request, ResponseCode responseCode) { if (responseCode == ResponseCode.RESULT_OK) { mActivity.mBillingQueue.add("restoreTransaction|ok"); if (Consts.DEBUG) { Log.d(TAG, "completed RestoreTransactions request"); } // Update the shared preferences so that we don't perform // a RestoreTransactions again. SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE); SharedPreferences.Editor edit = prefs.edit(); edit.putBoolean(DB_INITIALIZED, true); edit.commit(); } else { if (Consts.DEBUG) { Log.d(TAG, "RestoreTransactions error: " + responseCode); } mActivity.mBillingQueue.add( "restoreTransaction|error|" + responseCode.toString()); } } } /** * If the database has not been initialized, we send a * RESTORE_TRANSACTIONS request to Android Market to get the list of purchased items * for this user. This happens if the application has just been installed * or the user wiped data. We do not want to do this on every startup, rather, we want to do * only when the database needs to be initialized. */ private void restoreDatabase() { SharedPreferences prefs = getPreferences(MODE_PRIVATE); boolean initialized = prefs.getBoolean(DB_INITIALIZED, false); if (!initialized) { mBillingService.restoreTransactions(); } } /** An array of product list entries for the products that can be purchased. */ private enum Managed { MANAGED, UNMANAGED, SUBSCRIPTION } private PythonPurchaseObserver mPythonPurchaseObserver; private Handler mBillingHandler; private BillingService mBillingService; private PurchaseDatabase mPurchaseDatabase; private String mPayloadContents; public List mBillingQueue; public void billingServiceStart_() { mBillingQueue = new ArrayList(); // Start the billing part mPythonPurchaseObserver = new PythonPurchaseObserver(mBillingHandler); mBillingService = new BillingService(); mBillingService.setContext(this); mPurchaseDatabase = new PurchaseDatabase(this); ResponseHandler.register(mPythonPurchaseObserver); if (!mBillingService.checkBillingSupported()) { //showDialog(DIALOG_CANNOT_CONNECT_ID); Log.w(TAG, "NO BILLING SUPPORTED"); } if (!mBillingService.checkBillingSupported(Consts.ITEM_TYPE_SUBSCRIPTION)) { //showDialog(DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID); Log.w(TAG, "NO SUBSCRIPTION SUPPORTED"); } } public void billingServiceStop_() { } public void billingBuy_(String mSku) { Managed mManagedType = Managed.MANAGED; if (Consts.DEBUG) { Log.d(TAG, "buying sku: " + mSku); } if (mManagedType == Managed.MANAGED) { if (!mBillingService.requestPurchase(mSku, Consts.ITEM_TYPE_INAPP, mPayloadContents)) { Log.w(TAG, "ERROR IN BILLING REQUEST PURCHASE"); } } else if (mManagedType == Managed.SUBSCRIPTION) { if (!mBillingService.requestPurchase(mSku, Consts.ITEM_TYPE_INAPP, mPayloadContents)) { Log.w(TAG, "ERROR IN BILLING REQUEST PURCHASE"); } } } public String billingGetPurchasedItems_() { String ownedItems = ""; Cursor cursor = mPurchaseDatabase.queryAllPurchasedItems(); if (cursor == null) return ""; try { int productIdCol = cursor.getColumnIndexOrThrow( PurchaseDatabase.PURCHASED_PRODUCT_ID_COL); int qtCol = cursor.getColumnIndexOrThrow( PurchaseDatabase.PURCHASED_QUANTITY_COL); while (cursor.moveToNext()) { String productId = cursor.getString(productIdCol); String qt = cursor.getString(qtCol); productId = Security.unobfuscate(this, Configuration.billing_salt, productId); if ( productId == null ) continue; if ( ownedItems != "" ) ownedItems += "\n"; ownedItems += productId + "," + qt; } } finally { cursor.close(); } return ownedItems; } static void billingServiceStart() { mActivity.billingServiceStart_(); } static void billingServiceStop() { mActivity.billingServiceStop_(); } static void billingBuy(String sku) { mActivity.billingBuy_(sku); } static String billingGetPurchasedItems() { return mActivity.billingGetPurchasedItems_(); } static String billingGetPendingMessage() { if (mActivity.mBillingQueue.isEmpty()) return null; return mActivity.mBillingQueue.remove(0); } }