Browse Source

JobService app run time longevity experiment

pull/44/head
nonlin-lin-chaos-order-etc-etal 2 years ago
parent
commit
c849eab791
  1. 2
      .gitignore
  2. 14
      app/src/main/AndroidManifest.xml
  3. 15
      app/src/main/java/org/purplei2p/i2pd/DaemonWrapper.java
  4. 107
      app/src/main/java/org/purplei2p/i2pd/I2PDActivity.java
  5. 64
      app/src/main/java/org/purplei2p/i2pd/MyJobService.java
  6. 2
      app/src/main/java/org/purplei2p/i2pd/NetworkStateChangeReceiver.java
  7. 2
      app/src/main/java/org/purplei2p/i2pd/receivers/BootUpReceiver.java
  8. 50
      app/src/main/java/org/purplei2p/i2pd/receivers/MakeMeAJobReceiver.java

2
.gitignore vendored

@ -9,7 +9,7 @@ obj @@ -9,7 +9,7 @@ obj
.idea
.externalNativeBuild
ant.properties
local.properties
local*
build.sh
android.iml
build

14
app/src/main/AndroidManifest.xml

@ -29,6 +29,15 @@ @@ -29,6 +29,15 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<receiver
android:enabled="true"
android:name="org.purplei2p.i2pd.receivers.MakeMeAJobReceiver"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE">
<intent-filter>
<action android:name="org.purplei2p.i2pd.MAKEMEAJOB_ACTION" />
</intent-filter>
</receiver>
<meta-data
android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" />
@ -59,6 +68,11 @@ @@ -59,6 +68,11 @@
android:name=".ForegroundService"
android:enabled="true" />
<service
android:name=".MyJobService"
android:enabled="true"
android:permission="android.permission.BIND_JOB_SERVICE"/>
<activity
android:name=".I2PDPermsExplanationActivity"
android:label="@string/title_activity_i2_pdperms_asker_prompt"

15
app/src/main/java/org/purplei2p/i2pd/DaemonWrapper.java

