Skip to content
Open
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 @@ -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
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