SuperSu related changes.

This commit is contained in:
jens 2021-05-18 20:02:45 +02:00
parent a0ff8c80f0
commit 1560fd3343
9 changed files with 2858 additions and 614 deletions

View File

@ -510,8 +510,9 @@ public class ActivityPermissions extends Activity
case setAirplaneMode: case setAirplaneMode:
addToArrayListUnique(Manifest.permission.WRITE_SETTINGS, requiredPermissions); addToArrayListUnique(Manifest.permission.WRITE_SETTINGS, requiredPermissions);
addToArrayListUnique(Manifest.permission.ACCESS_NETWORK_STATE, requiredPermissions); addToArrayListUnique(Manifest.permission.ACCESS_NETWORK_STATE, requiredPermissions);
addToArrayListUnique(permissionNameSuperuser, requiredPermissions);
addToArrayListUnique(Manifest.permission.CHANGE_NETWORK_STATE, requiredPermissions); addToArrayListUnique(Manifest.permission.CHANGE_NETWORK_STATE, requiredPermissions);
/* Permission was not required anymore, even before Android 6: https://su.chainfire.eu/#updates-permission
addToArrayListUnique(permissionNameSuperuser, requiredPermissions);*/
break; break;
case setBluetooth: case setBluetooth:
addToArrayListUnique(Manifest.permission.BLUETOOTH_ADMIN, requiredPermissions); addToArrayListUnique(Manifest.permission.BLUETOOTH_ADMIN, requiredPermissions);
@ -522,8 +523,9 @@ public class ActivityPermissions extends Activity
case setDataConnection: case setDataConnection:
addToArrayListUnique(Manifest.permission.WRITE_SETTINGS, requiredPermissions); addToArrayListUnique(Manifest.permission.WRITE_SETTINGS, requiredPermissions);
addToArrayListUnique(Manifest.permission.ACCESS_NETWORK_STATE, requiredPermissions); addToArrayListUnique(Manifest.permission.ACCESS_NETWORK_STATE, requiredPermissions);
addToArrayListUnique(permissionNameSuperuser, requiredPermissions);
addToArrayListUnique(Manifest.permission.CHANGE_NETWORK_STATE, requiredPermissions); addToArrayListUnique(Manifest.permission.CHANGE_NETWORK_STATE, requiredPermissions);
/* Permission was not required anymore, even before Android 6: https://su.chainfire.eu/#updates-permission
addToArrayListUnique(permissionNameSuperuser, requiredPermissions);*/
break; break;
case setDisplayRotation: case setDisplayRotation:
addToArrayListUnique(Manifest.permission.WRITE_SETTINGS, requiredPermissions); addToArrayListUnique(Manifest.permission.WRITE_SETTINGS, requiredPermissions);
@ -1273,7 +1275,8 @@ public class ActivityPermissions extends Activity
mapActionPermissions.put("sendTextMessage", Manifest.permission.SEND_SMS); mapActionPermissions.put("sendTextMessage", Manifest.permission.SEND_SMS);
mapActionPermissions.put("setAirplaneMode", Manifest.permission.WRITE_SETTINGS); mapActionPermissions.put("setAirplaneMode", Manifest.permission.WRITE_SETTINGS);
mapActionPermissions.put("setAirplaneMode", Manifest.permission.ACCESS_NETWORK_STATE); mapActionPermissions.put("setAirplaneMode", Manifest.permission.ACCESS_NETWORK_STATE);
mapActionPermissions.put("setAirplaneMode", permissionNameSuperuser); /* Permission was not required anymore, even before Android 6: https://su.chainfire.eu/#updates-permission
mapActionPermissions.put("setAirplaneMode", permissionNameSuperuser);*/
mapActionPermissions.put("setAirplaneMode", Manifest.permission.CHANGE_NETWORK_STATE); mapActionPermissions.put("setAirplaneMode", Manifest.permission.CHANGE_NETWORK_STATE);
mapActionPermissions.put("setBluetooth", Manifest.permission.BLUETOOTH_ADMIN); mapActionPermissions.put("setBluetooth", Manifest.permission.BLUETOOTH_ADMIN);
mapActionPermissions.put("setBluetooth", Manifest.permission.BLUETOOTH); mapActionPermissions.put("setBluetooth", Manifest.permission.BLUETOOTH);
@ -1281,7 +1284,8 @@ public class ActivityPermissions extends Activity
mapActionPermissions.put("setBluetooth", Manifest.permission.WRITE_SETTINGS); mapActionPermissions.put("setBluetooth", Manifest.permission.WRITE_SETTINGS);
mapActionPermissions.put("setDataConnection", Manifest.permission.WRITE_SETTINGS); mapActionPermissions.put("setDataConnection", Manifest.permission.WRITE_SETTINGS);
mapActionPermissions.put("setDataConnection", Manifest.permission.ACCESS_NETWORK_STATE); mapActionPermissions.put("setDataConnection", Manifest.permission.ACCESS_NETWORK_STATE);
mapActionPermissions.put("setDataConnection", permissionNameSuperuser); /* Permission was not required anymore, even before Android 6: https://su.chainfire.eu/#updates-permission
mapActionPermissions.put("setDataConnection", permissionNameSuperuser);*/
mapActionPermissions.put("setDataConnection", Manifest.permission.CHANGE_NETWORK_STATE); mapActionPermissions.put("setDataConnection", Manifest.permission.CHANGE_NETWORK_STATE);
mapActionPermissions.put("setDisplayRotation", Manifest.permission.WRITE_SETTINGS); mapActionPermissions.put("setDisplayRotation", Manifest.permission.WRITE_SETTINGS);
mapActionPermissions.put("setUsbTethering", Manifest.permission.WRITE_SETTINGS); mapActionPermissions.put("setUsbTethering", Manifest.permission.WRITE_SETTINGS);