@ -30,9 +30,9 @@ public class DaemonWrapper { @@ -30,9 +30,9 @@ public class DaemonWrapper {
private final AssetManager assetManager;
private final ConnectivityManager connectivityManager;
private String i2pdpath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/i2pd";
private boolean assetsCopied;
private static final String appLocale = Locale.getDefault().getDisplayLanguage(Locale.ENGLISH).toLowerCase(); // lower-case system language (like "english")
/** lower-case system language at app start time (like "english") */
private static final String appLocale = Locale.getDefault().getDisplayLanguage(Locale.ENGLISH).toLowerCase();
public interface StateUpdateListener {
void daemonStateUpdate(State oldValue, State newValue);
@ -107,7 +107,10 @@ public class DaemonWrapper { @@ -107,7 +107,10 @@ public class DaemonWrapper {
}
public boolean isStartedOkay() {
return equals(State.startedOkay) || equals(State.gracefulShutdownInProgress);
return this == startedOkay || this == gracefulShutdownInProgress;
}
public boolean needsToBeAlive() {
return this == uninitialized || this == starting || this == startedOkay || this == gracefulShutdownInProgress;
}
}
@ -150,6 +153,7 @@ public class DaemonWrapper { @@ -150,6 +153,7 @@ public class DaemonWrapper {
return I2PD_JNI.getDataDir();
}
// TODO unused func
public void changeDataDir(String dataDir, Boolean updateAssets) {
I2PD_JNI.setDataDir(dataDir);
if (updateAssets) processAssets();
@ -209,9 +213,8 @@ public class DaemonWrapper { @@ -209,9 +213,8 @@ public class DaemonWrapper {
Log.d(TAG, "checking assets");
if (holderFile.exists()) {
try { // if holder file exists, read assets version string
FileReader fileReader = new FileReader(holderFile);
// if holder file exists, read assets version string
try (FileReader fileReader = new FileReader(holderFile)) {
try {
BufferedReader br = new BufferedReader(fileReader);

107
app/src/main/java/org/purplei2p/i2pd/I2PDActivity.java

@ -13,6 +13,7 @@ import android.content.ActivityNotFoundException; @@ -13,6 +13,7 @@ import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
@ -37,12 +38,17 @@ import androidx.core.content.ContextCompat; @@ -37,12 +38,17 @@ import androidx.core.content.ContextCompat;
import static android.provider.Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS;
import org.purplei2p.i2pd.receivers.MakeMeAJobReceiver;
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 final MakeMeAJobReceiver jobReceiver = isJobServiceApiAvailable() ?
new MakeMeAJobReceiver() : null;
private TextView textView;
private CheckBox HTTPProxyState;
private CheckBox SOCKSProxyState;
@ -50,15 +56,11 @@ public class I2PDActivity extends Activity { @@ -50,15 +56,11 @@ public class I2PDActivity extends Activity {
private CheckBox SAMState;
private CheckBox I2CPState;
private static volatile DaemonWrapper daemon;
private final DaemonWrapper.StateUpdateListener daemonStateUpdatedListener = new DaemonWrapper.StateUpdateListener() {
@Override
public void daemonStateUpdate(DaemonWrapper.State oldValue, DaemonWrapper.State newValue) {
updateStatusText();
}
};
public static DaemonWrapper getDaemon() { return daemon; }
private final DaemonWrapper.StateUpdateListener daemonStateUpdatedListener = (oldValue, newValue) -> updateStatusText();
private void updateStatusText() {
runOnUiThread(() -> {
@ -86,7 +88,7 @@ public class I2PDActivity extends Activity { @@ -86,7 +88,7 @@ public class I2PDActivity extends Activity {
String graceStr = DaemonWrapper.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);
Log.e(TAG,"exc",tr);
}
});
}
@ -135,7 +137,32 @@ public class I2PDActivity extends Activity { @@ -135,7 +137,32 @@ public class I2PDActivity extends Activity {
}
}
doBindService();
doBindServices();
if(isJobServiceApiAvailable()) {
IntentFilter filter = new IntentFilter();
filter.addAction(MakeMeAJobReceiver.MAKEMEAJOB_ACTION);
registerReceiver(jobReceiver, filter);
new Thread(() -> {
try {
while (getDaemon().getState().needsToBeAlive()) {
runOnUiThread(() -> {
try {
sendBroadcast(new Intent(MakeMeAJobReceiver.MAKEMEAJOB_ACTION));
} catch (Throwable tr) {
Log.e(TAG, "", tr);
}
});
Thread.sleep(60*1000);
}
} catch(InterruptedException ex) {
Log.d(TAG, "JOB_ACTION pinger thread interrupted");
} catch(Throwable tr) {
Log.e(TAG, "", tr);
}
}, "JOB_ACTION pinger").start();
}
final Timer gracefulQuitTimer = getGracefulQuitTimer();
if (gracefulQuitTimer != null) {
@ -157,15 +184,18 @@ public class I2PDActivity extends Activity { @@ -157,15 +184,18 @@ public class I2PDActivity extends Activity {
daemon.removeStateChangeListener(daemonStateUpdatedListener);
//cancelGracefulStop0();
try {
doUnbindService();
doUnbindServices();
} catch (IllegalArgumentException ex) {
Log.e(TAG, "throwable caught and ignored", ex);
if (ex.getMessage().startsWith("Service not registered: " + org.purplei2p.i2pd.I2PDActivity.class.getName())) {
if (ex.getMessage().startsWith("Service not registered: " + I2PDActivity.class.getName())) {
Log.i(TAG, "Service not registered exception seems to be normal, not a bug it seems.");
}
} catch (Throwable tr) {
Log.e(TAG, "throwable caught and ignored", tr);
}
if (isJobServiceApiAvailable()) {
unregisterReceiver(jobReceiver);
}
}
@Override
@ -199,7 +229,7 @@ public class I2PDActivity extends Activity { @@ -199,7 +229,7 @@ public class I2PDActivity extends Activity {
// private LocalService mBoundService;
private ServiceConnection mConnection = new ServiceConnection() {
private ServiceConnection foregroundServiceConnection = 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
@ -224,27 +254,46 @@ public class I2PDActivity extends Activity { @@ -224,27 +254,46 @@ public class I2PDActivity extends Activity {
}
};
private static volatile boolean mIsBound;
private ServiceConnection jobServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {}
public void onServiceDisconnected(ComponentName className) {}
};
private static volatile boolean foregroundServiceBound;
private static volatile boolean jobServiceBound;
private void doBindService() {
private void doBindServices() {
synchronized (I2PDActivity.class) {
if (!foregroundServiceBound) {
// 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), foregroundServiceConnection, Context.BIND_AUTO_CREATE);
foregroundServiceBound = true;
}
}
synchronized (I2PDActivity.class) {
if (mIsBound)
return;
// 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;
if (!jobServiceBound && org.purplei2p.i2pd.I2PDActivity.isJobServiceApiAvailable()) {
bindService(new Intent(this, MyJobService.class), jobServiceConnection, Context.BIND_AUTO_CREATE);
jobServiceBound = true;
}
}
}
private void doUnbindService() {
private void doUnbindServices() {
synchronized (I2PDActivity.class) {
if (mIsBound) {
if (foregroundServiceBound) {
// Detach our existing connection.
unbindService(mConnection);
mIsBound = false;
unbindService(foregroundServiceConnection);
foregroundServiceBound = false;
}
}
synchronized (I2PDActivity.class) {
if (jobServiceBound) {
// Detach our existing connection.
unbindService(jobServiceConnection);
jobServiceBound = false;
}
}
}
@ -258,10 +307,14 @@ public class I2PDActivity extends Activity { @@ -258,10 +307,14 @@ public class I2PDActivity extends Activity {
return true;
}
private boolean isBatteryOptimizationsOpenOsDialogApiAvailable() {
private static boolean isBatteryOptimizationsOpenOsDialogApiAvailable() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
public static boolean isJobServiceApiAvailable() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
// Handle action bar item clicks here. The action bar will

64
app/src/main/java/org/purplei2p/i2pd/MyJobService.java

@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
package org.purplei2p.i2pd;
import android.app.job.JobParameters;
import android.app.job.JobService;
import android.os.Build;
import android.util.Log;
import androidx.annotation.RequiresApi;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class MyJobService extends JobService {
private static final String TAG = "i2pdJobSrvc";
private final DaemonWrapper.StateUpdateListener listener = new DaemonWrapper.StateUpdateListener() {
@Override
public void daemonStateUpdate(DaemonWrapper.State oldValue, DaemonWrapper.State newValue) {
if(!newValue.needsToBeAlive()) {
/*synchronized (MyJobService.this) {
MyJobService.this.notifyAll(); //wakeup
}*/
removeDaemonStateChangeListener();
jobFinished(null, false);
}
}
};
@Override
public boolean onStartJob(JobParameters params) {
Log.d(TAG,"onStartJob entered");
I2PDActivity.getDaemon().addStateChangeListener(listener);
/*
"So, the system may kill the process at any time to reclaim memory, and in doing so,
it terminates the spawned thread running in the process. The solution to this problem is
typically to schedule a JobService from the BroadcastReceiver, so the system knows
that there is still active work being done in the process."
Source for the quote: https://developer.android.com/guide/components/activities/process-lifecycle
*/
/*
while(I2PDActivity.getDaemon().getState().needsToBeAlive()){
synchronized (MyJobService.this) {
try {
MyJobService.this.wait(); // occurs in main GUI thread when not commented out
} catch (InterruptedException e) {
Log.e(TAG,"onStartJob interrupted");
}
}
}
*/
return true;
}
@Override
public boolean onStopJob(JobParameters params) {
Log.d(TAG,"onStopJob entered");
removeDaemonStateChangeListener();
return true;
}
private void removeDaemonStateChangeListener() {
I2PDActivity.getDaemon().removeStateChangeListener(listener);
}
}

2
app/src/main/java/org/purplei2p/i2pd/NetworkStateChangeReceiver.java

@ -14,7 +14,7 @@ public class NetworkStateChangeReceiver extends BroadcastReceiver { @@ -14,7 +14,7 @@ public class NetworkStateChangeReceiver extends BroadcastReceiver {
public void onReceive(final Context context, final Intent intent) {
Log.d(TAG, "Network state change: onReceive entered");
try {
/*
/* TODO
Warning: This broadcast receiver declares an intent-filter for a protected broadcast action string,
which can only be sent by the system, not third-party applications. However, the receiver's `onReceive`
method does not appear to call `getAction` to ensure that the received Intent's action string matches

2
app/src/main/java/org/purplei2p/i2pd/receivers/BootUpReceiver.java

@ -18,7 +18,7 @@ public class BootUpReceiver extends BroadcastReceiver { @@ -18,7 +18,7 @@ public class BootUpReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
/* todo: disable the autostart? */
/*
/* TODO
Warning: This broadcast receiver declares an intent-filter for a protected broadcast action string,
which can only be sent by the system, not third-party applications. However, the receiver's `onReceive`
method does not appear to call `getAction` to ensure that the received Intent's action string matches

50
app/src/main/java/org/purplei2p/i2pd/receivers/MakeMeAJobReceiver.java

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
package org.purplei2p.i2pd.receivers;
import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import org.purplei2p.i2pd.I2PDActivity;
import org.purplei2p.i2pd.MyJobService;
public class MakeMeAJobReceiver extends BroadcastReceiver {
private static final String TAG = "i2pdMMJR";
public static final String MAKEMEAJOB_ACTION = "org.purplei2p.i2pd.MAKEMEAJOB_ACTION";
private static final int JOB_ID = 0;
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "MakeMeAJobReceiver.onReceive entered: actionString=='" + intent.getAction() + "'");
if(!MAKEMEAJOB_ACTION.equals(intent.getAction())){
Log.d(TAG, "MakeMeAJobReceiver exiting, got unknown action");
return;
}
scheduleJob(context);
Log.d(TAG, "MakeMeAJobReceiver: MyJobService job scheduled");
}
private static void scheduleJob(Context context) {
if(!I2PDActivity.isJobServiceApiAvailable()) {
Log.d(TAG, "MakeMeAJobReceiver JobService api not available, need OS API LEVEL >= 21'");
return;
}
JobScheduler jobScheduler = (JobScheduler)context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
ComponentName serviceComponent = new ComponentName(context, MyJobService.class);
JobInfo.Builder builder = new JobInfo.Builder(JOB_ID, serviceComponent);
// a workaround for "You're trying to build a job
// with no constraints, this is not allowed." exception
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
builder.setPersisted(true);
// ping it every minute, otherwise on my physical device API >= 21 it stops after 10 minutes
//builder.setPeriodic(60*1000);
jobScheduler.schedule(builder.build());
}
}
Loading…
Cancel
Save