various Android stuff. Fixed #1400

pull/1405/head
kote 5 years ago
parent 9bbce5dba6
commit 8f82d563c1

@ -12,5 +12,4 @@ local.properties
build.sh build.sh
android.iml android.iml
build build
*.iml

@ -9,6 +9,7 @@
<uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<application <application
android:allowBackup="true" android:allowBackup="true"

@ -5,7 +5,7 @@ buildscript {
google() google()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.3.2' classpath 'com.android.tools.build:gradle:3.4.2'
} }
} }
@ -16,18 +16,19 @@ repositories {
maven { maven {
url 'https://maven.google.com' url 'https://maven.google.com'
} }
google()
} }
dependencies { dependencies {
implementation 'com.android.support:support-compat:28.0.0' implementation 'androidx.core:core:1.0.2'
} }
android { android {
compileSdkVersion 28 compileSdkVersion 29
buildToolsVersion "28.0.3" buildToolsVersion "28.0.3"
defaultConfig { defaultConfig {
applicationId "org.purplei2p.i2pd" applicationId "org.purplei2p.i2pd"
targetSdkVersion 28 targetSdkVersion 29
minSdkVersion 14 minSdkVersion 14
versionCode 2270 versionCode 2270
versionName "2.27.0" versionName "2.27.0"
@ -81,4 +82,8 @@ android {
path './jni/Android.mk' path './jni/Android.mk'
} }
} }
compileOptions {
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
}
} }

@ -1 +1,3 @@
android.enableJetifier=true
android.useAndroidX=true
org.gradle.parallel=true org.gradle.parallel=true

@ -1,6 +1,6 @@
#Thu Mar 14 18:21:08 MSK 2019 #Tue Aug 20 14:39:08 MSK 2019
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip

@ -17,4 +17,9 @@
<string name="remaining">remaining</string> <string name="remaining">remaining</string>
<string name="title_activity_i2_pdperms_asker_prompt">Prompt</string> <string name="title_activity_i2_pdperms_asker_prompt">Prompt</string>
<string name="permDenied">SD card write permission denied, you need to allow this to continue</string> <string name="permDenied">SD card write permission denied, you need to allow this to continue</string>
<string name="battery_optimizations_enabled">Battery optimizations enabled</string>
<string name="battery_optimizations_enabled_explained">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.</string>
<string name="battery_optimizations_enabled_dialog">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.</string>
<string name="next">Next</string>
<string name="device_does_not_support_disabling_battery_optimizations">Your device does not support opting out of battery optimizations</string>
</resources> </resources>

@ -1,6 +1,5 @@
package org.purplei2p.i2pd; package org.purplei2p.i2pd;
import android.annotation.TargetApi;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationChannel; import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
@ -11,10 +10,9 @@ import android.content.Intent;
import android.os.Binder; import android.os.Binder;
import android.os.Build; import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.support.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import android.support.v4.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import android.util.Log; import android.util.Log;
import android.widget.Toast;
public class ForegroundService extends Service { public class ForegroundService extends Service {
private static final String TAG="FgService"; private static final String TAG="FgService";
@ -112,14 +110,15 @@ public class ForegroundService extends Service {
// If earlier version channel ID is not used // 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) // 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. // 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) .setOngoing(true)
.setSmallIcon(R.drawable.itoopie_notification_icon) // the status icon .setSmallIcon(R.drawable.itoopie_notification_icon); // the status icon
.setPriority(Notification.PRIORITY_DEFAULT) if(Build.VERSION.SDK_INT >= 16) builder = builder.setPriority(Notification.PRIORITY_DEFAULT);
.setCategory(Notification.CATEGORY_SERVICE) if(Build.VERSION.SDK_INT >= 21) builder = builder.setCategory(Notification.CATEGORY_SERVICE);
Notification notification = builder
.setTicker(text) // the status text .setTicker(text) // the status text
.setWhen(System.currentTimeMillis()) // the time stamp .setWhen(System.currentTimeMillis()) // the time stamp
.setContentTitle(getText(R.string.app_name)) // the label of the entry .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.setLightColor(Color.PURPLE);
chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
NotificationManager service = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); 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; return channelId;
} }
private static final DaemonSingleton daemon = DaemonSingleton.getInstance(); private static final DaemonSingleton daemon = DaemonSingleton.getInstance();
} }

