Crash the Worker Process, but keep the Android App running. Part 2

Featured image

Previous post described a method of crash-isolating 3rd party C/C++ code by using an external CLI program. Use of external programs is somewhat limiting. External programs are limited to POSIX APIs and have no easy access to Android APIs. This method will describe how to crash-isolate generic Android Java code.

Full example available at GitHub:ViliusSutkus89/CrashAndNotBurn. Precompiled APK also available.

Why is there a need to crash in the first place?

Not my code. External library. Legacy code. Cannot fix it. Stop judging. Et cetera.

The How

Service can be spawned in a separate process.

Define it in AndroidManifest.xml and give a new process name to it:

<service android:name=".CrashWorkerService" android:process=":crashWorkerService" />

WorkManager provides API on top of Service for easier usage. Not a must for our end-goal, but makes usage a bit easier. It has a few different Worker primitives:

  • Worker - Easiest to use, thread spawning already handled.
  • CoroutineWorker - Kotlin coroutines.
  • RxWorker - Implementation for RxJava.
  • ListenableWorker - Base class, bare-bones setup to roll your own threading or process model.
  • RemoteListenableWorker - ListenableWorker, which runs on a Service.

Sample implementation of RemoteListenableWorker:

public class RemoteCrashWorker extends RemoteListenableWorker {
    @NonNull
    @Override
    public ListenableFuture<Result> startRemoteWork() {
        return CallbackToFutureAdapter.getFuture(completer -> {
            RunWeirdCode();
            return completer.set(Result.success());
        });
    }
}

RemoteCrashWorker contains the to-be-executed code. It needs to be executed in a Service. Create the previously mentioned CrashWorkerService:

import androidx.work.multiprocess.RemoteWorkerService;
// Empty class, to have a class name other than RemoteWorkerService
public class CrashWorkerService extends RemoteWorkerService {
}

It does not have to be a separate class, androidx.work.multiprocess.RemoteWorkerService can be used as is, but it needs to be defined as a Service in AndroidManifest.xml.

WorkManager assumes that RemoteWorkers imply a non default WorkManager initialization. (Might be a bug, will report it eventually). Disable WorkManager default initializer in AndroidManifest.xml:

<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <meta-data
        android:name="androidx.work.WorkManagerInitializer"
        android:value="androidx.startup"
        tools:node="remove" />
</provider>

Non-default WorkManager initialization is done in the Application class:

import androidx.work.Configuration;
public class CrashAndNotBurnApplication extends Application implements Configuration.Provider {
    @NonNull
    @Override
    public Configuration getWorkManagerConfiguration() {
        return new Configuration.Builder()
                .setDefaultProcessName(getPackageName())
                .setMinimumLoggingLevel(android.util.Log.DEBUG)
                .build();
    }
}

Once that all is available, create a WorkRequest and tell WorkManager to run it:

WorkRequest remoteCrashWorkRequest = new OneTimeWorkRequest.Builder(RemoteCrashWorker.class)
  .setInputData(
    new Data.Builder()
      .putString(ARGUMENT_PACKAGE_NAME, getPackageName())
      .putString(ARGUMENT_CLASS_NAME, CrashWorkerService.class.getName())
      .build())
  .build();
WorkManager.getInstance(this).enqueue(remoteCrashWorkRequest);

If RunWeirdCode(); crashes, the “Application has stopped” popup will show up, but the main application will keep be running. Close app button refers to closing the the crashed Service process. It is A-Okay to press it.

Application has stopped

Expedited WorkRequest - Foreground Service

WorkManager may or may not start the request immediately. Background services have runtime limitations. Use a Foreground Service if the code is supposed to be running right away.

Request a automatically granted permission in AndroidManifest.xml:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

Tell WorkManager to expedite this work request:

WorkRequest remoteCrashWorkRequest = new OneTimeWorkRequest.Builder(RemoteCrashWorker.class)

  .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)

  .setInputData(
    new Data.Builder()
      .putString(ARGUMENT_PACKAGE_NAME, getPackageName())
      .putString(ARGUMENT_CLASS_NAME, CrashWorkerService.class.getName())
      .build())
  .build();
WorkManager.getInstance(this).enqueue(remoteCrashWorkRequest);

Foreground Services require a notification to be shown. Implement it in RemoteListenableWorker:

public class RemoteCrashWorker extends RemoteListenableWorker {
  // ...
  @NonNull
  @Override
  public ListenableFuture<ForegroundInfo> getForegroundInfoAsync() {
    return CallbackToFutureAdapter.getFuture(completer -> {
      Context ctx = getApplicationContext();
      String channel_id = ctx.getString(R.string.worker_notification_channel_id);
      String title = ctx.getString(R.string.crash_worker_notification_title);

      Notification notification = new NotificationCompat.Builder(ctx, channel_id)
        .setContentTitle(title)
        .setTicker(title)
        // Use whatever icon
        .setSmallIcon(android.R.drawable.ic_delete)
        .setOngoing(true)
        .build();

      // Use WorkRequest ID to generate Notification ID.
      // Each Notification ID must be unique to create a new notification for each work request.
      int notification_id = getId().hashCode();
      return completer.set(new ForegroundInfo(notification_id, notification));
    });
  }
}

A notification channel is required for Android O (API level 26) and later. Create it before getForegroundInfoAsync() resolves. Can be done in the Activity or Application to reduce amount of executed code in each Worker.

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  String id = getString(R.string.worker_notification_channel_id);
  CharSequence name = getString(R.string.worker_notification_channel_name);
  String description = getString(R.string.worker_notification_channel_description);
  NotificationChannel channel = new NotificationChannel(id, name, NotificationManager.IMPORTANCE_DEFAULT);
  channel.setDescription(description);
  getSystemService(NotificationManager.class).createNotificationChannel(channel);
}

Crash safely!