View File

@ -10,6 +10,7 @@ import android.net.NetworkInfo;
import android.net.wifi.WifiManager; import android.net.wifi.WifiManager;
import android.util.Log; import android.util.Log;
import com.jens.automation2.AutomationService;
import com.jens.automation2.Miscellaneous; import com.jens.automation2.Miscellaneous;
import com.jens.automation2.PointOfInterest; import com.jens.automation2.PointOfInterest;
import com.jens.automation2.R; import com.jens.automation2.R;
@ -101,7 +102,7 @@ public class WifiBroadcastReceiver extends BroadcastReceiver
Miscellaneous.logEvent("i", "WifiReceiver", context.getResources().getString(R.string.poiHasNoWifiNotStoppingCellLocationListener), 2); Miscellaneous.logEvent("i", "WifiReceiver", context.getResources().getString(R.string.poiHasNoWifiNotStoppingCellLocationListener), 2);
} }
findRules(parentLocationProvider); findRules(AutomationService.getInstance());
} }
else if(myWifi.isConnectedOrConnecting()) // first time connect from wifi-listener-perspective else if(myWifi.isConnectedOrConnecting()) // first time connect from wifi-listener-perspective
{ {
@ -113,7 +114,7 @@ public class WifiBroadcastReceiver extends BroadcastReceiver
String ssid = myWifiManager.getConnectionInfo().getSSID(); String ssid = myWifiManager.getConnectionInfo().getSSID();
setLastWifiSsid(ssid); setLastWifiSsid(ssid);
lastConnectedState = true; lastConnectedState = true;
findRules(parentLocationProvider); findRules(AutomationService.getInstance());
} }
else if(!myWifi.isConnectedOrConnecting()) // really disconnected? because sometimes also fires on connect else if(!myWifi.isConnectedOrConnecting()) // really disconnected? because sometimes also fires on connect
{ {
@ -126,7 +127,7 @@ public class WifiBroadcastReceiver extends BroadcastReceiver
mayCellLocationChangedReceiverBeActivatedFromWifiPointOfWifi = true; mayCellLocationChangedReceiverBeActivatedFromWifiPointOfWifi = true;
CellLocationChangedReceiver.startCellLocationChangedReceiver(); CellLocationChangedReceiver.startCellLocationChangedReceiver();
lastConnectedState = false; lastConnectedState = false;
findRules(parentLocationProvider); findRules(AutomationService.getInstance());
} }
catch(Exception e) catch(Exception e)
{ {
@ -141,13 +142,13 @@ public class WifiBroadcastReceiver extends BroadcastReceiver
} }
} }
public static void findRules(LocationProvider parentLocationProvider) public static void findRules(AutomationService automationServiceInstance)
{ {
ArrayList<Rule> ruleCandidates = Rule.findRuleCandidatesByWifiConnection(); ArrayList<Rule> ruleCandidates = Rule.findRuleCandidatesByWifiConnection();
for(Rule oneRule : ruleCandidates) for(Rule oneRule : ruleCandidates)
{ {
if(oneRule.applies(parentLocationProvider.parentService)) if(oneRule.applies(automationServiceInstance))
oneRule.activate(parentLocationProvider.parentService, false); oneRule.activate(automationServiceInstance, false);
} }
} }