@ -14,24 +14,34 @@ import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
import android.Manifest; import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.res.AssetManager; import android.content.res.AssetManager;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Build; import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.os.IBinder; import android.os.IBinder;
import android.os.PowerManager;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.util.Log; import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; 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 // For future package update checking
import org.purplei2p.i2pd.BuildConfig; import org.purplei2p.i2pd.BuildConfig;
@ -40,6 +50,7 @@ public class I2PDActivity extends Activity {
private static final String TAG = "i2pdActvt"; private static final String TAG = "i2pdActvt";
private static final int MY_PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE = 1; private static final int MY_PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE = 1;
public static final int GRACEFUL_DELAY_MILLIS = 10 * 60 * 1000; public static final int GRACEFUL_DELAY_MILLIS = 10 * 60 * 1000;
public static final String PACKAGE_URI_SCHEME = "package:";
private TextView textView; private TextView textView;
private boolean assetsCopied; private boolean assetsCopied;
@ -53,28 +64,22 @@ public class I2PDActivity extends Activity {
public void daemonStateUpdate() public void daemonStateUpdate()
{ {
processAssets(); processAssets();
runOnUiThread(new Runnable(){ runOnUiThread(() -> {
try {
@Override if(textView==null) return;
public void run() { Throwable tr = daemon.getLastThrowable();
try { if(tr!=null) {
if(textView==null) return; textView.setText(throwableToString(tr));
Throwable tr = daemon.getLastThrowable(); return;
if(tr!=null) { }
textView.setText(throwableToString(tr)); DaemonSingleton.State state = daemon.getState();
return; 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)) : "";
DaemonSingleton.State state = daemon.getState(); textView.setText(String.format("%s%s%s", getText(state.getStatusStringResourceId()), startResultStr, graceStr));
textView.setText( } catch (Throwable tr) {
String.valueOf(getText(state.getStatusStringResourceId()))+ Log.e(TAG,"error ignored",tr);
(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);
}
}
});
} }
}; };
private static volatile long graceStartedMillis; private static volatile long graceStartedMillis;
@ -92,6 +97,7 @@ public class I2PDActivity extends Activity {
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "onCreate");
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
textView = new TextView(this); textView = new TextView(this);
@ -121,6 +127,8 @@ public class I2PDActivity extends Activity {
} }
rescheduleGraceStop(gracefulQuitTimer, gracefulStopAtMillis); rescheduleGraceStop(gracefulQuitTimer, gracefulStopAtMillis);
} }
openBatteryOptimizationDialogIfNeeded();
} }
@Override @Override
@ -137,21 +145,17 @@ public class I2PDActivity extends Activity {
} }
@Override @Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)
{ {
switch (requestCode) if (requestCode == MY_PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE) {
{ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
case MY_PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE: Log.e(TAG, "WR_EXT_STORAGE perm granted");
{ else {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) Log.e(TAG, "WR_EXT_STORAGE perm declined, stopping i2pd");
Log.e(TAG, "Memory permission granted"); i2pdStop();
else //TODO must work w/o this perm, ask orignal
Log.e(TAG, "Memory permission declined"); }
// TODO: terminate }
return;
}
default: ;
}
} }
private static void cancelGracefulStop() { private static void cancelGracefulStop() {
@ -229,7 +233,7 @@ public class I2PDActivity extends Activity {
} }
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(@NonNull MenuItem item) {
// Handle action bar item clicks here. The action bar will // Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long // automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml. // as you specify a parent activity in AndroidManifest.xml.
@ -258,19 +262,15 @@ public class I2PDActivity extends Activity {
private void i2pdStop() { private void i2pdStop() {
cancelGracefulStop(); cancelGracefulStop();
new Thread(new Runnable(){ new Thread(() -> {
Log.d(TAG, "stopping");
@Override try {
public void run() { daemon.stopDaemon();
Log.d(TAG, "stopping"); } catch (Throwable tr) {
try{ Log.e(TAG, "", tr);
daemon.stopDaemon(); }
}catch (Throwable tr) { quit(); //TODO make menu items for starting i2pd. On my Android, I need to reboot the OS to restart i2pd.
Log.e(TAG, "", tr); },"stop").start();
}
}
},"stop").start();
} }
private static volatile Timer gracefulQuitTimer; 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.makeText(this, R.string.graceful_stop_is_in_progress,
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
new Thread(new Runnable(){ new Thread(() -> {
try {
@Override Log.d(TAG, "grac stopping");
public void run() { if(daemon.isStartedOkay()) {
try { daemon.stopAcceptingTunnels();
Log.d(TAG, "grac stopping"); long gracefulStopAtMillis;
if(daemon.isStartedOkay()) { synchronized (graceStartedMillis_LOCK) {
daemon.stopAcceptingTunnels(); graceStartedMillis = System.currentTimeMillis();
long gracefulStopAtMillis; gracefulStopAtMillis = graceStartedMillis + GRACEFUL_DELAY_MILLIS;
synchronized (graceStartedMillis_LOCK) { }
graceStartedMillis = System.currentTimeMillis(); rescheduleGraceStop(null,gracefulStopAtMillis);
gracefulStopAtMillis = graceStartedMillis + GRACEFUL_DELAY_MILLIS; } else {
} i2pdStop();
rescheduleGraceStop(null,gracefulStopAtMillis); }
} else { } catch(Throwable tr) {
i2pdStop(); Log.e(TAG,"",tr);
} }
} catch(Throwable tr) { },"gracInit").start();
Log.e(TAG,"",tr);
}
}
},"gracInit").start();
} }
private void i2pdCancelGracefulStop() private void i2pdCancelGracefulStop()
{ {
cancelGracefulStop(); cancelGracefulStop();
Toast.makeText(this, R.string.startedOkay, Toast.LENGTH_SHORT).show(); Toast.makeText(this, R.string.startedOkay, Toast.LENGTH_SHORT).show();
new Thread(new Runnable() new Thread(() -> {
{ try
@Override {
public void run() Log.d(TAG, "grac stopping cancel");
{ if(daemon.isStartedOkay())
try daemon.startAcceptingTunnels();
{ else
Log.d(TAG, "grac stopping cancel"); i2pdStop();
if(daemon.isStartedOkay()) }
daemon.startAcceptingTunnels(); catch(Throwable tr)
else {
i2pdStop(); Log.e(TAG,"",tr);
} }
catch(Throwable tr) },"gracCancel").start();
{
Log.e(TAG,"",tr);
}
}
},"gracCancel").start();
} }
private void rescheduleGraceStop(Timer gracefulQuitTimerOld, long gracefulStopAtMillis) { private void rescheduleGraceStop(Timer gracefulQuitTimerOld, long gracefulStopAtMillis) {
@ -393,7 +382,7 @@ public class I2PDActivity extends Activity {
// Make the directory. // Make the directory.
File dir = new File(i2pdpath, path); File dir = new File(i2pdpath, path);
dir.mkdirs(); Log.d(TAG, "dir.mkdirs() returned "+dir.mkdirs());
// Recurse on the contents. // Recurse on the contents.
for (String entry : contents) { for (String entry : contents) {
@ -431,45 +420,69 @@ public class I2PDActivity extends Activity {
private void deleteRecursive(File fileOrDirectory) { private void deleteRecursive(File fileOrDirectory) {
if (fileOrDirectory.isDirectory()) { if (fileOrDirectory.isDirectory()) {
for (File child : fileOrDirectory.listFiles()) { File[] files = fileOrDirectory.listFiles();
deleteRecursive(child); 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() { private void processAssets() {
if (!assetsCopied) try { if (!assetsCopied) try {
assetsCopied = true; // prevent from running on every state update 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 String versionName = BuildConfig.VERSION_NAME; // here will be app version, like 2.XX.XX
StringBuilder text = new StringBuilder(); StringBuilder text = new StringBuilder();
if (holderfile.exists()) try { // if holder file exists, read assets version string if (holderFile.exists()) {
BufferedReader br = new BufferedReader(new FileReader(holderfile)); try { // if holder file exists, read assets version string
String line; FileReader fileReader = new FileReader(holderFile);
while ((line = br.readLine()) != null) { try {
text.append(line); BufferedReader br = new BufferedReader(fileReader);
}
br.close(); try {
} String line;
catch (IOException e) {
Log.e(TAG, "", e); 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 version differs from current app version or null, try to delete certificates folder
if (!text.toString().contains(versionName)) try { if (!text.toString().contains(versionName)) try {
holderfile.delete(); boolean deleteResult = holderFile.delete();
File certpath = new File(i2pdpath, "certificates"); if(!deleteResult)Log.e(TAG, "holderFile.delete() returned "+deleteResult+", absolute path='"+holderFile.getAbsolutePath()+"'");
deleteRecursive(certpath); File certPath = new File(i2pdpath, "certificates");
deleteRecursive(certPath);
} }
catch (Throwable tr) { catch (Throwable tr) {
Log.e(TAG, "", 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("addressbook");
copyAsset("certificates"); copyAsset("certificates");
copyAsset("tunnels.d"); copyAsset("tunnels.d");
@ -478,14 +491,95 @@ public class I2PDActivity extends Activity {
copyAsset("tunnels.conf"); copyAsset("tunnels.conf");
// update holder file about successful copying // update holder file about successful copying
FileWriter writer = new FileWriter(holderfile); FileWriter writer = new FileWriter(holderFile);
writer.append(versionName); try {
writer.flush(); writer.append(versionName);
writer.close(); } finally {
try{
writer.close();
}catch (IOException e){
Log.e(TAG,"on writer close", e);
}
}
} }
catch (Throwable tr) 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);
} }
} }

Loading…
Cancel
Save