Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ public static void importJson(String json, Context context) throws JSONException
public static ArrayList<Trigger> createTriggerlist(String content) throws JSONException {
ArrayList<Trigger> result = new ArrayList<>();
JSONObject reader = new JSONObject(content);
JSONArray array = reader.getJSONArray("trigger");
JSONArray array = reader.optJSONArray("trigger");
if (array == null) {
return result;
}
for (int i = 0; i < array.length(); i++) {
JSONObject triggerObject = array.getJSONObject(i);
result.add(Trigger.Companion.fromString(triggerObject.toString()));
Expand All @@ -44,7 +47,10 @@ public static ArrayList<Trigger> createTriggerlist(String content) throws JSONEx
public static ArrayList<Task> createTasklist(String content) throws JSONException {
ArrayList<Task> result = new ArrayList<>();
JSONObject reader = new JSONObject(content);
JSONArray array = reader.getJSONArray("tasks");
JSONArray array = reader.optJSONArray("tasks");
if (array == null) {
return result;
}
for (int i = 0; i < array.length(); i++) {
JSONObject taskObject = array.getJSONObject(i);
result.add(Task.Companion.fromString(taskObject.toString()));
Expand All @@ -54,7 +60,10 @@ public static ArrayList<Task> createTasklist(String content) throws JSONExceptio
public static ArrayList<Filter> createFilterList(String content) throws JSONException {
ArrayList<Filter> result = new ArrayList<>();
JSONObject reader = new JSONObject(content);
JSONArray array = reader.getJSONArray("filters");
JSONArray array = reader.optJSONArray("filters");
if (array == null) {
return result;
}
for (int i = 0; i < array.length(); i++) {
JSONObject filterObject = array.getJSONObject(i);
result.add(Filter.Companion.fromString(filterObject.toString()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static ca.pkay.rcloneexplorer.util.ActivityHelper.tryStartActivity;
import static ca.pkay.rcloneexplorer.util.ActivityHelper.tryStartActivityForResult;
import static ca.pkay.rcloneexplorer.util.ActivityHelper.tryStartForegroundService;
import static ca.pkay.rcloneexplorer.util.ActivityHelper.tryStartService;

import android.annotation.SuppressLint;
Expand Down Expand Up @@ -703,7 +704,7 @@ public void onServeOptionsSelected(int protocol, boolean allowRemoteAccess, Stri
default:
return;
}
tryStartService(context, intent);
tryStartForegroundService(context, intent);
}

private void emptyTrash() {
Expand Down Expand Up @@ -1306,7 +1307,7 @@ private void showFileMenu(View view, final FileItem fileItem) {
}
// GH-87: Release old server
context.stopService(new Intent(context, StreamingService.class));
tryStartService(context, intent);
tryStartForegroundService(context, intent);
});
builder.setTitle(R.string.pick_a_protocol);
builder.show();
Expand Down Expand Up @@ -1929,7 +1930,7 @@ protected Boolean doInBackground(FileItem... fileItems) {
serveIntent.putExtra(StreamingService.SERVE_PORT, port);
// GH-87: Release old stream
context.stopService(new Intent(context, StreamingService.class));
tryStartService(context, serveIntent);
tryStartForegroundService(context, serveIntent);

Uri uri = Uri.parse("http://127.0.0.1:" + port)
.buildUpon()
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/ca/pkay/rcloneexplorer/Log2File.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ public Log2File(Context context) {

public void log(String message) {
File path = context.getExternalFilesDir("logs");
if (path == null) {
FLog.e(TAG, "log: external storage not available, cannot write log");
return;
}
File logFile = new File(path, "log.txt");

clearLogsIfTooBif(logFile);
Expand Down
39 changes: 34 additions & 5 deletions app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,17 @@ public String[] getRcloneEnv(String... overwriteOptions) {
String tmpDir = context.getCacheDir().getAbsolutePath();
environmentValues.add("TMPDIR=" + tmpDir);

// ignore chtimes errors
// ref: https://github.com/rclone/rclone/issues/2446
environmentValues.add("RCLONE_LOCAL_NO_SET_MODTIME=true");
// RCLONE_LOCAL_NO_SET_MODTIME used to be set here to work around chtimes
// crashes on old Android storage stacks (ref: rclone#2446). It makes the
// local backend's Precision() report ModTimeNotSupported unconditionally
// (backend/local/local.go), which makes rclone's default file comparison
// fall back to a silent size-only check for every sync, ignoring
// modification time entirely -- changed files of the same size never get
// re-synced. A failing chtimes() is not fatal in current rclone: a failed
// SetModTime is just logged and counted as an error, the transfer itself
// still succeeds (fs/operations/operations.go). So leave modtime-setting
// enabled and let rclone's own runtime precision probe (readPrecision())
// degrade gracefully per-remote if chtimes truly isn't supported there.

// Allow the caller to overwrite any option for special cases
Iterator<String> envVarIter = environmentValues.iterator();
Expand Down Expand Up @@ -688,12 +696,33 @@ public Process sync(RemoteItem remoteItem, String localPath, String remotePath,
String localRemotePath = (remoteItem.isRemoteType(RemoteItem.LOCAL)) ? getLocalRemotePathPrefix(remoteItem, context) + "/" : "";
String remoteSection = (remotePath.compareTo("//" + remoteName) == 0) ? remoteName + ":" + localRemotePath : remoteName + ":" + localRemotePath + remotePath;

ArrayList<String> defaultParameter = new ArrayList<>(Arrays.asList("--transfers", "1", "--stats=1s", "--stats-log-level", "NOTICE", "--use-json-log"));
boolean loggingEnabled = PreferenceManager
.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.pref_key_logs), false);
ArrayList<String> defaultParameter = new ArrayList<>(Arrays.asList("--transfers", "1", "--stats=1s", "--use-json-log"));
if (!loggingEnabled) {
defaultParameter.addAll(Arrays.asList("--stats-log-level", "NOTICE", "--log-level", "INFO"));
}
ArrayList<String> directionParameter = new ArrayList<>();

if(useMD5Sum){
boolean isSftpRemote = remoteItem.isRemoteType(RemoteItem.SFTP);
if(useMD5Sum && !isSftpRemote){
defaultParameter.add("--checksum");
}
if(isSftpRemote){
// Many SFTP servers (e.g. Synology restricted shells) can't run md5sum/sha1sum
// over SSH even though the files are reachable via SFTP. Disabling the remote
// hash commands avoids "corrupted on transfer: hashes differ" failures, but it
// also means no common hash type is left to compare with --checksum. If both
// were combined, rclone silently falls back to a size-only comparison and
// ignores modification time, so changed files of the same size never get
// re-synced. Skip --checksum here and let rclone use its default size+modtime
// comparison instead.
defaultParameter.add("--sftp-md5sum-command");
defaultParameter.add("none");
defaultParameter.add("--sftp-sha1sum-command");
defaultParameter.add("none");
}
if(deleteExcluded){
defaultParameter.add("--delete-excluded");
}
Expand Down
6 changes: 3 additions & 3 deletions app/src/main/java/ca/pkay/rcloneexplorer/RcloneRcd.java
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,9 @@ public String[] getEnv() {
String tmpDir = context.getCacheDir().getAbsolutePath();
environmentValues.add("TMPDIR=" + tmpDir);

// ignore chtimes errors
// ref: https://github.com/rclone/rclone/issues/2446
environmentValues.add("RCLONE_LOCAL_NO_SET_MODTIME=true");
// See Rclone.java getRcloneEnv() for why RCLONE_LOCAL_NO_SET_MODTIME is
// deliberately not set here: it forces rclone's local-backend file
// comparison to silently ignore modification time (size-only fallback).
return environmentValues.toArray(new String[0]);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package ca.pkay.rcloneexplorer.Services;

import android.app.IntentService;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.app.Service;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.IBinder;

import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;

Expand All @@ -19,7 +19,7 @@
import ca.pkay.rcloneexplorer.util.NotificationUtils;


public class StreamingService extends IntentService {
public class StreamingService extends Service {

private static final String TAG = "StreamingService";
public static final String SERVE_PATH_ARG = "ca.pkay.rcexplorer.streaming_service.arg1";
Expand All @@ -38,109 +38,126 @@ public class StreamingService extends IntentService {
private final String CHANNEL_NAME = "Streaming service";
private final int PERSISTENT_NOTIFICATION_ID = 179;
private Rclone rclone;
private Process runningProcess;

/**
* Creates an IntentService. Invoked by your subclass's constructor.*
*/
public StreamingService() {
super("ca.pkay.rcexplorer.streamingservice");
}
private volatile Process runningProcess;
private Thread serveThread;

@Override
public void onCreate() {
super.onCreate();
setNotificationChannel();
NotificationUtils.createNotificationChannel(this,
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW,
getString(R.string.streaming_service_notification_channel_description)
);
rclone = new Rclone(this);
}

@Override
protected void onHandleIntent(@Nullable Intent intent) {
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) {
return;
stopSelf();
return START_NOT_STICKY;
}

final String servePath = intent.getStringExtra(SERVE_PATH_ARG);
final RemoteItem remote = intent.getParcelableExtra(REMOTE_ARG);
final Boolean showNotificationText = intent.getBooleanExtra(SHOW_NOTIFICATION_TEXT, false);
final boolean showNotificationText = intent.getBooleanExtra(SHOW_NOTIFICATION_TEXT, false);
final int protocol = intent.getIntExtra(SERVE_PROTOCOL, SERVE_HTTP);
final int port = intent.getIntExtra(SERVE_PORT, 8080);
final Boolean allowRemoteAccess = intent.getBooleanExtra(ALLOW_REMOTE_ACCESS, false);
final boolean allowRemoteAccess = intent.getBooleanExtra(ALLOW_REMOTE_ACCESS, false);
final String authenticationUsername = intent.getStringExtra(AUTHENTICATION_USERNAME);
final String authenticationPassword = intent.getStringExtra(AUTHENTICATION_PASSWORD);

int flags = 0;
int pendingFlags = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
flags = PendingIntent.FLAG_IMMUTABLE;
pendingFlags = PendingIntent.FLAG_IMMUTABLE;
}
Intent foregroundIntent = new Intent(this, StreamingService.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, foregroundIntent, flags);

Intent cancelIntent = new Intent(this, ServeCancelAction.class);
PendingIntent cancelPendingIntent = PendingIntent.getBroadcast(this, 0, cancelIntent, flags);
PendingIntent cancelPendingIntent = PendingIntent.getBroadcast(this, 0, cancelIntent, pendingFlags);

NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_streaming)
.setContentTitle(getString(R.string.streaming_service_notification_title))
.setPriority(NotificationCompat.PRIORITY_LOW)
.setContentIntent(pendingIntent)
.addAction(R.drawable.ic_cancel_download, getString(R.string.cancel), cancelPendingIntent);

if (showNotificationText) {
Uri uri = Uri.parse("http://127.0.0.1:" + port);
Intent webPageIntent = new Intent(Intent.ACTION_VIEW, uri);
webPageIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent webPagePendingIntent = PendingIntent.getActivity(this, 0, webPageIntent, flags);
PendingIntent webPagePendingIntent = PendingIntent.getActivity(this, 0, webPageIntent, pendingFlags);
builder.setContentIntent(webPagePendingIntent);
builder.setContentText(getString(R.string.streaming_service_notification_content, port));
}

// Promote to foreground immediately on the main thread — this is critical on GrapheneOS
// and Android 12+ where the 5-second window is strictly enforced. IntentService called
// startForeground() from a background thread, causing the service to be killed before
// promotion on stricter OS variants.
startForeground(PERSISTENT_NOTIFICATION_ID, builder.build());

switch (protocol) {
case SERVE_FTP:
runningProcess = rclone.serve(Rclone.SERVE_PROTOCOL_FTP, port, allowRemoteAccess, authenticationUsername, authenticationPassword, remote, servePath);
break;
case SERVE_WEBDAV:
runningProcess = rclone.serve(Rclone.SERVE_PROTOCOL_WEBDAV, port, allowRemoteAccess, authenticationUsername, authenticationPassword, remote, servePath);
break;
case SERVE_DLNA:
runningProcess = rclone.serve(Rclone.SERVE_PROTOCOL_DLNA, port, allowRemoteAccess, authenticationUsername, authenticationPassword, remote, servePath);
break;
case SERVE_HTTP:
default:
runningProcess = rclone.serve(Rclone.SERVE_PROTOCOL_HTTP, port, allowRemoteAccess, authenticationUsername, authenticationPassword, remote, servePath);
break;
}
stopServeProcess();

serveThread = new Thread(() -> {
switch (protocol) {
case SERVE_FTP:
runningProcess = rclone.serve(Rclone.SERVE_PROTOCOL_FTP, port, allowRemoteAccess, authenticationUsername, authenticationPassword, remote, servePath);
break;
case SERVE_WEBDAV:
runningProcess = rclone.serve(Rclone.SERVE_PROTOCOL_WEBDAV, port, allowRemoteAccess, authenticationUsername, authenticationPassword, remote, servePath);
break;
case SERVE_DLNA:
runningProcess = rclone.serve(Rclone.SERVE_PROTOCOL_DLNA, port, allowRemoteAccess, authenticationUsername, authenticationPassword, remote, servePath);
break;
case SERVE_HTTP:
default:
runningProcess = rclone.serve(Rclone.SERVE_PROTOCOL_HTTP, port, allowRemoteAccess, authenticationUsername, authenticationPassword, remote, servePath);
break;
}

if (runningProcess != null) {
try {
runningProcess.waitFor();
} catch (InterruptedException e) {
FLog.e(TAG, "onHandleIntent: error waiting for process", e);
if (runningProcess != null) {
try {
runningProcess.waitFor();
} catch (InterruptedException e) {
FLog.e(TAG, "serve thread interrupted while waiting for process", e);
Thread.currentThread().interrupt();
}
}
}

if (runningProcess != null && runningProcess.exitValue() != 0) {
rclone.logErrorOutput(runningProcess);
}
if (runningProcess != null && runningProcess.exitValue() != 0) {
rclone.logErrorOutput(runningProcess);
}

stopForeground(true);
stopForeground(true);
stopSelf();
});
serveThread.start();

return START_NOT_STICKY;
}

@Override
public void onDestroy() {
stopServeProcess();
super.onDestroy();
if (null != runningProcess) {
runningProcess.destroy();
}
}

private void setNotificationChannel() {
NotificationUtils.createNotificationChannel(this,
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW,
getString(R.string.streaming_service_notification_channel_description)
);
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}

private void stopServeProcess() {
if (runningProcess != null) {
runningProcess.destroy();
runningProcess = null;
}
if (serveThread != null) {
serveThread.interrupt();
serveThread = null;
}
}
}
13 changes: 13 additions & 0 deletions app/src/main/java/ca/pkay/rcloneexplorer/util/ActivityHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.TypedValue;
Expand Down Expand Up @@ -99,6 +100,18 @@ public static void tryStartService(@NonNull Context context, @NonNull Intent int
}
}

public static void tryStartForegroundService(@NonNull Context context, @NonNull Intent intent) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
} else {
context.startService(intent);
}
} catch (IllegalStateException e) {
FLog.e(TAG, "Host context state is invalid, not starting foreground service", e);
}
}

public static void applyTheme(Activity activity) {
applyDarkMode(activity);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ class EphemeralWorker (private var mContext: Context, workerParams: WorkerParame

override fun doWork(): Result {

if (sIsLoggingEnabled) {
log2File = Log2File(mContext)
}

registerBroadcastReceivers()

updateForegroundNotification(mNotificationManager?.updateNotification(
Expand Down
Loading