View File

@ -161,7 +161,7 @@ public class ConnectivityReceiver extends BroadcastReceiver implements Automatio
WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
WifiInfo wifiInfo = wifiManager.getConnectionInfo(); WifiInfo wifiInfo = wifiManager.getConnectionInfo();
WifiBroadcastReceiver.setLastWifiSsid(wifiInfo.getSSID()); WifiBroadcastReceiver.setLastWifiSsid(wifiInfo.getSSID());
WifiBroadcastReceiver.findRules(automationServiceRef.getLocationProvider()); WifiBroadcastReceiver.findRules(automationServiceRef);
break; break;
case ConnectivityManager.TYPE_MOBILE: case ConnectivityManager.TYPE_MOBILE:
boolean isRoaming = isRoaming(context); boolean isRoaming = isRoaming(context);
@ -219,7 +219,7 @@ public class ConnectivityReceiver extends BroadcastReceiver implements Automatio
// This will serve as a disconnected event. Happens if wifi is connected, then module deactivated. // This will serve as a disconnected event. Happens if wifi is connected, then module deactivated.
Miscellaneous.logEvent("i", "Connectivity", "Wifi deactivated while having been connected before.", 4); Miscellaneous.logEvent("i", "Connectivity", "Wifi deactivated while having been connected before.", 4);
WifiBroadcastReceiver.lastConnectedState = false; WifiBroadcastReceiver.lastConnectedState = false;
WifiBroadcastReceiver.findRules(automationServiceRef.getLocationProvider()); WifiBroadcastReceiver.findRules(automationServiceRef);
} }
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2012-2014 Jorrit "Chainfire" Jongma * Copyright (C) 2012-2019 Jorrit "Chainfire" Jongma
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -20,10 +20,15 @@ import android.content.Context;
import android.os.Handler; import android.os.Handler;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/** /**
* Base application class to extend from, solving some issues with * Base application class to extend from, solving some issues with
* toasts and AsyncTasks you are likely to run into * toasts and AsyncTasks you are likely to run into
*/ */
@SuppressWarnings("WeakerAccess")
public class Application extends android.app.Application { public class Application extends android.app.Application {
/** /**
* Shows a toast message * Shows a toast message
@ -31,7 +36,8 @@ public class Application extends android.app.Application {
* @param context Any context belonging to this application * @param context Any context belonging to this application
* @param message The message to show * @param message The message to show
*/ */
public static void toast(Context context, String message) { @AnyThread
public static void toast(@Nullable Context context, @NonNull String message) {
// this is a static method so it is easier to call, // this is a static method so it is easier to call,
// as the context checking and casting is done for you // as the context checking and casting is done for you
@ -45,7 +51,7 @@ public class Application extends android.app.Application {
final Context c = context; final Context c = context;
final String m = message; final String m = message;
((Application)context).runInApplicationThread(new Runnable() { ((Application) context).runInApplicationThread(new Runnable() {
@Override @Override
public void run() { public void run() {
Toast.makeText(c, m, Toast.LENGTH_LONG).show(); Toast.makeText(c, m, Toast.LENGTH_LONG).show();
@ -54,14 +60,15 @@ public class Application extends android.app.Application {
} }
} }
private static Handler mApplicationHandler = new Handler(); private static final Handler mApplicationHandler = new Handler();
/** /**
* Run a runnable in the main application thread * Run a runnable in the main application thread
* *
* @param r Runnable to run * @param r Runnable to run
*/ */
public void runInApplicationThread(Runnable r) { @AnyThread
public void runInApplicationThread(@NonNull Runnable r) {
mApplicationHandler.post(r); mApplicationHandler.post(r);
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2012-2014 Jorrit "Chainfire" Jongma * Copyright (C) 2012-2019 Jorrit "Chainfire" Jongma
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,12 +18,19 @@ package eu.chainfire.libsuperuser;
import android.os.Looper; import android.os.Looper;
import android.util.Log; import android.util.Log;
import android.os.Process;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.jens.automation2.BuildConfig; import com.jens.automation2.BuildConfig;
/** /**
* Utility class for logging and debug features that (by default) does nothing when not in debug mode * Utility class for logging and debug features that (by default) does nothing when not in debug mode
*/ */
@SuppressWarnings({"WeakerAccess", "UnusedReturnValue", "unused"})
@AnyThread
public class Debug { public class Debug {
// ----- DEBUGGING ----- // ----- DEBUGGING -----
@ -63,12 +70,14 @@ public class Debug {
public static final int LOG_GENERAL = 0x0001; public static final int LOG_GENERAL = 0x0001;
public static final int LOG_COMMAND = 0x0002; public static final int LOG_COMMAND = 0x0002;
public static final int LOG_OUTPUT = 0x0004; public static final int LOG_OUTPUT = 0x0004;
public static final int LOG_POOL = 0x0008;
public static final int LOG_NONE = 0x0000; public static final int LOG_NONE = 0x0000;
public static final int LOG_ALL = 0xFFFF; public static final int LOG_ALL = 0xFFFF;
private static int logTypes = LOG_ALL; private static int logTypes = LOG_ALL;
@Nullable
private static OnLogListener logListener = null; private static OnLogListener logListener = null;
/** /**
@ -81,7 +90,7 @@ public class Debug {
* @param typeIndicator String indicator for message type * @param typeIndicator String indicator for message type
* @param message The message to log * @param message The message to log
*/ */
private static void logCommon(int type, String typeIndicator, String message) { private static void logCommon(int type, @NonNull String typeIndicator, @NonNull String message) {
if (debug && ((logTypes & type) == type)) { if (debug && ((logTypes & type) == type)) {
if (logListener != null) { if (logListener != null) {
logListener.onLog(type, typeIndicator, message); logListener.onLog(type, typeIndicator, message);
@ -98,7 +107,7 @@ public class Debug {
* *
* @param message The message to log * @param message The message to log
*/ */
public static void log(String message) { public static void log(@NonNull String message) {
logCommon(LOG_GENERAL, "G", message); logCommon(LOG_GENERAL, "G", message);
} }
@ -109,7 +118,7 @@ public class Debug {
* *
* @param message The message to log * @param message The message to log
*/ */
public static void logCommand(String message) { public static void logCommand(@NonNull String message) {
logCommon(LOG_COMMAND, "C", message); logCommon(LOG_COMMAND, "C", message);
} }
@ -120,10 +129,19 @@ public class Debug {
* *
* @param message The message to log * @param message The message to log
*/ */
public static void logOutput(String message) { public static void logOutput(@NonNull String message) {
logCommon(LOG_OUTPUT, "O", message); logCommon(LOG_OUTPUT, "O", message);
} }
/**
* <p>Log pool event</p>
*
* @param message The message to log
*/
public static void logPool(@NonNull String message) {
logCommon(LOG_POOL, "P", message);
}
/** /**
* <p>Enable or disable logging specific types of message</p> * <p>Enable or disable logging specific types of message</p>
* *
@ -151,6 +169,7 @@ public class Debug {
* to occur.</p> * to occur.</p>
* *
* @param type LOG_* constants * @param type LOG_* constants
* @return enabled?
*/ */
public static boolean getLogTypeEnabled(int type) { public static boolean getLogTypeEnabled(int type) {
return ((logTypes & type) == type); return ((logTypes & type) == type);
@ -164,6 +183,7 @@ public class Debug {
* debug mode into account for the result.</p> * debug mode into account for the result.</p>
* *
* @param type LOG_* constants * @param type LOG_* constants
* @return enabled and in debug mode?
*/ */
public static boolean getLogTypeEnabledEffective(int type) { public static boolean getLogTypeEnabledEffective(int type) {
return getDebug() && getLogTypeEnabled(type); return getDebug() && getLogTypeEnabled(type);
@ -178,7 +198,7 @@ public class Debug {
* *
* @param onLogListener Custom log listener or NULL to revert to default * @param onLogListener Custom log listener or NULL to revert to default
*/ */
public static void setOnLogListener(OnLogListener onLogListener) { public static void setOnLogListener(@Nullable OnLogListener onLogListener) {
logListener = onLogListener; logListener = onLogListener;
} }
@ -187,6 +207,7 @@ public class Debug {
* *
* @return Current custom log handler or NULL if none is present * @return Current custom log handler or NULL if none is present
*/ */
@Nullable
public static OnLogListener getOnLogListener() { public static OnLogListener getOnLogListener() {
return logListener; return logListener;
} }
@ -236,7 +257,7 @@ public class Debug {
* @return Running on main thread ? * @return Running on main thread ?
*/ */
public static boolean onMainThread() { public static boolean onMainThread() {
return ((Looper.myLooper() != null) && (Looper.myLooper() == Looper.getMainLooper())); return ((Looper.myLooper() != null) && (Looper.myLooper() == Looper.getMainLooper()) && (Process.myUid() != 0));
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2012-2014 Jorrit "Chainfire" Jongma * Copyright (C) 2012-2019 Jorrit "Chainfire" Jongma
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -37,6 +37,7 @@ import android.content.Intent;
* window possibly obscuring SuperSU dialogs". * window possibly obscuring SuperSU dialogs".
* </p> * </p>
*/ */
@SuppressWarnings({"unused"})
public abstract class HideOverlaysReceiver extends BroadcastReceiver { public abstract class HideOverlaysReceiver extends BroadcastReceiver {
public static final String ACTION_HIDE_OVERLAYS = "eu.chainfire.supersu.action.HIDE_OVERLAYS"; public static final String ACTION_HIDE_OVERLAYS = "eu.chainfire.supersu.action.HIDE_OVERLAYS";
public static final String CATEGORY_HIDE_OVERLAYS = Intent.CATEGORY_INFO; public static final String CATEGORY_HIDE_OVERLAYS = Intent.CATEGORY_INFO;
@ -45,7 +46,7 @@ public abstract class HideOverlaysReceiver extends BroadcastReceiver {
@Override @Override
public final void onReceive(Context context, Intent intent) { public final void onReceive(Context context, Intent intent) {
if (intent.hasExtra(EXTRA_HIDE_OVERLAYS)) { if (intent.hasExtra(EXTRA_HIDE_OVERLAYS)) {
onHideOverlays(intent.getBooleanExtra(EXTRA_HIDE_OVERLAYS, false)); onHideOverlays(context, intent, intent.getBooleanExtra(EXTRA_HIDE_OVERLAYS, false));
} }
} }
@ -53,7 +54,9 @@ public abstract class HideOverlaysReceiver extends BroadcastReceiver {
* Called when overlays <em>should</em> be hidden or <em>may</em> be shown * Called when overlays <em>should</em> be hidden or <em>may</em> be shown
* again. * again.
* *
* @param context App context
* @param intent Received intent
* @param hide Should overlays be hidden? * @param hide Should overlays be hidden?
*/ */
public abstract void onHideOverlays(boolean hide); public abstract void onHideOverlays(Context context, Intent intent, boolean hide);
} }

View File

@ -0,0 +1,186 @@
/*
* Copyright (C) 2012-2019 Jorrit "Chainfire" Jongma
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.chainfire.libsuperuser;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
@SuppressWarnings("WeakerAccess")
@AnyThread
public class MarkerInputStream extends InputStream {
private static final String EXCEPTION_EOF = "EOF encountered, shell probably died";
@NonNull
private final StreamGobbler gobbler;
private final InputStream inputStream;
private final byte[] marker;
private final int markerLength;
private final int markerMaxLength;
private final byte[] read1 = new byte[1];
private final byte[] buffer = new byte[65536];
private int bufferUsed = 0;
private volatile boolean eof = false;
private volatile boolean done = false;
public MarkerInputStream(@NonNull StreamGobbler gobbler, @NonNull String marker) throws UnsupportedEncodingException {
this.gobbler = gobbler;
this.gobbler.suspendGobbling();
this.inputStream = gobbler.getInputStream();
this.marker = marker.getBytes("UTF-8");
this.markerLength = marker.length();
this.markerMaxLength = marker.length() + 5; // marker + space + exitCode(max(3)) + \n
}
@Override
public int read() throws IOException {
while (true) {
int r = read(read1, 0, 1);
if (r < 0) return -1;
if (r == 0) {
// wait for data to become available
try {
Thread.sleep(16);
} catch (InterruptedException e) {
// no action
}
continue;
}
return (int)read1[0] & 0xFF;
}
}
@Override
public int read(@NonNull byte[] b) throws IOException {
return read(b, 0, b.length);
}
private void fill(int safeSizeToWaitFor) {
// fill up our own buffer
if (isEOF()) return;
try {
int a;
while (((a = inputStream.available()) > 0) || (safeSizeToWaitFor > 0)) {
int left = buffer.length - bufferUsed;
if (left == 0) return;
int r = inputStream.read(buffer, bufferUsed, Math.max(safeSizeToWaitFor, Math.min(a, left)));
if (r >= 0) {
bufferUsed += r;
safeSizeToWaitFor -= r;
} else {
// This shouldn't happen *unless* we have both the full content and the end
// marker, otherwise the shell was interrupted/died. An IOException is raised
// in read() below if that is the case.
setEOF();
break;
}
}
} catch (IOException e) {
setEOF();
}
}
@Override
public synchronized int read(@NonNull byte[] b, int off, int len) throws IOException {
if (done) return -1;
fill(markerLength - bufferUsed);
// we need our buffer to be big enough to detect the marker
if (bufferUsed < markerLength) return 0;
// see if we have our marker
int match = -1;
for (int i = Math.max(0, bufferUsed - markerMaxLength); i < bufferUsed - markerLength; i++) {
boolean found = true;
for (int j = 0; j < markerLength; j++) {
if (buffer[i + j] != marker[j]) {
found = false;
break;
}
}
if (found) {
match = i;
break;
}
}
if (match == 0) {
// marker is at the front of the buffer
while (buffer[bufferUsed -1] != (byte)'\n') {
if (isEOF()) throw new IOException(EXCEPTION_EOF);
fill(1);
}
if (gobbler.getOnLineListener() != null) gobbler.getOnLineListener().onLine(new String(buffer, 0, bufferUsed - 1, "UTF-8"));
done = true;
return -1;
} else {
int ret;
if (match == -1) {
if (isEOF()) throw new IOException(EXCEPTION_EOF);
// marker isn't in the buffer, drain as far as possible while keeping some space
// leftover so we can still find the marker if its read is split between two fill()
// calls
ret = Math.min(len, bufferUsed - markerMaxLength);
} else {
// even if eof, it is possibly we have both the content and the end marker, which
// counts as a completed command, so we don't throw IOException here
// marker found, max drain up to marker, this will eventually cause the marker to be
// at the front of the buffer
ret = Math.min(len, match);
}
if (ret > 0) {
System.arraycopy(buffer, 0, b, off, ret);
bufferUsed -= ret;
System.arraycopy(buffer, ret, buffer, 0, bufferUsed);
} else {
try {
// prevent 100% CPU on reading from for example /dev/random
Thread.sleep(4);
} catch (Exception e) {
// no action
}
}
return ret;
}
}
@SuppressWarnings("StatementWithEmptyBody")
@Override
public synchronized void close() throws IOException {
if (!isEOF() && !done) {
// drain
byte[] buffer = new byte[1024];
while (read(buffer) >= 0) {
}
}
}
public synchronized boolean isEOF() {
return eof;
}
public synchronized void setEOF() {
eof = true;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2012-2014 Jorrit "Chainfire" Jongma * Copyright (C) 2012-2019 Jorrit "Chainfire" Jongma
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -21,11 +21,27 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.util.List; import java.util.List;
import java.util.Locale;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
/** /**
* Thread utility class continuously reading from an InputStream * Thread utility class continuously reading from an InputStream
*/ */
@SuppressWarnings({"WeakerAccess"})
public class StreamGobbler extends Thread { public class StreamGobbler extends Thread {
private static int threadCounter = 0;
private static int incThreadCounter() {
synchronized (StreamGobbler.class) {
int ret = threadCounter;
threadCounter++;
return ret;
}
}
/** /**
* Line callback interface * Line callback interface
*/ */
@ -42,10 +58,30 @@ public class StreamGobbler extends Thread {
void onLine(String line); void onLine(String line);
} }
private String shell = null; /**
private BufferedReader reader = null; * Stream closed callback interface
private List<String> writer = null; */
private OnLineListener listener = null; public interface OnStreamClosedListener {
/**
* <p>Stream closed callback</p>
*/
void onStreamClosed();
}
@NonNull
private final String shell;
@NonNull
private final InputStream inputStream;
@NonNull
private final BufferedReader reader;
@Nullable
private final List<String> writer;
@Nullable
private final OnLineListener lineListener;
@Nullable
private final OnStreamClosedListener streamClosedListener;
private volatile boolean active = true;
private volatile boolean calledOnClose = false;
/** /**
* <p>StreamGobbler constructor</p> * <p>StreamGobbler constructor</p>
@ -56,12 +92,17 @@ public class StreamGobbler extends Thread {
* *
* @param shell Name of the shell * @param shell Name of the shell
* @param inputStream InputStream to read from * @param inputStream InputStream to read from
* @param outputList List<String> to write to, or null * @param outputList {@literal List<String>} to write to, or null
*/ */
public StreamGobbler(String shell, InputStream inputStream, List<String> outputList) { @AnyThread
public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream, @Nullable List<String> outputList) {
super("Gobbler#" + incThreadCounter());
this.shell = shell; this.shell = shell;
this.inputStream = inputStream;
reader = new BufferedReader(new InputStreamReader(inputStream)); reader = new BufferedReader(new InputStreamReader(inputStream));
writer = outputList; writer = outputList;
lineListener = null;
streamClosedListener = null;
} }
/** /**
@ -74,25 +115,45 @@ public class StreamGobbler extends Thread {
* @param shell Name of the shell * @param shell Name of the shell
* @param inputStream InputStream to read from * @param inputStream InputStream to read from
* @param onLineListener OnLineListener callback * @param onLineListener OnLineListener callback
* @param onStreamClosedListener OnStreamClosedListener callback
*/ */
public StreamGobbler(String shell, InputStream inputStream, OnLineListener onLineListener) { @AnyThread
public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream, @Nullable OnLineListener onLineListener, @Nullable OnStreamClosedListener onStreamClosedListener) {
super("Gobbler#" + incThreadCounter());
this.shell = shell; this.shell = shell;
this.inputStream = inputStream;
reader = new BufferedReader(new InputStreamReader(inputStream)); reader = new BufferedReader(new InputStreamReader(inputStream));
listener = onLineListener; lineListener = onLineListener;
streamClosedListener = onStreamClosedListener;
writer = null;
} }
@Override @Override
public void run() { public void run() {
// keep reading the InputStream until it ends (or an error occurs) // keep reading the InputStream until it ends (or an error occurs)
// optionally pausing when a command is executed that consumes the InputStream itself
try { try {
String line; String line;
while ((line = reader.readLine()) != null) { while ((line = reader.readLine()) != null) {
Debug.logOutput(String.format("[%s] %s", shell, line)); Debug.logOutput(String.format(Locale.ENGLISH, "[%s] %s", shell, line));
if (writer != null) writer.add(line); if (writer != null) writer.add(line);
if (listener != null) listener.onLine(line); if (lineListener != null) lineListener.onLine(line);
while (!active) {
synchronized (this) {
try {
this.wait(128);
} catch (InterruptedException e) {
// no action
}
}
}
} }
} catch (IOException e) { } catch (IOException e) {
// reader probably closed, expected exit condition // reader probably closed, expected exit condition
if (streamClosedListener != null) {
calledOnClose = true;
streamClosedListener.onStreamClosed();
}
} }
// make sure our stream is closed and resources will be freed // make sure our stream is closed and resources will be freed
@ -101,5 +162,96 @@ public class StreamGobbler extends Thread {
} catch (IOException e) { } catch (IOException e) {
// read already closed // read already closed
} }
if (!calledOnClose) {
if (streamClosedListener != null) {
calledOnClose = true;
streamClosedListener.onStreamClosed();
}
}
}
/**
* <p>Resume consuming the input from the stream</p>
*/
@AnyThread
public void resumeGobbling() {
if (!active) {
synchronized (this) {
active = true;
this.notifyAll();
}
}
}
/**
* <p>Suspend gobbling, so other code may read from the InputStream instead</p>
*
* <p>This should <i>only</i> be called from the OnLineListener callback!</p>
*/
@AnyThread
public void suspendGobbling() {
synchronized (this) {
active = false;
this.notifyAll();
}
}
/**
* <p>Wait for gobbling to be suspended</p>
*
* <p>Obviously this cannot be called from the same thread as {@link #suspendGobbling()}</p>
*/
@WorkerThread
public void waitForSuspend() {
synchronized (this) {
while (active) {
try {
this.wait(32);
} catch (InterruptedException e) {
// no action
}
}
}
}
/**
* <p>Is gobbling suspended ?</p>
*
* @return is gobbling suspended?
*/
@AnyThread
public boolean isSuspended() {
synchronized (this) {
return !active;
}
}
/**
* <p>Get current source InputStream</p>
*
* @return source InputStream
*/
@NonNull
@AnyThread
public InputStream getInputStream() {
return inputStream;
}
/**
* <p>Get current OnLineListener</p>
*
* @return OnLineListener
*/
@Nullable
@AnyThread
public OnLineListener getOnLineListener() {
return lineListener;
}
void conditionalJoin() throws InterruptedException {
if (calledOnClose) return; // deadlock from callback, we're inside exit procedure
if (Thread.currentThread() == this) return; // can't join self
join();
} }
} }