diff --git a/android/.gitignore b/android/.gitignore
index 297d122f..c1f63742 100644
--- a/android/.gitignore
+++ b/android/.gitignore
@@ -12,5 +12,4 @@ local.properties
build.sh
android.iml
build
-
-
+*.iml
diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml
index 2ae14711..757b35bb 100755
--- a/android/AndroidManifest.xml
+++ b/android/AndroidManifest.xml
@@ -9,6 +9,7 @@
+
remaining
Prompt
SD card write permission denied, you need to allow this to continue
+ Battery optimizations enabled
+ Your device is doing some heavy battery optimizations on I2PD that might lead to daemon closing with no other reason.\nIt is recommended to disable those battery optimizations.
+ Your device is doing some heavy battery optimizations on I2PD that might lead to daemon closing with no other reason.\n\nYou will now be asked to disable those.
+ Next
+ Your device does not support opting out of battery optimizations
diff --git a/android/src/org/purplei2p/i2pd/ForegroundService.java b/android/src/org/purplei2p/i2pd/ForegroundService.java
index 5c10e138..c1b1cc26 100644
--- a/android/src/org/purplei2p/i2pd/ForegroundService.java
+++ b/android/src/org/purplei2p/i2pd/ForegroundService.java
@@ -1,6 +1,5 @@
package org.purplei2p.i2pd;
-import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
@@ -11,10 +10,9 @@ import android.content.Intent;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
-import android.support.annotation.RequiresApi;
-import android.support.v4.app.NotificationCompat;
+import androidx.annotation.RequiresApi;
+import androidx.core.app.NotificationCompat;
import android.util.Log;
-import android.widget.Toast;
public class ForegroundService extends Service {
private static final String TAG="FgService";
@@ -112,14 +110,15 @@ public class ForegroundService extends Service {
// If earlier version channel ID is not used
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
- String channelId = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) ? createNotificationChannel() : "";
+ String channelId = Build.VERSION.SDK_INT >= 26 ? createNotificationChannel() : "";
// Set the info for the views that show in the notification panel.
- Notification notification = new NotificationCompat.Builder(this, channelId)
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(this, channelId)
.setOngoing(true)
- .setSmallIcon(R.drawable.itoopie_notification_icon) // the status icon
- .setPriority(Notification.PRIORITY_DEFAULT)
- .setCategory(Notification.CATEGORY_SERVICE)
+ .setSmallIcon(R.drawable.itoopie_notification_icon); // the status icon
+ if(Build.VERSION.SDK_INT >= 16) builder = builder.setPriority(Notification.PRIORITY_DEFAULT);
+ if(Build.VERSION.SDK_INT >= 21) builder = builder.setCategory(Notification.CATEGORY_SERVICE);
+ Notification notification = builder
.setTicker(text) // the status text
.setWhen(System.currentTimeMillis()) // the time stamp
.setContentTitle(getText(R.string.app_name)) // the label of the entry
@@ -141,9 +140,10 @@ public class ForegroundService extends Service {
//chan.setLightColor(Color.PURPLE);
chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
NotificationManager service = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
- service.createNotificationChannel(chan);
+ if(service!=null)service.createNotificationChannel(chan);
+ else Log.e(TAG, "error: NOTIFICATION_SERVICE is null");
return channelId;
}
- private static final DaemonSingleton daemon = DaemonSingleton.getInstance();
+ private static final DaemonSingleton daemon = DaemonSingleton.getInstance();
}
diff --git a/android/src/org/purplei2p/i2pd/I2PDActivity.java b/android/src/org/purplei2p/i2pd/I2PDActivity.java
index b5f85c5b..d97df833 100755
--- a/android/src/org/purplei2p/i2pd/I2PDActivity.java
+++ b/android/src/org/purplei2p/i2pd/I2PDActivity.java
@@ -14,24 +14,34 @@ import java.util.Timer;
import java.util.TimerTask;
import android.Manifest;
+import android.annotation.SuppressLint;
import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
+import android.content.SharedPreferences;
import android.content.res.AssetManager;
import android.content.pm.PackageManager;
+import android.net.Uri;
import android.os.Bundle;
import android.os.Build;
import android.os.Environment;
import android.os.IBinder;
+import android.os.PowerManager;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
import android.widget.Toast;
-import android.support.v4.app.ActivityCompat;
-import android.support.v4.content.ContextCompat;
+
+import androidx.annotation.NonNull;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
// For future package update checking
import org.purplei2p.i2pd.BuildConfig;
@@ -40,6 +50,7 @@ public class I2PDActivity extends Activity {
private static final String TAG = "i2pdActvt";
private static final int MY_PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE = 1;
public static final int GRACEFUL_DELAY_MILLIS = 10 * 60 * 1000;
+ public static final String PACKAGE_URI_SCHEME = "package:";
private TextView textView;
private boolean assetsCopied;
@@ -53,28 +64,22 @@ public class I2PDActivity extends Activity {
public void daemonStateUpdate()
{
processAssets();
- runOnUiThread(new Runnable(){
-
- @Override
- public void run() {
- try {
- if(textView==null) return;
- Throwable tr = daemon.getLastThrowable();
- if(tr!=null) {
- textView.setText(throwableToString(tr));
- return;
- }
- DaemonSingleton.State state = daemon.getState();
- textView.setText(
- String.valueOf(getText(state.getStatusStringResourceId()))+
- (DaemonSingleton.State.startFailed.equals(state) ? ": "+daemon.getDaemonStartResult() : "")+
- (DaemonSingleton.State.gracefulShutdownInProgress.equals(state) ? ": "+formatGraceTimeRemaining()+" "+getText(R.string.remaining) : "")
- );
- } catch (Throwable tr) {
- Log.e(TAG,"error ignored",tr);
- }
- }
- });
+ runOnUiThread(() -> {
+ try {
+ if(textView==null) return;
+ Throwable tr = daemon.getLastThrowable();
+ if(tr!=null) {
+ textView.setText(throwableToString(tr));
+ return;
+ }
+ DaemonSingleton.State state = daemon.getState();
+ String startResultStr = DaemonSingleton.State.startFailed.equals(state) ? String.format(": %s", daemon.getDaemonStartResult()) : "";
+ String graceStr = DaemonSingleton.State.gracefulShutdownInProgress.equals(state) ? String.format(": %s %s", formatGraceTimeRemaining(), getText(R.string.remaining)) : "";
+ textView.setText(String.format("%s%s%s", getText(state.getStatusStringResourceId()), startResultStr, graceStr));
+ } catch (Throwable tr) {
+ Log.e(TAG,"error ignored",tr);
+ }
+ });
}
};
private static volatile long graceStartedMillis;
@@ -92,6 +97,7 @@ public class I2PDActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
+ Log.i(TAG, "onCreate");
super.onCreate(savedInstanceState);
textView = new TextView(this);
@@ -121,6 +127,8 @@ public class I2PDActivity extends Activity {
}
rescheduleGraceStop(gracefulQuitTimer, gracefulStopAtMillis);
}
+
+ openBatteryOptimizationDialogIfNeeded();
}
@Override
@@ -137,21 +145,17 @@ public class I2PDActivity extends Activity {
}
@Override
- public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults)
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)
{
- switch (requestCode)
- {
- case MY_PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE:
- {
- if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
- Log.e(TAG, "Memory permission granted");
- else
- Log.e(TAG, "Memory permission declined");
- // TODO: terminate
- return;
- }
- default: ;
- }
+ if (requestCode == MY_PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
+ Log.e(TAG, "WR_EXT_STORAGE perm granted");
+ else {
+ Log.e(TAG, "WR_EXT_STORAGE perm declined, stopping i2pd");
+ i2pdStop();
+ //TODO must work w/o this perm, ask orignal
+ }
+ }
}
private static void cancelGracefulStop() {
@@ -229,7 +233,7 @@ public class I2PDActivity extends Activity {
}
@Override
- public boolean onOptionsItemSelected(MenuItem item) {
+ public boolean onOptionsItemSelected(@NonNull 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.
@@ -258,19 +262,15 @@ public class I2PDActivity extends Activity {
private void i2pdStop() {
cancelGracefulStop();
- new Thread(new Runnable(){
-
- @Override
- public void run() {
- Log.d(TAG, "stopping");
- try{
- daemon.stopDaemon();
- }catch (Throwable tr) {
- Log.e(TAG, "", tr);
- }
- }
-
- },"stop").start();
+ new Thread(() -> {
+ Log.d(TAG, "stopping");
+ try {
+ daemon.stopDaemon();
+ } catch (Throwable tr) {
+ Log.e(TAG, "", tr);
+ }
+ quit(); //TODO make menu items for starting i2pd. On my Android, I need to reboot the OS to restart i2pd.
+ },"stop").start();
}
private static volatile Timer gracefulQuitTimer;
@@ -288,55 +288,44 @@ public class I2PDActivity extends Activity {
}
Toast.makeText(this, R.string.graceful_stop_is_in_progress,
Toast.LENGTH_SHORT).show();
- new Thread(new Runnable(){
-
- @Override
- public void run() {
- try {
- Log.d(TAG, "grac stopping");
- if(daemon.isStartedOkay()) {
- daemon.stopAcceptingTunnels();
- long gracefulStopAtMillis;
- synchronized (graceStartedMillis_LOCK) {
- graceStartedMillis = System.currentTimeMillis();
- gracefulStopAtMillis = graceStartedMillis + GRACEFUL_DELAY_MILLIS;
- }
- rescheduleGraceStop(null,gracefulStopAtMillis);
- } else {
- i2pdStop();
- }
- } catch(Throwable tr) {
- Log.e(TAG,"",tr);
- }
- }
-
- },"gracInit").start();
+ new Thread(() -> {
+ try {
+ Log.d(TAG, "grac stopping");
+ if(daemon.isStartedOkay()) {
+ daemon.stopAcceptingTunnels();
+ long gracefulStopAtMillis;
+ synchronized (graceStartedMillis_LOCK) {
+ graceStartedMillis = System.currentTimeMillis();
+ gracefulStopAtMillis = graceStartedMillis + GRACEFUL_DELAY_MILLIS;
+ }
+ rescheduleGraceStop(null,gracefulStopAtMillis);
+ } else {
+ i2pdStop();
+ }
+ } catch(Throwable tr) {
+ Log.e(TAG,"",tr);
+ }
+ },"gracInit").start();
}
private void i2pdCancelGracefulStop()
{
cancelGracefulStop();
Toast.makeText(this, R.string.startedOkay, Toast.LENGTH_SHORT).show();
- new Thread(new Runnable()
- {
- @Override
- public void run()
- {
- try
- {
- Log.d(TAG, "grac stopping cancel");
- if(daemon.isStartedOkay())
- daemon.startAcceptingTunnels();
- else
- i2pdStop();
- }
- catch(Throwable tr)
- {
- Log.e(TAG,"",tr);
- }
- }
-
- },"gracCancel").start();
+ new Thread(() -> {
+ try
+ {
+ Log.d(TAG, "grac stopping cancel");
+ if(daemon.isStartedOkay())
+ daemon.startAcceptingTunnels();
+ else
+ i2pdStop();
+ }
+ catch(Throwable tr)
+ {
+ Log.e(TAG,"",tr);
+ }
+ },"gracCancel").start();
}
private void rescheduleGraceStop(Timer gracefulQuitTimerOld, long gracefulStopAtMillis) {
@@ -393,7 +382,7 @@ public class I2PDActivity extends Activity {
// Make the directory.
File dir = new File(i2pdpath, path);
- dir.mkdirs();
+ Log.d(TAG, "dir.mkdirs() returned "+dir.mkdirs());
// Recurse on the contents.
for (String entry : contents) {
@@ -431,45 +420,69 @@ public class I2PDActivity extends Activity {
private void deleteRecursive(File fileOrDirectory) {
if (fileOrDirectory.isDirectory()) {
- for (File child : fileOrDirectory.listFiles()) {
- deleteRecursive(child);
+ File[] files = fileOrDirectory.listFiles();
+ if(files!=null) {
+ for (File child : files) {
+ deleteRecursive(child);
+ }
}
}
- fileOrDirectory.delete();
+ boolean deleteResult = fileOrDirectory.delete();
+ if(!deleteResult)Log.e(TAG, "fileOrDirectory.delete() returned "+deleteResult+", absolute path='"+fileOrDirectory.getAbsolutePath()+"'");
}
private void processAssets() {
if (!assetsCopied) try {
assetsCopied = true; // prevent from running on every state update
- File holderfile = new File(i2pdpath, "assets.ready");
+ File holderFile = new File(i2pdpath, "assets.ready");
String versionName = BuildConfig.VERSION_NAME; // here will be app version, like 2.XX.XX
StringBuilder text = new StringBuilder();
- if (holderfile.exists()) try { // if holder file exists, read assets version string
- BufferedReader br = new BufferedReader(new FileReader(holderfile));
- String line;
+ if (holderFile.exists()) {
+ try { // if holder file exists, read assets version string
+ FileReader fileReader = new FileReader(holderFile);
- while ((line = br.readLine()) != null) {
- text.append(line);
- }
- br.close();
- }
- catch (IOException e) {
- Log.e(TAG, "", e);
- }
+ try {
+ BufferedReader br = new BufferedReader(fileReader);
+
+ try {
+ String line;
+
+ while ((line = br.readLine()) != null) {
+ text.append(line);
+ }
+ }finally {
+ try{
+ br.close();
+ } catch (IOException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+ } finally {
+ try{
+ fileReader.close();
+ } catch (IOException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "", e);
+ }
+ }
// if version differs from current app version or null, try to delete certificates folder
if (!text.toString().contains(versionName)) try {
- holderfile.delete();
- File certpath = new File(i2pdpath, "certificates");
- deleteRecursive(certpath);
+ boolean deleteResult = holderFile.delete();
+ if(!deleteResult)Log.e(TAG, "holderFile.delete() returned "+deleteResult+", absolute path='"+holderFile.getAbsolutePath()+"'");
+ File certPath = new File(i2pdpath, "certificates");
+ deleteRecursive(certPath);
}
catch (Throwable tr) {
Log.e(TAG, "", tr);
}
- // copy assets. If processed file exists, it won't be overwrited
+ // copy assets. If processed file exists, it won't be overwritten
copyAsset("addressbook");
copyAsset("certificates");
copyAsset("tunnels.d");
@@ -478,14 +491,95 @@ public class I2PDActivity extends Activity {
copyAsset("tunnels.conf");
// update holder file about successful copying
- FileWriter writer = new FileWriter(holderfile);
- writer.append(versionName);
- writer.flush();
- writer.close();
+ FileWriter writer = new FileWriter(holderFile);
+ try {
+ writer.append(versionName);
+ } finally {
+ try{
+ writer.close();
+ }catch (IOException e){
+ Log.e(TAG,"on writer close", e);
+ }
+ }
}
catch (Throwable tr)
{
- Log.e(TAG,"copy assets",tr);
+ Log.e(TAG,"on assets copying", tr);
}
}
+
+ @SuppressLint("BatteryLife")
+ private void openBatteryOptimizationDialogIfNeeded() {
+ boolean questionEnabled = getPreferences().getBoolean(getBatteryOptimizationPreferenceKey(), true);
+ Log.i(TAG,"BATT_OPTIM_questionEnabled=="+questionEnabled);
+ if (!isKnownIgnoringBatteryOptimizations()
+ && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M
+ && questionEnabled) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.battery_optimizations_enabled);
+ builder.setMessage(R.string.battery_optimizations_enabled_dialog);
+ builder.setPositiveButton(R.string.next, (dialog, which) -> {
+ try {
+ startActivity(new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, Uri.parse(PACKAGE_URI_SCHEME + getPackageName())));
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG,"BATT_OPTIM_ActvtNotFound", e);
+ Toast.makeText(this, R.string.device_does_not_support_disabling_battery_optimizations, Toast.LENGTH_SHORT).show();
+ }
+ });
+ builder.setOnDismissListener(dialog -> setNeverAskForBatteryOptimizationsAgain());
+ final AlertDialog dialog = builder.create();
+ dialog.setCanceledOnTouchOutside(false);
+ dialog.show();
+ }
+ }
+
+ private void setNeverAskForBatteryOptimizationsAgain() {
+ getPreferences().edit().putBoolean(getBatteryOptimizationPreferenceKey(), false).apply();
+ }
+
+ protected boolean isKnownIgnoringBatteryOptimizations() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ final PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
+ if (pm == null) {
+ Log.i(TAG, "BATT_OPTIM: POWER_SERVICE==null");
+ return false;
+ }
+ boolean ignoring = pm.isIgnoringBatteryOptimizations(getPackageName());
+ Log.i(TAG, "BATT_OPTIM: ignoring==" + ignoring);
+ return ignoring;
+ } else {
+ Log.i(TAG, "BATT_OPTIM: old sdk version=="+Build.VERSION.SDK_INT);
+ return false;
+ }
+ }
+
+ protected SharedPreferences getPreferences() {
+ return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ }
+
+ private String getBatteryOptimizationPreferenceKey() {
+ @SuppressLint("HardwareIds") String device = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
+ return "show_battery_optimization" + (device == null ? "" : device);
+ }
+
+ private void quit() {
+ try {
+ 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);
+ }
+ try{
+ daemon.stopDaemon();
+ }catch (Throwable tr) {
+ Log.e(TAG, "", tr);
+ }
+ System.exit(0);
+ }
}