diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 58d46ed8..b6cc6f26 100755 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -3,14 +3,23 @@ package="org.purplei2p.i2pd" android:versionCode="1" android:versionName="1.0"> - - - + + + + + + + + + + + diff --git a/android/jni/Android.mk b/android/jni/Android.mk index bef6d5ec..90a679b2 100755 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -12,7 +12,6 @@ LOCAL_STATIC_LIBRARIES := \ miniupnpc LOCAL_LDLIBS := -lz -#LOCAL_CFLAGS := LOCAL_SRC_FILES := DaemonAndroid.cpp i2pd_android.cpp \ $(IFADDRS_PATH)/ifaddrs.c \ ../../HTTPServer.cpp ../../I2PControl.cpp ../../Daemon.cpp ../../Config.cpp \ @@ -58,9 +57,8 @@ LOCAL_SRC_FILES := DaemonAndroid.cpp i2pd_android.cpp \ ../../TunnelEndpoint.cpp \ ../../TunnelGateway.cpp \ ../../TunnelPool.cpp \ - ../../UPnP.cpp \ ../../util.cpp \ - ../../i2pd.cpp + ../../i2pd.cpp ../../UPnP.cpp include $(BUILD_SHARED_LIBRARY) diff --git a/android/jni/Application.mk b/android/jni/Application.mk index 34f9dd63..e8a51add 100755 --- a/android/jni/Application.mk +++ b/android/jni/Application.mk @@ -1,5 +1,7 @@ #APP_ABI := all -APP_ABI := armeabi-v7a x86 +#APP_ABI := armeabi-v7a x86 +#APP_ABI := x86 +APP_ABI := armeabi-v7a #can be android-3 but will fail for x86 since arch-x86 is not present at ndkroot/platforms/android-3/ . libz is taken from there. APP_PLATFORM := android-9 @@ -11,7 +13,7 @@ APP_STL := gnustl_shared # Enable c++11 extentions in source code APP_CPPFLAGS += -std=c++11 -APP_CPPFLAGS += -DUSE_UPNP -DANDROID -D__ANDROID__ +APP_CPPFLAGS += -DANDROID -D__ANDROID__ -DUSE_UPNP ifeq ($(TARGET_ARCH_ABI),armeabi-v7a) APP_CPPFLAGS += -DANDROID_ARM7A endif diff --git a/android/jni/DaemonAndroid.cpp b/android/jni/DaemonAndroid.cpp index 02f6e3f7..038a07fa 100644 --- a/android/jni/DaemonAndroid.cpp +++ b/android/jni/DaemonAndroid.cpp @@ -1,5 +1,9 @@ #include "DaemonAndroid.h" #include "../../Daemon.h" +#include +#include +#include +#include //#include "mainwindow.h" namespace i2p @@ -58,10 +62,11 @@ namespace android } } */ - DaemonAndroidImpl::DaemonAndroidImpl (): + DaemonAndroidImpl::DaemonAndroidImpl () + //: /*mutex(nullptr), */ - m_IsRunning(false), - m_RunningChangedCallback(nullptr) + //m_IsRunning(false), + //m_RunningChangedCallback(nullptr) { } @@ -73,15 +78,15 @@ namespace android bool DaemonAndroidImpl::init(int argc, char* argv[]) { //mutex=new QMutex(QMutex::Recursive); - setRunningCallback(0); - m_IsRunning=false; + //setRunningCallback(0); + //m_IsRunning=false; return Daemon.init(argc,argv); } void DaemonAndroidImpl::start() { //QMutexLocker locker(mutex); - setRunning(true); + //setRunning(true); Daemon.start(); } @@ -89,7 +94,7 @@ namespace android { //QMutexLocker locker(mutex); Daemon.stop(); - setRunning(false); + //setRunning(false); } void DaemonAndroidImpl::restart() @@ -98,7 +103,7 @@ namespace android stop(); start(); } - + /* void DaemonAndroidImpl::setRunningCallback(runningChangedCallback cb) { m_RunningChangedCallback = cb; @@ -119,46 +124,65 @@ namespace android m_RunningChangedCallback(); } } - +*/ static DaemonAndroidImpl daemon; - + static char* argv[1]={strdup("tmp")}; /** - * returns 1 if daemon init failed - * returns 0 if daemon initialized and started okay + * returns error details if failed + * returns "ok" if daemon initialized and started okay */ - int start(/*int argc, char* argv[]*/) + std::string start(/*int argc, char* argv[]*/) { - int result; - + try { - //Log.d(TAG"Initialising the daemon..."); - bool daemonInitSuccess = daemon.init(0,0/*argc, argv*/); - if(!daemonInitSuccess) - { - //QMessageBox::critical(0, "Error", "Daemon init failed"); - return 1; - } - //Log.d(TAG"Initialised, creating the main window..."); - //MainWindow w; - //Log.d(TAG"Before main window.show()..."); - //w.show (); + //int result; { - //i2p::qt::Controller daemonQtController(daemon); - //Log.d(TAG"Starting the daemon..."); - //emit daemonQtController.startDaemon(); - //daemon.start (); - //Log.d(TAG"Starting GUI event loop..."); - //result = app.exec(); - //daemon.stop (); - daemon.start(); - return 0; + //Log.d(TAG"Initialising the daemon..."); + bool daemonInitSuccess = daemon.init(1,argv); + if(!daemonInitSuccess) + { + //QMessageBox::critical(0, "Error", "Daemon init failed"); + return "Daemon init failed"; + } + //Log.d(TAG"Initialised, creating the main window..."); + //MainWindow w; + //Log.d(TAG"Before main window.show()..."); + //w.show (); + + { + //i2p::qt::Controller daemonQtController(daemon); + //Log.d(TAG"Starting the daemon..."); + //emit daemonQtController.startDaemon(); + //daemon.start (); + //Log.d(TAG"Starting GUI event loop..."); + //result = app.exec(); + //daemon.stop (); + daemon.start(); + } } - } - //QMessageBox::information(&w, "Debug", "demon stopped"); - //Log.d(TAG"Exiting the application"); - //return result; + //QMessageBox::information(&w, "Debug", "demon stopped"); + //Log.d(TAG"Exiting the application"); + //return result; + } + catch (boost::exception& ex) + { + std::stringstream ss; + ss << boost::diagnostic_information(ex); + return ss.str(); + } + catch (std::exception& ex) + { + std::stringstream ss; + ss << ex.what(); + return ss.str(); + } + catch(...) + { + return "unknown exception"; + } + return "ok"; } void stop() diff --git a/android/jni/DaemonAndroid.h b/android/jni/DaemonAndroid.h index f6ee618f..9cc8219b 100644 --- a/android/jni/DaemonAndroid.h +++ b/android/jni/DaemonAndroid.h @@ -1,11 +1,12 @@ #ifndef DAEMON_ANDROID_H #define DAEMON_ANDROID_H +#include + namespace i2p { namespace android { - //FIXME currently NOT threadsafe class DaemonAndroidImpl { public: @@ -13,7 +14,7 @@ namespace android DaemonAndroidImpl (); ~DaemonAndroidImpl (); - typedef void (*runningChangedCallback)(); + //typedef void (*runningChangedCallback)(); /** * @return success @@ -22,21 +23,21 @@ namespace android void start(); void stop(); void restart(); - void setRunningCallback(runningChangedCallback cb); - bool isRunning(); + //void setRunningCallback(runningChangedCallback cb); + //bool isRunning(); private: - void setRunning(bool running); + //void setRunning(bool running); private: //QMutex* mutex; - bool m_IsRunning; - runningChangedCallback m_RunningChangedCallback; + //bool m_IsRunning; + //runningChangedCallback m_RunningChangedCallback; }; /** - * returns 1 if daemon init failed - * returns 0 if daemon initialized and started okay + * returns "ok" if daemon init failed + * returns errinfo if daemon initialized and started okay */ - int start(); + std::string start(); // stops the daemon void stop(); diff --git a/android/jni/i2pd_android.cpp b/android/jni/i2pd_android.cpp index b84ec1ac..40b50345 100755 --- a/android/jni/i2pd_android.cpp +++ b/android/jni/i2pd_android.cpp @@ -3,6 +3,7 @@ #include #include "org_purplei2p_i2pd_I2PD_JNI.h" #include "DaemonAndroid.h" +#include "../../RouterContext.h" JNIEXPORT jstring JNICALL Java_org_purplei2p_i2pd_I2PD_1JNI_getABICompiledWith (JNIEnv * env, jclass clazz) { @@ -41,9 +42,9 @@ JNIEXPORT jstring JNICALL Java_org_purplei2p_i2pd_I2PD_1JNI_getABICompiledWith return env->NewStringUTF(ABI); } -JNIEXPORT jint JNICALL Java_org_purplei2p_i2pd_I2PD_1JNI_startDaemon +JNIEXPORT jstring JNICALL Java_org_purplei2p_i2pd_I2PD_1JNI_startDaemon (JNIEnv * env, jclass clazz) { - return (jint)i2p::android::start(); + return env->NewStringUTF(i2p::android::start().c_str()); } JNIEXPORT void JNICALL Java_org_purplei2p_i2pd_I2PD_1JNI_stopDaemon @@ -51,3 +52,12 @@ JNIEXPORT void JNICALL Java_org_purplei2p_i2pd_I2PD_1JNI_stopDaemon i2p::android::stop(); } +JNIEXPORT void JNICALL Java_org_purplei2p_i2pd_I2PD_1JNI_stopAcceptingTunnels + (JNIEnv * env, jclass clazz) { + i2p::context.SetAcceptsTunnels (false); +} + +JNIEXPORT void JNICALL Java_org_purplei2p_i2pd_I2PD_1JNI_onNetworkStateChanged + (JNIEnv * env, jclass clazz, jboolean isConnected) { + bool isConnectedBool = (bool) isConnected; +} diff --git a/android/jni/org_purplei2p_i2pd_I2PD_JNI.h b/android/jni/org_purplei2p_i2pd_I2PD_JNI.h index ddbcace8..04923d22 100644 --- a/android/jni/org_purplei2p_i2pd_I2PD_JNI.h +++ b/android/jni/org_purplei2p_i2pd_I2PD_JNI.h @@ -15,6 +15,18 @@ extern "C" { JNIEXPORT jstring JNICALL Java_org_purplei2p_i2pd_I2PD_1JNI_getABICompiledWith (JNIEnv *, jclass); +JNIEXPORT jstring JNICALL Java_org_purplei2p_i2pd_I2PD_1JNI_startDaemon + (JNIEnv *, jclass); + +JNIEXPORT void JNICALL Java_org_purplei2p_i2pd_I2PD_1JNI_stopDaemon + (JNIEnv *, jclass); + +JNIEXPORT void JNICALL Java_org_purplei2p_i2pd_I2PD_1JNI_stopAcceptingTunnels + (JNIEnv *, jclass); + +JNIEXPORT void JNICALL Java_org_purplei2p_i2pd_I2PD_1JNI_onNetworkStateChanged + (JNIEnv * env, jclass clazz, jboolean isConnected); + #ifdef __cplusplus } #endif diff --git a/android/project.properties b/android/project.properties index c6998b3d..7ce68660 100644 --- a/android/project.properties +++ b/android/project.properties @@ -11,4 +11,4 @@ #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt # Project target. -target=android-9 +target=android-24 diff --git a/android/res/menu/options_main.xml b/android/res/menu/options_main.xml new file mode 100644 index 00000000..388dfd83 --- /dev/null +++ b/android/res/menu/options_main.xml @@ -0,0 +1,16 @@ + + + + diff --git a/android/res/values/strings.xml b/android/res/values/strings.xml index b02181c8..8c78e88b 100755 --- a/android/res/values/strings.xml +++ b/android/res/values/strings.xml @@ -1,4 +1,9 @@ i2pd + i2pd started + Quit + Graceful Quit + Graceful quit is already in progress + Graceful quit is in progress diff --git a/android/src/org/purplei2p/i2pd/ForegroundService.java b/android/src/org/purplei2p/i2pd/ForegroundService.java new file mode 100644 index 00000000..6ff826c4 --- /dev/null +++ b/android/src/org/purplei2p/i2pd/ForegroundService.java @@ -0,0 +1,88 @@ +package org.purplei2p.i2pd; + +import android.app.Notification; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; +import android.support.v4.app.NotificationCompat; +import android.util.Log; + +public class ForegroundService extends Service { +// private NotificationManager mNM; + + // Unique Identification Number for the Notification. + // We use it on Notification start, and to cancel it. + private int NOTIFICATION = R.string.i2pd_started; + + /** + * Class for clients to access. Because we know this service always + * runs in the same process as its clients, we don't need to deal with + * IPC. + */ + public class LocalBinder extends Binder { + ForegroundService getService() { + return ForegroundService.this; + } + } + + @Override + public void onCreate() { +// mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); + + // Display a notification about us starting. We put an icon in the status bar. + showNotification(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.i("ForegroundService", "Received start id " + startId + ": " + intent); + return START_NOT_STICKY; + } + + @Override + public void onDestroy() { + // Cancel the persistent notification. + //mNM.cancel(NOTIFICATION); + stopForeground(true); + + // Tell the user we stopped. + //Toast.makeText(this, R.string.local_service_stopped, Toast.LENGTH_SHORT).show(); + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + // This is the object that receives interactions from clients. See + // RemoteService for a more complete example. + private final IBinder mBinder = new LocalBinder(); + + /** + * Show a notification while this service is running. + */ + private void showNotification() { + // In this sample, we'll use the same text for the ticker and the expanded notification + CharSequence text = getText(R.string.i2pd_started); + + // The PendingIntent to launch our activity if the user selects this notification + PendingIntent contentIntent = PendingIntent.getActivity(this, 0, + new Intent(this, I2PD.class), 0); + + // Set the info for the views that show in the notification panel. + Notification notification = new NotificationCompat.Builder(this) + .setSmallIcon(R.drawable.itoopie_notification_icon) // the status icon + .setTicker(text) // the status text + .setWhen(System.currentTimeMillis()) // the time stamp + .setContentTitle(getText(R.string.app_name)) // the label of the entry + .setContentText(text) // the contents of the entry + .setContentIntent(contentIntent) // The intent to send when the entry is clicked + .build(); + + // Send the notification. + //mNM.notify(NOTIFICATION, notification); + startForeground(NOTIFICATION, notification); + } +} \ No newline at end of file diff --git a/android/src/org/purplei2p/i2pd/I2PD.java b/android/src/org/purplei2p/i2pd/I2PD.java index 6f77c53f..ef22f941 100755 --- a/android/src/org/purplei2p/i2pd/I2PD.java +++ b/android/src/org/purplei2p/i2pd/I2PD.java @@ -1,20 +1,272 @@ package org.purplei2p.i2pd; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Timer; +import java.util.TimerTask; + +import android.annotation.SuppressLint; import android.app.Activity; -import android.widget.TextView; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Build; import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.TextView; +import android.widget.Toast; public class I2PD extends Activity { + private static final String TAG = "i2pd"; + + private static Throwable loadLibsThrowable; + static { + try { + I2PD_JNI.loadLibraries(); + } catch (Throwable tr) { + loadLibsThrowable = tr; + } + } + private String daemonStartResult="N/A"; + private boolean destroyed=false; + private TextView textView; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + destroyed=false; + + //set the app be foreground (do not unload when RAM needed) + doBindService(); - TextView tv = new TextView(this); - tv.setText( "libi2pd.so was compiled with ABI " + getABICompiledWith()); - setContentView(tv); + textView = new TextView(this); + setContentView(textView); + if (loadLibsThrowable != null) { + textView.setText(throwableToString(loadLibsThrowable)+"\r\n"); + return; + } + try { + textView.setText( + "libi2pd.so was compiled with ABI " + getABICompiledWith() + "\r\n"+ + "Starting daemon... "); + new Thread(new Runnable(){ + + @Override + public void run() { + try { + doStartDaemon(); + } catch (final Throwable tr) { + appendThrowable(tr); + } + } + + },"i2pdDaemonStarting").start(); + } catch (Throwable tr) { + textView.setText(textView.getText().toString()+throwableToString(tr)); + } } - public String getABICompiledWith() { + @Override + protected void onDestroy() { + super.onDestroy(); + localDestroy(); + } + + private synchronized void localDestroy() { + if(destroyed)return; + destroyed=true; + if(gracefulQuitTimer!=null) { + gracefulQuitTimer.cancel(); + gracefulQuitTimer = null; + } + try{ + doUnbindService(); + }catch(Throwable tr){ + Log.e(TAG, "", tr); + } + if("ok".equals(daemonStartResult)) { + try { + I2PD_JNI.stopDaemon(); + } catch (Throwable tr) { + Log.e(TAG, "error", tr); + } + } + } + + private CharSequence throwableToString(Throwable tr) { + StringWriter sw = new StringWriter(8192); + PrintWriter pw = new PrintWriter(sw); + tr.printStackTrace(pw); + pw.close(); + return sw.toString(); + } + + public String getABICompiledWith() { return I2PD_JNI.getABICompiledWith(); } + + private synchronized void doStartDaemon() { + if(destroyed)return; + daemonStartResult = I2PD_JNI.startDaemon(); + runOnUiThread(new Runnable(){ + + @Override + public void run() { + synchronized (I2PD.this) { + if(destroyed)return; + textView.setText( + textView.getText().toString()+ + "start result: "+daemonStartResult+"\r\n" + ); + } + } + + }); + } + + private void appendThrowable(final Throwable tr) { + runOnUiThread(new Runnable(){ + + @Override + public void run() { + synchronized (I2PD.this) { + if(destroyed)return; + textView.setText(textView.getText().toString()+throwableToString(tr)+"\r\n"); + } + } + }); + } + +// private LocalService mBoundService; + + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + // This is called when the connection with the service has been + // established, giving us the service object we can use to + // interact with the service. Because we have bound to a explicit + // service that we know is running in our own process, we can + // cast its IBinder to a concrete class and directly access it. +// mBoundService = ((LocalService.LocalBinder)service).getService(); + + // Tell the user about this for our demo. +// Toast.makeText(Binding.this, R.string.local_service_connected, +// Toast.LENGTH_SHORT).show(); + } + + public void onServiceDisconnected(ComponentName className) { + // This is called when the connection with the service has been + // unexpectedly disconnected -- that is, its process crashed. + // Because it is running in our same process, we should never + // see this happen. +// mBoundService = null; +// Toast.makeText(Binding.this, R.string.local_service_disconnected, +// Toast.LENGTH_SHORT).show(); + } + }; + + + private boolean mIsBound; + + private void doBindService() { + // Establish a connection with the service. We use an explicit + // class name because we want a specific service implementation that + // we know will be running in our own process (and thus won't be + // supporting component replacement by other applications). + bindService(new Intent(this, + ForegroundService.class), mConnection, Context.BIND_AUTO_CREATE); + mIsBound = true; + } + + private void doUnbindService() { + if (mIsBound) { + // Detach our existing connection. + unbindService(mConnection); + mIsBound = false; + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.options_main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + + switch(id){ + case R.id.action_quit: + quit(); + return true; + case R.id.action_graceful_quit: + gracefulQuit(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + @SuppressLint("NewApi") + private void quit() { + try { + localDestroy(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + finishAndRemoveTask(); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + finishAffinity(); + } else { + //moveTaskToBack(true); + finish(); + } + }catch (Throwable tr) { + Log.e(TAG, "", tr); + } + } + + private Timer gracefulQuitTimer; + private synchronized void gracefulQuit() { + if(gracefulQuitTimer!=null){ + Toast.makeText(this, R.string.graceful_quit_is_already_in_progress, + Toast.LENGTH_SHORT).show(); + return; + } + Toast.makeText(this, R.string.graceful_quit_is_in_progress, + Toast.LENGTH_SHORT).show(); + new Thread(new Runnable(){ + + @Override + public void run() { + try{ + synchronized (I2PD.this) { + if("ok".equals(daemonStartResult)) { + I2PD_JNI.stopAcceptingTunnels(); + gracefulQuitTimer = new Timer(true); + gracefulQuitTimer.schedule(new TimerTask(){ + + @Override + public void run() { + quit(); + } + + }, 10*60*1000/*millis*/); + }else{ + quit(); + } + } + } catch(Throwable tr) { + Log.e(TAG,"",tr); + } + } + + },"gracQuitInit").start(); + } } diff --git a/android/src/org/purplei2p/i2pd/I2PD_JNI.java b/android/src/org/purplei2p/i2pd/I2PD_JNI.java index 040cca1c..4f5913e3 100644 --- a/android/src/org/purplei2p/i2pd/I2PD_JNI.java +++ b/android/src/org/purplei2p/i2pd/I2PD_JNI.java @@ -3,14 +3,19 @@ package org.purplei2p.i2pd; public class I2PD_JNI { public static native String getABICompiledWith(); /** - * returns 1 if daemon init failed - * returns 0 if daemon initialized and started okay + * returns error info if failed + * returns "ok" if daemon initialized and started okay */ - public static native int startDaemon(); + public static native String startDaemon(); //should only be called after startDaemon() success public static native void stopDaemon(); + + public static native void stopAcceptingTunnels(); + + public static native void onNetworkStateChanged(boolean isConnected); - static { + public static void loadLibraries() { + //System.loadLibrary("gnustl_shared"); System.loadLibrary("i2pd"); } } diff --git a/android/src/org/purplei2p/i2pd/NetworkStateChangeReceiver.java b/android/src/org/purplei2p/i2pd/NetworkStateChangeReceiver.java new file mode 100644 index 00000000..e2f284b0 --- /dev/null +++ b/android/src/org/purplei2p/i2pd/NetworkStateChangeReceiver.java @@ -0,0 +1,30 @@ +package org.purplei2p.i2pd; + +import android.util.Log; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +public class NetworkStateChangeReceiver extends BroadcastReceiver { + + private static final String TAG = "i2pd"; + + //api level 1 + @Override + public void onReceive(final Context context, final Intent intent) { + Log.d(TAG,"Network state change"); + try { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetworkInfo = cm.getActiveNetworkInfo(); + boolean isConnected = activeNetworkInfo!=null && activeNetworkInfo.isConnected(); + // https://developer.android.com/training/monitoring-device-state/connectivity-monitoring.html?hl=ru + // boolean isWiFi = activeNetworkInfo!=null && (activeNetworkInfo.getType() == ConnectivityManager.TYPE_WIFI); + + I2PD_JNI.onNetworkStateChanged(isConnected); + } catch (Throwable tr) { + Log.d(TAG,"",tr); + } + } +}